Respect display names of Android content URIs

* java/org/gnu/emacs/EmacsNative.java (displayNameHash): New
function.

* java/org/gnu/emacs/EmacsService.java (buildContentName): New
argument RESOLVER.  Generate names holding URI's display name if
available.  All callers changed.

* lisp/international/mule-cmds.el (set-default-coding-systems):
Fix file name coding system as utf-8-unix on Android as on Mac
OS.

* src/androidvfs.c (enum android_vnode_type): New enum
ANDROID_VNODE_CONTENT_AUTHORITY_NAMED.
(android_content_name): Register root directories for this new
type.
(displayNameHash): New function.
(android_get_content_name): New argument WITH_CHECKSUM.  If
present, treat the final two components as a pair of checksum
and display name, and verify and exclude the two.
(android_authority_name): Provide new argument as appropriate.
(android_authority_initial_name): New function.
This commit is contained in:
Po Lu 2024-03-19 12:08:17 +08:00
parent ce29ae32d0
commit f2e239c6a7
5 changed files with 231 additions and 25 deletions

View file

@ -281,7 +281,7 @@ public static native SurroundingText getSurroundingText (short window,
public static native int[] getSelection (short window);
/* Graphics functions used as a replacement for potentially buggy
/* Graphics functions used as replacements for potentially buggy
Android APIs. */
public static native void blitRect (Bitmap src, Bitmap dest, int x1,
@ -289,7 +289,6 @@ public static native void blitRect (Bitmap src, Bitmap dest, int x1,
/* Increment the generation ID of the specified BITMAP, forcing its
texture to be re-uploaded to the GPU. */
public static native void notifyPixelsChanged (Bitmap bitmap);
@ -313,6 +312,13 @@ public static native void blitRect (Bitmap src, Bitmap dest, int x1,
in the process. */
public static native boolean ftruncate (int fd);
/* Functions that assist in generating content file names. */
/* Calculate an 8 digit checksum for the byte array DISPLAYNAME
suitable for inclusion in a content file name. */
public static native String displayNameHash (byte[] displayName);
static
{
/* Older versions of Android cannot link correctly with shared

View file

@ -252,7 +252,7 @@ private class EmacsClientThread extends Thread
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
content = EmacsService.buildContentName (uri);
content = EmacsService.buildContentName (uri, getContentResolver ());
return content;
}
@ -423,6 +423,7 @@ private class EmacsClientThread extends Thread
/* Obtain the intent that started Emacs. */
intent = getIntent ();
action = intent.getAction ();
resolver = getContentResolver ();
if (action == null)
{
@ -536,7 +537,7 @@ private class EmacsClientThread extends Thread
if ((scheme = uri.getScheme ()) != null
&& scheme.equals ("content"))
{
tem1 = EmacsService.buildContentName (uri);
tem1 = EmacsService.buildContentName (uri, resolver);
attachmentString = ("'(\"" + (tem1.replace ("\\", "\\\\")
.replace ("\"", "\\\"")
.replace ("$", "\\$"))
@ -568,7 +569,8 @@ private class EmacsClientThread extends Thread
&& (scheme = uri.getScheme ()) != null
&& scheme.equals ("content"))
{
tem1 = EmacsService.buildContentName (uri);
tem1
= EmacsService.buildContentName (uri, resolver);
builder.append ("\"");
builder.append (tem1.replace ("\\", "\\\\")
.replace ("\"", "\\\"")
@ -609,7 +611,6 @@ private class EmacsClientThread extends Thread
underlying file, but it cannot be found without
opening the file and doing readlink on its file
descriptor in /proc/self/fd. */
resolver = getContentResolver ();
fd = null;
try

View file

@ -79,6 +79,7 @@
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.util.Log;
@ -1033,22 +1034,87 @@ invocation of app_process (through android-emacs) can
return false;
}
/* Return a 8 character checksum for the string STRING, after encoding
as UTF-8 data. */
public static String
getDisplayNameHash (String string)
{
byte[] encoded;
try
{
encoded = string.getBytes ("UTF-8");
return EmacsNative.displayNameHash (encoded);
}
catch (UnsupportedEncodingException exception)
{
/* This should be impossible. */
return "error";
}
}
/* Build a content file name for URI.
Return a file name within the /contents/by-authority
pseudo-directory that `android_get_content_name' can then
transform back into an encoded URI.
If a display name can be requested from URI (using the resolver
RESOLVER), append it to this file name.
A content name consists of any number of unencoded path segments
separated by `/' characters, possibly followed by a question mark
and an encoded query string. */
public static String
buildContentName (Uri uri)
buildContentName (Uri uri, ContentResolver resolver)
{
StringBuilder builder;
String displayName;
String[] projection;
Cursor cursor;
int column;
builder = new StringBuilder ("/content/by-authority/");
displayName = null;
cursor = null;
try
{
projection = new String[] { OpenableColumns.DISPLAY_NAME, };
cursor = resolver.query (uri, projection, null, null, null);
if (cursor != null)
{
cursor.moveToFirst ();
column
= cursor.getColumnIndexOrThrow (OpenableColumns.DISPLAY_NAME);
displayName
= cursor.getString (column);
/* Verify that the display name is valid, i.e. it
contains no characters unsuitable for a file name and
is nonempty. */
if (displayName.isEmpty () || displayName.contains ("/"))
displayName = null;
}
}
catch (Exception e)
{
/* Ignored. */
}
finally
{
if (cursor != null)
cursor.close ();
}
/* If a display name is available, at this point it should be the
value of displayName. */
builder = new StringBuilder (displayName != null
? "/content/by-authority-named/"
: "/content/by-authority/");
builder.append (uri.getAuthority ());
/* First, append each path segment. */
@ -1065,6 +1131,16 @@ invocation of app_process (through android-emacs) can
if (uri.getEncodedQuery () != null)
builder.append ('?').append (uri.getEncodedQuery ());
/* Append the display name. */
if (displayName != null)
{
builder.append ('/');
builder.append (getDisplayNameHash (displayName));
builder.append ('/');
builder.append (displayName);
}
return builder.toString ();
}

View file

@ -350,9 +350,10 @@ This also sets the following values:
if CODING-SYSTEM is ASCII-compatible"
(check-coding-system coding-system)
(setq-default buffer-file-coding-system coding-system)
(if (eq system-type 'darwin)
;; The file-name coding system on Darwin systems is always utf-8.
(if (or (eq system-type 'darwin)
(eq system-type 'android))
;; The file-name coding system on Darwin and Android systems is
;; always UTF-8.
(setq default-file-name-coding-system 'utf-8-unix)
(if (and (or (not coding-system)
(coding-system-get coding-system 'ascii-compatible-p)))

View file

@ -33,6 +33,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#include <sys/mman.h>
#include <stat-time.h>
#include <md5.h>
#include <linux/ashmem.h>
@ -255,6 +256,7 @@ enum android_vnode_type
ANDROID_VNODE_AFS,
ANDROID_VNODE_CONTENT,
ANDROID_VNODE_CONTENT_AUTHORITY,
ANDROID_VNODE_CONTENT_AUTHORITY_NAMED,
ANDROID_VNODE_SAF_ROOT,
ANDROID_VNODE_SAF_TREE,
ANDROID_VNODE_SAF_FILE,
@ -2435,6 +2437,7 @@ struct android_content_vdir
};
static struct android_vnode *android_authority_initial (char *, size_t);
static struct android_vnode *android_authority_initial_name (char *, size_t);
static struct android_vnode *android_saf_root_initial (char *, size_t);
/* Content provider meta-interface. This implements a vnode at
@ -2445,9 +2448,9 @@ static struct android_vnode *android_saf_root_initial (char *, size_t);
a list of each directory tree Emacs has been granted permanent
access to through the Storage Access Framework.
/content/by-authority exists on Android 4.4 and later; it contains
no directories, but provides a `name' function that converts
children into content URIs. */
/content/by-authority and /content/by-authority-named exists on
Android 4.4 and later; it contains no directories, but provides a
`name' function that converts children into content URIs. */
static struct android_vnode *android_content_name (struct android_vnode *,
char *, size_t);
@ -2490,7 +2493,7 @@ static struct android_vops content_vfs_ops =
static const char *content_directory_contents[] =
{
"storage", "by-authority",
"storage", "by-authority", "by-authority-named",
};
/* Chain consisting of all open content directory streams. */
@ -2508,8 +2511,9 @@ android_content_name (struct android_vnode *vnode, char *name,
int api;
static struct android_special_vnode content_vnodes[] = {
{ "storage", 7, android_saf_root_initial, },
{ "by-authority", 12, android_authority_initial, },
{ "storage", 7, android_saf_root_initial, },
{ "by-authority", 12, android_authority_initial, },
{ "by-authority-named", 18, android_authority_initial_name, },
};
/* Canonicalize NAME. */
@ -2551,7 +2555,7 @@ android_content_name (struct android_vnode *vnode, char *name,
call its root lookup function with the rest of NAME there. */
if (api < 19)
i = 2;
i = 3;
else if (api < 21)
i = 1;
else
@ -2855,18 +2859,59 @@ android_content_initial (char *name, size_t length)
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-prototypes"
#else /* GNUC */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-prototypes"
#endif /* __clang__ */
/* Content URI management functions. */
JNIEXPORT jstring JNICALL
NATIVE_NAME (displayNameHash) (JNIEnv *env, jobject object,
jbyteArray display_name)
{
char checksum[9], block[MD5_DIGEST_SIZE];
jbyte *data;
data = (*env)->GetByteArrayElements (env, display_name, NULL);
if (!data)
return NULL;
/* Hash the buffer. */
md5_buffer ((char *) data, (*env)->GetArrayLength (env, display_name),
block);
(*env)->ReleaseByteArrayElements (env, display_name, data, JNI_ABORT);
/* Generate the digest string. */
hexbuf_digest (checksum, (char *) block, 4);
checksum[8] = '\0';
return (*env)->NewStringUTF (env, checksum);
}
#ifdef __clang__
#pragma clang diagnostic pop
#else /* GNUC */
#pragma GCC diagnostic pop
#endif /* __clang__ */
/* Return the content URI corresponding to a `/content/by-authority'
file name, or NULL if it is invalid for some reason. FILENAME
should be relative to /content/by-authority, with no leading
directory separator character. */
directory separator character.
WITH_CHECKSUM should be true if FILENAME contains a display name and
a checksum for that display name. */
static char *
android_get_content_name (const char *filename)
android_get_content_name (const char *filename, bool with_checksum)
{
char *fill, *buffer;
size_t length;
char checksum[9], new_checksum[9], block[MD5_DIGEST_SIZE];
const char *p2, *p1;
/* Make sure FILENAME isn't obviously invalid: it must contain an
authority name and a file name component. */
@ -2888,11 +2933,55 @@ android_get_content_name (const char *filename)
return NULL;
}
if (!with_checksum)
goto no_checksum;
/* Content file names hold two components providing a display name and
a short checksum that protects against files being opened under
display names besides those provided in the content file name at
the time of generation. */
p1 = strrchr (filename, '/'); /* Display name. */
p2 = memrchr (filename, '/', p1 - filename); /* Start of checksum. */
/* If the name be excessively short or the checksum of an invalid
length, return. */
if (!p2 || (p1 - p2) != 9)
{
errno = ENOENT;
return NULL;
}
/* Copy the checksum into CHECKSUM. */
memcpy (checksum, p2 + 1, 8);
new_checksum[8] = checksum[8] = '\0';
/* Hash this string and store 8 bytes of the resulting digest into
new_checksum. */
md5_buffer (p1 + 1, strlen (p1 + 1), block);
hexbuf_digest (new_checksum, (char *) block, 4);
/* Compare both checksums. */
if (strcmp (new_checksum, checksum))
{
errno = ENOENT;
return NULL;
}
/* Remove the checksum and file display name from the URI. */
length = p2 - filename;
no_checksum:
if (length > INT_MAX)
{
errno = ENOMEM;
return NULL;
}
/* Prefix FILENAME with content:// and return the buffer containing
that URI. */
buffer = xmalloc (sizeof "content://" + length);
sprintf (buffer, "content://%s", filename);
buffer = xmalloc (sizeof "content://" + length + 1);
sprintf (buffer, "content://%.*s", (int) length, filename);
return buffer;
}
@ -2932,7 +3021,7 @@ android_check_content_access (const char *uri, int mode)
/* Content authority-based vnode implementation.
/contents/by-authority is a simple vnode implementation that converts
/content/by-authority is a simple vnode implementation that converts
components to content:// URIs.
It does not canonicalize file names by removing parent directory
@ -3039,7 +3128,14 @@ android_authority_name (struct android_vnode *vnode, char *name,
if (android_verify_jni_string (name))
goto no_entry;
uri_name = android_get_content_name (name);
if (vp->vnode.type == ANDROID_VNODE_CONTENT_AUTHORITY_NAMED)
/* This indicates that the two trailing components of NAME
provide a checksum and a file display name, to be verified,
then excluded from the content URI. */
uri_name = android_get_content_name (name, true);
else
uri_name = android_get_content_name (name, false);
if (!uri_name)
goto error;
@ -3333,6 +3429,32 @@ android_authority_initial (char *name, size_t length)
return android_authority_name (&temp.vnode, name, length);
}
/* Find the vnode designated by NAME relative to the root of the
by-authority-named directory.
If NAME is empty or a single leading separator character, return
a vnode representing the by-authority directory itself.
Otherwise, represent the remainder of NAME as a URI (without
normalizing it) and return a vnode corresponding to that.
Value may also be NULL with errno set if the designated vnode is
not available, such as when Android windowing has not been
initialized. */
static struct android_vnode *
android_authority_initial_name (char *name, size_t length)
{
struct android_authority_vnode temp;
temp.vnode.ops = &authority_vfs_ops;
temp.vnode.type = ANDROID_VNODE_CONTENT_AUTHORITY_NAMED;
temp.vnode.flags = 0;
temp.uri = NULL;
return android_authority_name (&temp.vnode, name, length);
}
/* SAF ``root'' vnode implementation.