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:
parent
ce29ae32d0
commit
f2e239c6a7
5 changed files with 231 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ();
|
||||
}
|
||||
|
||||
|
|
|
@ -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)))
|
||||
|
|
150
src/androidvfs.c
150
src/androidvfs.c
|
@ -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.
|
||||
|
|
Loading…
Add table
Reference in a new issue