From 91a7e9d83f212e478958c2bafd59057ec816cba0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 3 Aug 2023 10:41:40 +0800 Subject: [PATCH] Update Android port * java/org/gnu/emacs/EmacsSafThread.java (CacheToplevel): (EmacsSafThread): (DocIdEntry): (getCache): (pruneCache): (cacheDirectoryFromCursor): (run): (documentIdFromName1): (statDocument1): (openDocumentDirectory1): (openDocument1): Introduce a file status cache and populate it with files within directories as they are opened. * java/org/gnu/emacs/EmacsService.java (createDocument): (createDirectory): (moveDocument): Invalidate the file status cache wherever needed. * src/fileio.c (check_vfs_filename): (Fset_file_modes): Permit `set-file-modes' to silently fail on asset and content files. --- java/org/gnu/emacs/EmacsSafThread.java | 326 +++++++++++++++++-------- java/org/gnu/emacs/EmacsService.java | 38 ++- src/fileio.c | 30 ++- 3 files changed, 274 insertions(+), 120 deletions(-) diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index 007ea9acfbd..29cd3fa6bc7 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -139,11 +139,39 @@ public final class EmacsSafThread extends HandlerThread /* Map between document names and children. */ HashMap children; + /* Map between document names and file status. */ + HashMap statCache; + /* Map between document IDs and cache items. */ HashMap idCache; }; - private final class DocIdEntry + private static final class StatCacheEntry + { + /* The time at which this cache entry was created. */ + long time; + + /* Flags, size, and modification time of this file. */ + long flags, size, mtime; + + /* Whether or not this file is a directory. */ + boolean isDirectory; + + public + StatCacheEntry () + { + time = SystemClock.uptimeMillis (); + } + + public boolean + isValid () + { + return ((SystemClock.uptimeMillis () - time) + < CACHE_INVALID_TIME * 1000); + } + }; + + private static final class DocIdEntry { /* The document ID. */ String documentId; @@ -162,10 +190,14 @@ public final class EmacsSafThread extends HandlerThread containing this entry, and TOPLEVEL is the toplevel representing it. SIGNAL is a cancellation signal. + RESOLVER is the content provider used to retrieve file + information. + Value is NULL if the file cannot be found. */ public CacheEntry - getCacheEntry (Uri tree, CacheToplevel toplevel, + getCacheEntry (ContentResolver resolver, Uri tree, + CacheToplevel toplevel, CancellationSignal signal) { Uri uri; @@ -272,6 +304,7 @@ public final class EmacsSafThread extends HandlerThread toplevel = new CacheToplevel (); toplevel.children = new HashMap (); + toplevel.statCache = new HashMap (); toplevel.idCache = new HashMap (); cacheToplevels.put (uri, toplevel); return toplevel; @@ -311,7 +344,9 @@ public final class EmacsSafThread extends HandlerThread pruneCache () { Iterator iter; + Iterator statIter; CacheEntry tem; + StatCacheEntry stat; for (CacheToplevel toplevel : cacheToplevels.values ()) { @@ -339,6 +374,25 @@ public final class EmacsSafThread extends HandlerThread iter.remove (); } + + statIter = toplevel.statCache.values ().iterator (); + + while (statIter.hasNext ()) + { + /* Get the cache entry. */ + stat = statIter.next (); + + /* If it's not valid anymore, remove it. Iterating over a + collection whose contents are being removed is + undefined unless the removal is performed using the + iterator's own `remove' function, so tem.remove cannot + be used here. */ + + if (stat.isValid ()) + continue; + + statIter.remove (); + } } postPruneMessage (); @@ -379,8 +433,60 @@ public final class EmacsSafThread extends HandlerThread return cacheEntry; } + /* Cache file status for DOCUMENTID within TOPLEVEL. Value is the + new cache entry. CURSOR is the cursor from where to retrieve the + file status, in the form of the columns COLUMN_FLAGS, + COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED. */ + + private StatCacheEntry + cacheFileStatus (String documentId, CacheToplevel toplevel, + Cursor cursor) + { + StatCacheEntry entry; + int flagsIndex, columnIndex, typeIndex; + int sizeIndex, mtimeIndex; + String type; + + /* Obtain the indices for columns wanted from this cursor. */ + flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS); + sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE); + typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); + + /* COLUMN_LAST_MODIFIED is allowed to be absent in a + conforming documents provider. */ + if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0) + return null; + + /* Get the file status from CURSOR. */ + entry = new StatCacheEntry (); + entry.flags = cursor.getInt (flagsIndex); + type = cursor.getString (typeIndex); + + if (type == null) + return null; + + entry.isDirectory = type.equals (Document.MIME_TYPE_DIR); + + if (cursor.isNull (sizeIndex)) + /* The size is unknown. */ + entry.size = -1; + else + entry.size = cursor.getLong (sizeIndex); + + /* mtimeIndex is potentially unset, since document providers + aren't obligated to provide modification times. */ + + if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex)) + entry.mtime = cursor.getLong (mtimeIndex); + + /* Finally, add this entry to the cache and return. */ + toplevel.statCache.put (documentId, entry); + return entry; + } + /* Cache the type and as many of the children of the directory - designated by DOC_ID as possible into TOPLEVEL. + designated by DOCUMENTID as possible into TOPLEVEL. CURSOR should be a cursor representing an open directory stream, with its projection consisting of at least the display name, @@ -435,6 +541,12 @@ public final class EmacsSafThread extends HandlerThread idEntry.documentId = id; entry.children.put (id, idEntry); + /* Cache the file status for ID within TOPELVEL too; if a + directory listing is being requested, it's very likely + that a series of calls for file status will follow. */ + + cacheFileStatus (id, toplevel, cursor); + /* If this constituent is a directory, don't cache any information about it. It cannot be cached without knowing its children. */ @@ -499,6 +611,7 @@ public final class EmacsSafThread extends HandlerThread toplevel = getCache (uri); toplevel.idCache.remove (documentId); + toplevel.statCache.remove (documentId); /* If the parent of CACHENAME is cached, remove it. */ @@ -570,6 +683,7 @@ public final class EmacsSafThread extends HandlerThread toplevel = getCache (uri); toplevel.idCache.remove (documentId); + toplevel.statCache.remove (documentId); /* Now remove DOCUMENTID from CACHENAME's cache entry, if any. */ @@ -619,6 +733,27 @@ public final class EmacsSafThread extends HandlerThread }); } + /* Invalidate the file status cache entry for DOCUMENTID within URI. + Call this when the contents of a file (i.e. the constituents of a + directory file) may have changed, but the document's display name + has not. */ + + public void + postInvalidateStat (final Uri uri, final String documentId) + { + handler.post (new Runnable () { + @Override + public void + run () + { + CacheToplevel toplevel; + + toplevel = getCache (uri); + toplevel.statCache.remove (documentId); + } + }); + } + /* ``Prototypes'' for nested functions that are run within the SAF @@ -857,7 +992,8 @@ public final class EmacsSafThread extends HandlerThread /* Fetch just the information for this document. */ if (cache == null) - cache = idEntry.getCacheEntry (uri, toplevel, signal); + cache = idEntry.getCacheEntry (resolver, uri, toplevel, + signal); if (cache == null) { @@ -1082,114 +1218,105 @@ public final class EmacsSafThread extends HandlerThread statDocument1 (String uri, String documentId, CancellationSignal signal) { - Uri uriObject; + Uri uriObject, tree; String[] projection; long[] stat; - int flagsIndex, columnIndex, typeIndex; - int sizeIndex, mtimeIndex, flags; - long tem; - String tem1; Cursor cursor; + CacheToplevel toplevel; + StatCacheEntry cache; - uriObject = Uri.parse (uri); + tree = Uri.parse (uri); if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); + documentId = DocumentsContract.getTreeDocumentId (tree); /* Create a document URI representing DOCUMENTID within URI's authority. */ uriObject - = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); + = DocumentsContract.buildDocumentUriUsingTree (tree, documentId); - /* Now stat this document. */ + /* See if the file status cache currently contains this + document. */ - projection = new String[] { - Document.COLUMN_FLAGS, - Document.COLUMN_LAST_MODIFIED, - Document.COLUMN_MIME_TYPE, - Document.COLUMN_SIZE, - }; + toplevel = getCache (tree); + cache = toplevel.statCache.get (documentId); - cursor = resolver.query (uriObject, projection, null, - null, null, signal); - - if (cursor == null) - return null; - - /* Obtain the indices for columns wanted from this cursor. */ - flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS); - sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE); - typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); - - if (!cursor.moveToFirst () - /* COLUMN_LAST_MODIFIED is allowed to be absent in a - conforming documents provider. */ - || flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0) + if (cache == null || !cache.isValid ()) { - cursor.close (); - return null; - } + /* Stat this document and enter its information into the + cache. */ - /* Create the array of file status. */ - stat = new long[3]; + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_SIZE, + }; - try - { - flags = cursor.getInt (flagsIndex); + cursor = resolver.query (uriObject, projection, null, + null, null, signal); - stat[0] |= S_IRUSR; - if ((flags & Document.FLAG_SUPPORTS_WRITE) != 0) - stat[0] |= S_IWUSR; + if (cursor == null) + return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) - stat[0] |= S_IFCHR; + try + { + if (!cursor.moveToFirst ()) + return null; - if (cursor.isNull (sizeIndex)) - stat[1] = -1; /* The size is unknown. */ - else - stat[1] = cursor.getLong (sizeIndex); + cache = cacheFileStatus (documentId, toplevel, cursor); + } + finally + { + cursor.close (); + } - tem1 = cursor.getString (typeIndex); + /* If cache is still null, return null. */ - /* Check if this is a directory file. */ - if (tem1.equals (Document.MIME_TYPE_DIR) - /* Files shouldn't be specials and directories at the same - time, but Android doesn't forbid document providers - from returning this information. */ - && (stat[0] & S_IFCHR) == 0) - { - /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, - just assume they're writable. */ - stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR; + if (cache == null) + return null; + } - /* Directory files cannot be modified if - FLAG_DIR_SUPPORTS_CREATE is not set. */ + /* Create the array of file status and populate it with the + information within cache. */ + stat = new long[3]; - if ((flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) - stat[0] &= ~S_IWUSR; - } + stat[0] |= S_IRUSR; + if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0) + stat[0] |= S_IWUSR; - /* If this file is neither a character special nor a - directory, indicate that it's a regular file. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) + stat[0] |= S_IFCHR; - if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) - stat[0] |= S_IFREG; + stat[1] = cache.size; - if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex)) - { - /* Content providers are allowed to not provide mtime. */ - tem = cursor.getLong (mtimeIndex); - stat[2] = tem; - } - } - finally + /* Check if this is a directory file. */ + if (cache.isDirectory + /* Files shouldn't be specials and directories at the same + time, but Android doesn't forbid document providers + from returning this information. */ + && (stat[0] & S_IFCHR) == 0) { - cursor.close (); + /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, + just assume they're writable. */ + stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR; + + /* Directory files cannot be modified if + FLAG_DIR_SUPPORTS_CREATE is not set. */ + + if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) + stat[0] &= ~S_IWUSR; } + /* If this file is neither a character special nor a + directory, indicate that it's a regular file. */ + + if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) + stat[0] |= S_IFREG; + + stat[2] = cache.mtime; return stat; } @@ -1389,6 +1516,9 @@ public final class EmacsSafThread extends HandlerThread Document.COLUMN_DISPLAY_NAME, Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, + Document.COLUMN_FLAGS, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_SIZE, }; cursor = resolver.query (uriObject, projection, null, null, @@ -1441,6 +1571,7 @@ public final class EmacsSafThread extends HandlerThread Uri treeUri, documentUri; String mode; ParcelFileDescriptor fileDescriptor; + CacheToplevel toplevel; treeUri = Uri.parse (uri); @@ -1450,35 +1581,26 @@ public final class EmacsSafThread extends HandlerThread documentUri = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); - if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) - { - /* Select the mode used to open the file. `rw' means open - a stat-able file, while `rwt' means that and to - truncate the file as well. */ + /* Select the mode used to open the file. */ + if (write) + { if (truncate) mode = "rwt"; else mode = "rw"; - - fileDescriptor - = resolver.openFileDescriptor (documentUri, mode, - signal); } else - { - /* Select the mode used to open the file. `openFile' - below means always open a stat-able file. */ + mode = "r"; - if (truncate) - /* Invalid mode! */ - return null; - else - mode = "r"; + fileDescriptor + = resolver.openFileDescriptor (documentUri, mode, + signal); - fileDescriptor = resolver.openFile (documentUri, mode, - signal); - } + /* Every time a document is opened, remove it from the file status + cache. */ + toplevel = getCache (treeUri); + toplevel.statCache.remove (documentId); return fileDescriptor; } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 8554dadd06e..a3dea368272 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1586,7 +1586,7 @@ public final class EmacsService extends Service String mimeType, separator, mime, extension; int index; MimeTypeMap singleton; - Uri directoryUri, docUri; + Uri treeUri, directoryUri, docUri; /* Try to get the MIME type for this document. Default to ``application/octet-stream''. */ @@ -1608,15 +1608,15 @@ public final class EmacsService extends Service } /* Now parse URI. */ - directoryUri = Uri.parse (uri); + treeUri = Uri.parse (uri); if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (directoryUri); + documentId = DocumentsContract.getTreeDocumentId (treeUri); /* And build a file URI referring to the directory. */ directoryUri - = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri, documentId); docUri = DocumentsContract.createDocument (resolver, @@ -1626,6 +1626,11 @@ public final class EmacsService extends Service if (docUri == null) return null; + /* Invalidate the file status of the containing directory. */ + + if (storageThread != null) + storageThread.postInvalidateStat (treeUri, documentId); + /* Return the ID of the new document. */ return DocumentsContract.getDocumentId (docUri); } @@ -1638,18 +1643,18 @@ public final class EmacsService extends Service throws FileNotFoundException { int index; - Uri directoryUri, docUri; + Uri treeUri, directoryUri, docUri; /* Now parse URI. */ - directoryUri = Uri.parse (uri); + treeUri = Uri.parse (uri); if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (directoryUri); + documentId = DocumentsContract.getTreeDocumentId (treeUri); /* And build a file URI referring to the directory. */ directoryUri - = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri, documentId); /* If name ends with a directory separator character, delete @@ -1669,7 +1674,12 @@ public final class EmacsService extends Service if (docUri == null) return null; - /* Return the ID of the new document. */ + /* Return the ID of the new document, but first invalidate the + state of the containing directory. */ + + if (storageThread != null) + storageThread.postInvalidateStat (treeUri, documentId); + return DocumentsContract.getDocumentId (docUri); } @@ -1763,7 +1773,15 @@ public final class EmacsService extends Service /* Now invalidate the caches for both DIRNAME and DOCID. */ if (storageThread != null) - storageThread.postInvalidateCacheDir (uri1, docId, dirName); + { + storageThread.postInvalidateCacheDir (uri1, docId, dirName); + + /* Invalidate the stat cache entries for both the source and + destination directories, since their contents have + changed. */ + storageThread.postInvalidateStat (uri1, dstId); + storageThread.postInvalidateStat (uri1, srcId); + } return (name != null ? DocumentsContract.getDocumentId (name) diff --git a/src/fileio.c b/src/fileio.c index 5ce933ec45b..1ccb871ce49 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -184,9 +184,12 @@ static bool e_write (int, Lisp_Object, ptrdiff_t, ptrdiff_t, /* Establish that ENCODED is not contained within a special directory whose contents are not eligible for Unix VFS operations. Signal a - `file-error' with REASON if it does. */ + `file-error' with REASON if it does. -static void + If REASON is NULL, instead return whether ENCODED is contained + within such a directory. */ + +static bool check_vfs_filename (Lisp_Object encoded, const char *reason) { #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY @@ -194,11 +197,16 @@ check_vfs_filename (Lisp_Object encoded, const char *reason) name = SSDATA (encoded); - if (android_is_special_directory (name, "/assets")) - xsignal2 (Qfile_error, build_string (reason), encoded); + if (android_is_special_directory (name, "/assets") + || android_is_special_directory (name, "/content")) + { + if (!reason) + return true; + + xsignal2 (Qfile_error, build_string (reason), encoded); + } - if (android_is_special_directory (name, "/content")) - xsignal2 (Qfile_error, build_string (reason), encoded); + return false; #endif /* defined HAVE_ANDROID && !defined ANDROID_STUBIFY */ } @@ -3657,8 +3665,14 @@ command from GNU Coreutils. */) return call4 (handler, Qset_file_modes, absname, mode, flag); encoded = ENCODE_FILE (absname); - check_vfs_filename (encoded, "Trying to change access modes of file" - " within special directory"); + + /* Silently ignore attempts to change the access modes of files + within /contents on Android, preventing errors within backup file + creation. */ + + if (check_vfs_filename (encoded, NULL)) + return Qnil; + char *fname = SSDATA (encoded); mode_t imode = XFIXNUM (mode) & 07777; if (fchmodat (AT_FDCWD, fname, imode, nofollow) != 0) -- 2.39.2