emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

master f2e239c6a7d 1/2: Respect display names of Android content URIs


From: Po Lu
Subject: master f2e239c6a7d 1/2: Respect display names of Android content URIs
Date: Tue, 19 Mar 2024 00:18:19 -0400 (EDT)

branch: master
commit f2e239c6a7d54ec3849a3bb783685953b6683752
Author: Po Lu <luangruo@yahoo.com>
Commit: Po Lu <luangruo@yahoo.com>

    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.
---
 java/org/gnu/emacs/EmacsNative.java       |  10 +-
 java/org/gnu/emacs/EmacsOpenActivity.java |   9 +-
 java/org/gnu/emacs/EmacsService.java      |  80 +++++++++++++++-
 lisp/international/mule-cmds.el           |   7 +-
 src/androidvfs.c                          | 150 +++++++++++++++++++++++++++---
 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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]