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.
This commit is contained in:
Po Lu 2023-08-03 10:41:40 +08:00
parent 60dda3105c
commit 91a7e9d83f
3 changed files with 280 additions and 126 deletions

View file

@ -139,11 +139,39 @@ private static final class CacheToplevel
/* Map between document names and children. */
HashMap<String, DocIdEntry> children;
/* Map between document names and file status. */
HashMap<String, StatCacheEntry> statCache;
/* Map between document IDs and cache items. */
HashMap<String, CacheEntry> 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 @@ private final class DocIdEntry
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 @@ private static final class CacheEntry
toplevel = new CacheToplevel ();
toplevel.children = new HashMap<String, DocIdEntry> ();
toplevel.statCache = new HashMap<String, StatCacheEntry> ();
toplevel.idCache = new HashMap<String, CacheEntry> ();
cacheToplevels.put (uri, toplevel);
return toplevel;
@ -311,7 +344,9 @@ private static final class CacheEntry
pruneCache ()
{
Iterator<CacheEntry> iter;
Iterator<StatCacheEntry> statIter;
CacheEntry tem;
StatCacheEntry stat;
for (CacheToplevel toplevel : cacheToplevels.values ())
{
@ -339,6 +374,25 @@ private static final class CacheEntry
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 @@ private static final class CacheEntry
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 @@ private static final class CacheEntry
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 @@ private static final class CacheEntry
toplevel = getCache (uri);
toplevel.idCache.remove (documentId);
toplevel.statCache.remove (documentId);
/* If the parent of CACHENAME is cached, remove it. */
@ -570,6 +683,7 @@ private static final class CacheEntry
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 @@ private static final class CacheEntry
});
}
/* 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 abstract Object runObject (CancellationSignal signal)
/* 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 @@ type is either NULL (in which case id should also be NULL) or
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. */
projection = new String[] {
Document.COLUMN_FLAGS,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_SIZE,
};
cursor = resolver.query (uriObject, projection, null,
null, null, signal);
if (cursor == null)
return null;
try
{
if (!cursor.moveToFirst ())
return null;
cache = cacheFileStatus (documentId, toplevel, cursor);
}
finally
{
cursor.close ();
}
/* If cache is still null, return null. */
if (cache == null)
return null;
}
/* Create the array of file status. */
/* Create the array of file status and populate it with the
information within cache. */
stat = new long[3];
try
stat[0] |= S_IRUSR;
if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
stat[0] |= S_IWUSR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
stat[0] |= S_IFCHR;
stat[1] = cache.size;
/* 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)
{
flags = cursor.getInt (flagsIndex);
/* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
just assume they're writable. */
stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
stat[0] |= S_IRUSR;
if ((flags & Document.FLAG_SUPPORTS_WRITE) != 0)
stat[0] |= S_IWUSR;
/* Directory files cannot be modified if
FLAG_DIR_SUPPORTS_CREATE is not set. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
stat[0] |= S_IFCHR;
if (cursor.isNull (sizeIndex))
stat[1] = -1; /* The size is unknown. */
else
stat[1] = cursor.getLong (sizeIndex);
tem1 = cursor.getString (typeIndex);
/* 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;
/* Directory files cannot be modified if
FLAG_DIR_SUPPORTS_CREATE is not set. */
if ((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;
if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
{
/* Content providers are allowed to not provide mtime. */
tem = cursor.getLong (mtimeIndex);
stat[2] = tem;
}
}
finally
{
cursor.close ();
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 @@ In addition, arbitrary runtime exceptions (such as
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 @@ In addition, arbitrary runtime exceptions (such as
Uri treeUri, documentUri;
String mode;
ParcelFileDescriptor fileDescriptor;
CacheToplevel toplevel;
treeUri = Uri.parse (uri);
@ -1450,35 +1581,26 @@ In addition, arbitrary runtime exceptions (such as
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;
}

View file

@ -1586,7 +1586,7 @@ In addition, arbitrary runtime exceptions (such as
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 @@ In addition, arbitrary runtime exceptions (such as
}
/* 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 @@ In addition, arbitrary runtime exceptions (such as
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 @@ In addition, arbitrary runtime exceptions (such as
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 @@ In addition, arbitrary runtime exceptions (such as
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 @@ In addition, arbitrary runtime exceptions (such as
/* 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)

View file

@ -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;
if (android_is_special_directory (name, "/content"))
xsignal2 (Qfile_error, build_string (reason), encoded);
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)