From 03cf3bbb5c38aa55abd6f7d4860025f7482fcfc3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 28 Jul 2023 12:21:47 +0800 Subject: [PATCH] Update Android port * java/org/gnu/emacs/EmacsDirectoryEntry.java (EmacsDirectoryEntry): Make class final. * java/org/gnu/emacs/EmacsService.java (accessDocument) (openDocumentDirectory, openDocument, createDocument): Throw access and IO error exceptions instead of catching them. (createDirectory, deleteDocument): New functions. * src/android.c (android_init_emacs_service): Add new functions. * src/android.h (struct android_emacs_service): Likewise. * src/androidvfs.c (android_saf_exception_check): New function. Translate between Java exceptions and errno values. (android_saf_stat, android_saf_access, android_saf_delete_document) (struct android_saf_tree_vnode, android_document_id_from_name) (android_saf_tree_name, android_saf_tree_rmdir) (android_saf_tree_opendir_1, android_saf_tree_opendir) (android_saf_file_open, android_saf_file_unlink) (android_saf_new_open, android_saf_new_mkdir): Implement missing VFS operations and derive errno values from the type of any exceptions thrown. (android_vfs_init): Initialize exception classes. (android_mkdir, android_fstat): Remove trailing whitespace. --- java/org/gnu/emacs/EmacsDirectoryEntry.java | 2 +- java/org/gnu/emacs/EmacsService.java | 210 +++++++------ src/android.c | 5 + src/android.h | 2 + src/androidvfs.c | 316 ++++++++++++++++++-- 5 files changed, 410 insertions(+), 125 deletions(-) diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java index 9c10f2e8771..75c52e48002 100644 --- a/java/org/gnu/emacs/EmacsDirectoryEntry.java +++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java @@ -22,7 +22,7 @@ package org.gnu.emacs; /* Structure holding a single ``directory entry'' from a document provider. */ -public class EmacsDirectoryEntry +public final class EmacsDirectoryEntry { /* The type of this directory entry. 0 means a regular file and 1 means a directory. */ diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index bc62e050345..aa672994f12 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1723,9 +1723,12 @@ public final class EmacsService extends Service not: -1, if the file does not exist. - -2, upon a security exception or if WRITABLE the file - is not writable. - -3, upon any other error. */ + -2, if WRITABLE and the file is not writable. + -3, upon any other error. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ public int accessDocument (String uri, String documentId, boolean writable) @@ -1754,24 +1757,8 @@ public final class EmacsService extends Service Document.COLUMN_MIME_TYPE, }; - try - { - cursor = resolver.query (uriObject, projection, null, - null, null); - } - catch (SecurityException exception) - { - /* A SecurityException can be thrown if Emacs doesn't have - access to uriObject. */ - return -2; - } - catch (UnsupportedOperationException exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return -3; - } + cursor = resolver.query (uriObject, projection, null, + null, null); if (cursor == null || !cursor.moveToFirst ()) return -1; @@ -1816,13 +1803,10 @@ public final class EmacsService extends Service if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) return -3; } - catch (Exception exception) + finally { - /* Whether or not type errors cause exceptions to be signaled - is defined ``by the implementation of Cursor'', whatever - that means. */ - exception.printStackTrace (); - return -3; + /* Close the cursor if an exception occurs. */ + cursor.close (); } return 0; @@ -1832,7 +1816,11 @@ public final class EmacsService extends Service designated by the specified DOCUMENTID within the tree URI. If DOCUMENTID is NULL, use the document ID within URI itself. - Value is NULL upon failure. */ + Value is NULL upon failure. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ public Cursor openDocumentDirectory (String uri, String documentId) @@ -1861,25 +1849,8 @@ public final class EmacsService extends Service Document.COLUMN_MIME_TYPE, }; - try - { - cursor = resolver.query (uriObject, projection, null, null, - null); - } - catch (SecurityException exception) - { - /* A SecurityException can be thrown if Emacs doesn't have - access to uriObject. */ - return null; - } - catch (UnsupportedOperationException exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return null; - } - + cursor = resolver.query (uriObject, projection, null, null, + null); /* Return the cursor. */ return cursor; } @@ -1966,11 +1937,15 @@ public final class EmacsService extends Service Value is NULL upon failure or a parcel file descriptor upon success. Call `ParcelFileDescriptor.close' on this file - descriptor instead of using the `close' system call. */ + descriptor instead of using the `close' system call. + + FileNotFoundException and/or SecurityException and + UnsupportedOperationException may be thrown upon failure. */ public ParcelFileDescriptor openDocument (String uri, String documentId, boolean write, boolean truncate) + throws FileNotFoundException { Uri treeUri, documentUri; String mode; @@ -1984,40 +1959,33 @@ public final class EmacsService extends Service documentUri = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); - try + if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - 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. */ - - if (truncate) - mode = "rwt"; - else - mode = "rw"; - - fileDescriptor - = resolver.openFileDescriptor (documentUri, mode, - null); - } - else - { - /* Select the mode used to open the file. `openFile' - below means always open a stat-able file. */ + /* 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. */ - if (truncate) - /* Invalid mode! */ - return null; - else - mode = "r"; + if (truncate) + mode = "rwt"; + else + mode = "rw"; - fileDescriptor = resolver.openFile (documentUri, mode, null); - } + fileDescriptor + = resolver.openFileDescriptor (documentUri, mode, + null); } - catch (Exception exception) + else { - return null; + /* Select the mode used to open the file. `openFile' + below means always open a stat-able file. */ + + if (truncate) + /* Invalid mode! */ + return null; + else + mode = "r"; + + fileDescriptor = resolver.openFile (documentUri, mode, null); } return fileDescriptor; @@ -2030,11 +1998,15 @@ public final class EmacsService extends Service If DOCUMENTID is NULL, create the document inside the root of that tree. + Either FileNotFoundException, SecurityException or + UnsupportedOperationException may be thrown upon failure. + Return the document ID of the new file upon success, NULL otherwise. */ public String createDocument (String uri, String documentId, String name) + throws FileNotFoundException { String mimeType, separator, mime, extension; int index; @@ -2072,23 +2044,77 @@ public final class EmacsService extends Service = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, documentId); - try - { - docUri = DocumentsContract.createDocument (resolver, - directoryUri, - mimeType, name); + docUri = DocumentsContract.createDocument (resolver, + directoryUri, + mimeType, name); - if (docUri == null) - return null; + if (docUri == null) + return null; - /* Return the ID of the new document. */ - return DocumentsContract.getDocumentId (docUri); - } - catch (Exception exception) - { - exception.printStackTrace (); - } + /* Return the ID of the new document. */ + return DocumentsContract.getDocumentId (docUri); + } - return null; + /* Like `createDocument', but create a directory instead of an + ordinary document. */ + + public String + createDirectory (String uri, String documentId, String name) + throws FileNotFoundException + { + int index; + Uri directoryUri, docUri; + + /* Now parse URI. */ + directoryUri = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (directoryUri); + + /* And build a file URI referring to the directory. */ + + directoryUri + = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + documentId); + + /* If name ends with a directory separator character, delete + it. */ + + if (name.endsWith ("/")) + name = name.substring (0, name.length () - 1); + + /* From Android's perspective, directories are just ordinary + documents with the `MIME_TYPE_DIR' type. */ + + docUri = DocumentsContract.createDocument (resolver, + directoryUri, + Document.MIME_TYPE_DIR, + name); + + if (docUri == null) + return null; + + /* Return the ID of the new document. */ + return DocumentsContract.getDocumentId (docUri); + } + + /* Delete the document identified by ID from the document tree + identified by URI. Return 0 upon success and -1 upon + failure. */ + + public int + deleteDocument (String uri, String id) + throws FileNotFoundException + { + Uri uriObject; + + uriObject = Uri.parse (uri); + uriObject = DocumentsContract.buildDocumentUriUsingTree (uriObject, + id); + + if (DocumentsContract.deleteDocument (resolver, uriObject)) + return 0; + + return -1; } }; diff --git a/src/android.c b/src/android.c index 098fa6c383d..687c0b48a2a 100644 --- a/src/android.c +++ b/src/android.c @@ -1577,6 +1577,11 @@ android_init_emacs_service (void) FIND_METHOD (create_document, "createDocument", "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;)Ljava/lang/String;"); + FIND_METHOD (create_directory, "createDirectory", + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)Ljava/lang/String;"); + FIND_METHOD (delete_document, "deleteDocument", + "(Ljava/lang/String;Ljava/lang/String;)I"); #undef FIND_METHOD } diff --git a/src/android.h b/src/android.h index 94a3ad46e74..fd391fa6435 100644 --- a/src/android.h +++ b/src/android.h @@ -279,6 +279,8 @@ struct android_emacs_service jmethodID read_directory_entry; jmethodID open_document; jmethodID create_document; + jmethodID create_directory; + jmethodID delete_document; }; extern JNIEnv *android_java_env; diff --git a/src/androidvfs.c b/src/androidvfs.c index c174c35f02b..2cd50963e97 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -276,6 +276,10 @@ static struct emacs_directory_entry_class entry_class; class. */ static struct android_parcel_file_descriptor_class fd_class; +/* Global references to several exception classes. */ +static jclass file_not_found_exception, security_exception; +static jclass unsupported_operation_exception, out_of_memory_error; + /* Initialize `cursor_class' using the given JNI environment ENV. Calling this function is not necessary on Android 4.4 and earlier. */ @@ -3688,6 +3692,85 @@ android_saf_root_get_directory (int dirfd) /* Functions common to both SAF directory and file nodes. */ +/* Check for JNI exceptions, clear them, and set errno accordingly. + Also, free each of the N local references given as arguments if an + exception takes place. + + Value is 1 if an exception has taken place, 0 otherwise. + + If the exception thrown derives from FileNotFoundException, set + errno to ENOENT. + + If the exception thrown derives from SecurityException, set errno + to EACCES. + + If the exception thrown derives from UnsupportedOperationException, + set errno to ENOSYS. + + If the exception thrown derives from OutOfMemoryException, call + `memory_full'. + + If the exception thrown is anything else, set errno to EIO. */ + +static int +android_saf_exception_check (int n, ...) +{ + jthrowable exception; + JNIEnv *env; + va_list ap; + + env = android_java_env; + va_start (ap, n); + + /* First, check for an exception. */ + + if (!(*env)->ExceptionCheck (env)) + /* No exception has taken place. Return 0. */ + return 0; + + exception = (*env)->ExceptionOccurred (env); + + if (!exception) + /* JNI couldn't return a local reference to the exception. */ + memory_full (0); + + /* Clear the exception, making it safe to subsequently call other + JNI functions. */ + (*env)->ExceptionClear (env); + + /* Delete each of the N arguments. */ + + while (n > 0) + { + ANDROID_DELETE_LOCAL_REF (va_arg (ap, jobject)); + n--; + } + + /* Now set errno or signal memory_full as required. */ + + if ((*env)->IsInstanceOf (env, (jobject) exception, + file_not_found_exception)) + errno = ENOENT; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + security_exception)) + errno = EACCES; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + unsupported_operation_exception)) + errno = ENOSYS; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + out_of_memory_error)) + { + ANDROID_DELETE_LOCAL_REF ((jobject) exception); + memory_full (0); + } + else + errno = EIO; + + /* expression is still a local reference! */ + ANDROID_DELETE_LOCAL_REF ((jobject) exception); + return 1; +} + /* Return file status for the document designated by ID_NAME within the document tree identified by URI_NAME. @@ -3727,11 +3810,13 @@ android_saf_stat (const char *uri_name, const char *id_name, if (id) { - android_exception_check_2 (uri, id); + if (android_saf_exception_check (2, uri, id)) + return -1; + ANDROID_DELETE_LOCAL_REF (id); } - else - android_exception_check_1 (uri); + else if (android_saf_exception_check (1, uri)) + return -1; ANDROID_DELETE_LOCAL_REF (uri); @@ -3810,11 +3895,13 @@ android_saf_access (const char *uri_name, const char *id_name, if (id) { - android_exception_check_2 (uri, id); + if (android_saf_exception_check (2, uri, id)) + return -1; + ANDROID_DELETE_LOCAL_REF (id); } - else - android_exception_check_1 (uri); + else if (android_saf_exception_check (1, uri)) + return -1; ANDROID_DELETE_LOCAL_REF (uri); @@ -3840,6 +3927,46 @@ android_saf_access (const char *uri_name, const char *id_name, return 0; } +/* Delete the document designated by DOC_ID within the tree identified + through the URI TREE. Return 0 if the document has been deleted, + set errno and return -1 upon failure. */ + +static int +android_saf_delete_document (const char *tree, const char *doc_id) +{ + jobject id, uri; + jmethodID method; + jint rc; + + /* Build the strings holding the ID and URI. */ + id = (*android_java_env)->NewStringUTF (android_java_env, + doc_id); + android_exception_check (); + uri = (*android_java_env)->NewStringUTF (android_java_env, + tree); + android_exception_check_1 (id); + + /* Now, try to delete the document. */ + method = service_class.delete_document; + rc = (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + method, uri, id); + + if (android_saf_exception_check (2, id, uri)) + return -1; + + ANDROID_DELETE_LOCAL_REF (id); + ANDROID_DELETE_LOCAL_REF (uri); + + if (rc) + { + errno = EACCES; + return -1; + } + + return 0; +} + /* SAF directory vnode. A file within a SAF directory tree is @@ -3863,7 +3990,9 @@ struct android_saf_tree_vnode char *tree_id; /* The document ID of the directory represented, or NULL if this is - the root directory of the tree. */ + the root directory of the tree. Since file and new vnodes don't + represent the root directory, this field is always set in + them. */ char *document_id; /* The file name of this tree vnode. This is a ``path'' to the @@ -4004,7 +4133,7 @@ android_verify_jni_string (const char *name) must name a directory file within TREE_URI. If NAME is not correct for the Java ``modified UTF-8'' coding - system, return -1. + system, return -1 and set errno to ENOENT. Upon success, return 0 or 1 (contingent upon whether or not the last component within NAME is a directory) and place the document @@ -4014,7 +4143,8 @@ android_verify_jni_string (const char *name) within NAME does and is also a directory, return -2 and place the document ID of that directory within *ID. - If the designated file can't be located, return -1. */ + If the designated file can't be located, return -1 and set errno + accordingly. */ static int android_document_id_from_name (const char *tree_uri, char *name, @@ -4023,7 +4153,6 @@ android_document_id_from_name (const char *tree_uri, char *name, jobjectArray result; jstring uri; jbyteArray java_name; - size_t length; jint rc; jmethodID method; const char *doc_id; @@ -4055,7 +4184,10 @@ android_document_id_from_name (const char *tree_uri, char *name, method, uri, java_name, result); - android_exception_check_3 (result, uri, java_name); + + if (android_saf_exception_check (3, result, uri, java_name)) + goto finish; + ANDROID_DELETE_LOCAL_REF (uri); ANDROID_DELETE_LOCAL_REF (java_name); @@ -4178,7 +4310,6 @@ android_saf_tree_name (struct android_vnode *vnode, char *name, /* The document ID can't be found. */ xfree (filename); - errno = ENOENT; return NULL; } @@ -4354,9 +4485,19 @@ android_saf_tree_symlink (const char *target, struct android_vnode *vnode) static int android_saf_tree_rmdir (struct android_vnode *vnode) { - /* TODO */ - errno = ENOSYS; - return -1; + struct android_saf_tree_vnode *vp; + + vp = (struct android_saf_tree_vnode *) vnode; + + /* Don't allow deleting the root directory. */ + + if (!vp->document_id) + { + errno = EROFS; + return -1; + } + + return android_saf_delete_document (vp->tree_uri, vp->document_id); } static int @@ -4412,8 +4553,8 @@ android_saf_tree_mkdir (struct android_vnode *vnode, mode_t mode) /* Open a database Cursor containing each directory entry within the supplied SAF tree vnode VP. - Value is NULL upon failure, a local reference to the Cursor object - otherwise. */ + Value is NULL upon failure with errno set to a suitable value, a + local reference to the Cursor object otherwise. */ static jobject android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) @@ -4445,11 +4586,13 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) if (id) { - android_exception_check_2 (id, uri); + if (android_saf_exception_check (2, id, uri)) + return NULL; + ANDROID_DELETE_LOCAL_REF (id); } - else - android_exception_check_1 (uri); + else if (android_saf_exception_check (1, uri)) + return NULL; ANDROID_DELETE_LOCAL_REF (uri); @@ -4657,7 +4800,6 @@ android_saf_tree_opendir (struct android_vnode *vnode) { xfree (dir); xfree (dir->name); - errno = ENOENT; return NULL; } @@ -4880,7 +5022,10 @@ android_saf_file_open (struct android_vnode *vnode, int flags, service_class.class, method, uri, id, write, trunc); - android_exception_check_2 (uri, id); + + if (android_saf_exception_check (2, uri, id)) + return -1; + ANDROID_DELETE_LOCAL_REF (uri); ANDROID_DELETE_LOCAL_REF (id); @@ -4953,9 +5098,10 @@ android_saf_file_open (struct android_vnode *vnode, int flags, static int android_saf_file_unlink (struct android_vnode *vnode) { - /* TODO */ - errno = ENOSYS; - return -1; + struct android_saf_file_vnode *vp; + + vp = (struct android_saf_file_vnode *) vnode; + return android_saf_delete_document (vp->tree_uri, vp->document_id); } static int @@ -5114,7 +5260,7 @@ android_saf_new_open (struct android_vnode *vnode, int flags, { errno = ENOENT; return -1; - } + } /* Otherwise, try to create a new document. First, build strings for the name, ID and document URI. */ @@ -5137,7 +5283,9 @@ android_saf_new_open (struct android_vnode *vnode, int flags, service_class.class, method, uri, id, name); - android_exception_check_3 (name, id, uri); + + if (android_saf_exception_check (3, name, id, uri)) + return -1; /* Delete unused local references. */ ANDROID_DELETE_LOCAL_REF (name); @@ -5225,9 +5373,90 @@ android_saf_new_access (struct android_vnode *vnode, int mode) static int android_saf_new_mkdir (struct android_vnode *vnode, mode_t mode) { - /* TODO */ - errno = ENOSYS; - return -1; + struct android_saf_new_vnode *vp; + jstring name, id, uri, new_id; + jmethodID method; + const char *new_doc_id; + char *end; + + vp = (struct android_saf_tree_vnode *) vnode; + + /* Find the last component of vp->name. */ + end = strrchr (vp->name, '/'); + + /* VP->name must contain at least one directory separator. */ + eassert (end); + + if (end[1] == '\0') + { + /* There's a trailing directory separator. Search + backwards. */ + + end--; + while (end != vp->name && *end != '/') + end--; + + /* vp->name[0] is always a directory separator. */ + eassert (*end == '/'); + } + + /* Otherwise, try to create a new document. First, build strings + for the name, ID and document URI. */ + + name = (*android_java_env)->NewStringUTF (android_java_env, + end + 1); + android_exception_check (); + id = (*android_java_env)->NewStringUTF (android_java_env, + vp->document_id); + android_exception_check_1 (name); + uri = (*android_java_env)->NewStringUTF (android_java_env, + vp->tree_uri); + android_exception_check_2 (name, id); + + /* Next, try to create a new document and retrieve its ID. */ + + method = service_class.create_directory; + new_id = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id, + name); + + if (android_saf_exception_check (3, name, id, uri)) + return -1; + + /* Delete unused local references. */ + ANDROID_DELETE_LOCAL_REF (name); + ANDROID_DELETE_LOCAL_REF (id); + ANDROID_DELETE_LOCAL_REF (uri); + + if (!new_id) + { + /* The file couldn't be created for some reason. */ + errno = EIO; + return -1; + } + + /* Now, free VP->document_id and replace it with the service + document ID. */ + + new_doc_id = (*android_java_env)->GetStringUTFChars (android_java_env, + new_id, NULL); + + if (android_saf_exception_check (3, name, id, uri)) + return -1; + + xfree (vp->document_id); + vp->document_id = xstrdup (new_doc_id); + + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + new_id, new_doc_id); + ANDROID_DELETE_LOCAL_REF (new_id); + + /* Finally, transform this vnode into a directory vnode. */ + vp->vnode.type = ANDROID_VNODE_SAF_TREE; + vp->vnode.ops = &saf_tree_vfs_ops; + return 0; } static struct android_vdir * @@ -5449,6 +5678,29 @@ android_vfs_init (JNIEnv *env, jobject manager) android_init_cursor_class (env); android_init_entry_class (env); android_init_fd_class (env); + + /* Initialize each of the exception classes used by + `android_saf_exception_check'. */ + + old = (*env)->FindClass (env, "java/io/FileNotFoundException"); + file_not_found_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (file_not_found_exception); + + old = (*env)->FindClass (env, "java/lang/SecurityException"); + security_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (security_exception); + + old = (*env)->FindClass (env, "java/lang/UnsupportedOperationException"); + unsupported_operation_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (unsupported_operation_exception); + + old = (*env)->FindClass (env, "java/lang/OutOfMemoryError"); + out_of_memory_error = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (out_of_memory_error); } /* The replacement functions that follow have several major @@ -5606,7 +5858,7 @@ android_mkdir (const char *name, mode_t mode) rc = (*vp->ops->mkdir) (vp, mode); (*vp->ops->close) (vp); - return rc; + return rc; } /* Rename the vnode designated by SRC to the vnode designated by DST. @@ -5724,7 +5976,7 @@ android_fstat (int fd, struct stat *statb) for (tem = afs_file_descriptors; tem; tem = tem->next) { if (tem->fd == fd) - { + { memcpy (statb, &tem->statb, sizeof *statb); return 0; } -- 2.39.5