summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--java/org/gnu/emacs/EmacsNative.java10
-rw-r--r--java/org/gnu/emacs/EmacsOpenActivity.java9
-rw-r--r--java/org/gnu/emacs/EmacsService.java80
-rw-r--r--lisp/international/mule-cmds.el7
-rw-r--r--src/androidvfs.c150
5 files changed, 231 insertions, 25 deletions
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java
index 898eaef41a7..654e94b1a7d 100644
--- a/java/org/gnu/emacs/EmacsNative.java
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -281,7 +281,7 @@ public final class EmacsNative
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 final class EmacsNative
/* 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 final class EmacsNative
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
diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java
index 9ae1bf353dd..2cdfa2ec776 100644
--- a/java/org/gnu/emacs/EmacsOpenActivity.java
+++ b/java/org/gnu/emacs/EmacsOpenActivity.java
@@ -252,7 +252,7 @@ public final class EmacsOpenActivity extends Activity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
- content = EmacsService.buildContentName (uri);
+ content = EmacsService.buildContentName (uri, getContentResolver ());
return content;
}
@@ -423,6 +423,7 @@ public final class EmacsOpenActivity extends Activity
/* Obtain the intent that started Emacs. */
intent = getIntent ();
action = intent.getAction ();
+ resolver = getContentResolver ();
if (action == null)
{
@@ -536,7 +537,7 @@ public final class EmacsOpenActivity extends Activity
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 @@ public final class EmacsOpenActivity extends Activity
&& (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 @@ public final class EmacsOpenActivity extends Activity
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
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java
index 9bc40d63311..19aa3dee456 100644
--- a/java/org/gnu/emacs/EmacsService.java
+++ b/java/org/gnu/emacs/EmacsService.java
@@ -79,6 +79,7 @@ import android.os.VibrationEffect;
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 @@ public final class EmacsService extends Service
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;
+
+ displayName = null;
+ cursor = null;
- builder = new StringBuilder ("/content/by-authority/");
+ 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 @@ public final class EmacsService extends Service
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 ();
}
diff --git a/lisp/international/mule-cmds.el b/lisp/international/mule-cmds.el
index 6b4c83112e3..e80c42f523a 100644
--- a/lisp/international/mule-cmds.el
+++ b/lisp/international/mule-cmds.el
@@ -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)))
diff --git a/src/androidvfs.c b/src/androidvfs.c
index 4bb652f3eb7..9e3d5cab8cf 100644
--- a/src/androidvfs.c
+++ b/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.