diff --git a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp index f592a1fb1ae1c..f9fb85c446ad5 100644 --- a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp +++ b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp @@ -30,15 +30,13 @@ #include "jni.h" #include "JNIHelp.h" #include "android_runtime/AndroidRuntime.h" +#include "nativehelper/ScopedPrimitiveArray.h" namespace { -constexpr int min(int a, int b) { - return a < b ? a : b; -} - // Maximum number of bytes to write in one request. constexpr size_t MAX_WRITE = 256 * 1024; +constexpr size_t NUM_MAX_HANDLES = 1024; // Largest possible request. // The request size is bounded by the maximum size of a FUSE_WRITE request @@ -47,6 +45,8 @@ constexpr size_t MAX_REQUEST_SIZE = sizeof(struct fuse_in_header) + sizeof(struct fuse_write_in) + MAX_WRITE; static jclass app_fuse_class; +static jmethodID app_fuse_get_file_size; +static jmethodID app_fuse_get_object_bytes; struct FuseRequest { char buffer[MAX_REQUEST_SIZE]; @@ -79,15 +79,25 @@ public: * The class is used to access AppFuse class in Java from fuse handlers. */ class AppFuse { + JNIEnv* env_; + jobject self_; + + // Map between file handle and inode. + std::map handles_; + uint32_t handle_counter_; + public: - AppFuse(JNIEnv* /*env*/, jobject /*self*/) { - } + AppFuse(JNIEnv* env, jobject self) : + env_(env), self_(self), handle_counter_(0) {} bool handle_fuse_request(int fd, const FuseRequest& req) { ALOGV("Request op=%d", req.header().opcode); switch (req.header().opcode) { // TODO: Handle more operations that are enough to provide seekable // FD. + case FUSE_LOOKUP: + invoke_handler(fd, req, &AppFuse::handle_fuse_lookup); + return true; case FUSE_INIT: invoke_handler(fd, req, &AppFuse::handle_fuse_init); return true; @@ -96,6 +106,18 @@ public: return true; case FUSE_FORGET: return false; + case FUSE_OPEN: + invoke_handler(fd, req, &AppFuse::handle_fuse_open); + return true; + case FUSE_READ: + invoke_handler(fd, req, &AppFuse::handle_fuse_read, 8192); + return true; + case FUSE_RELEASE: + invoke_handler(fd, req, &AppFuse::handle_fuse_release, 0); + return true; + case FUSE_FLUSH: + invoke_handler(fd, req, &AppFuse::handle_fuse_flush, 0); + return true; default: { ALOGV("NOTIMPL op=%d uniq=%" PRIx64 " nid=%" PRIx64 "\n", req.header().opcode, @@ -108,10 +130,37 @@ public: } private: + int handle_fuse_lookup(const fuse_in_header& header, + const char* name, + fuse_entry_out* out, + uint32_t* /*unused*/) { + if (header.nodeid != 1) { + return -ENOENT; + } + + const int n = atoi(name); + if (n == 0) { + return -ENOENT; + } + + int64_t size = get_file_size(n); + if (size < 0) { + return -ENOENT; + } + + out->nodeid = n; + out->attr_valid = 10; + out->entry_valid = 10; + out->attr.ino = n; + out->attr.mode = S_IFREG | 0777; + out->attr.size = size; + return 0; + } + int handle_fuse_init(const fuse_in_header&, const fuse_init_in* in, fuse_init_out* out, - size_t* reply_size) { + uint32_t* reply_size) { // Kernel 2.6.16 is the first stable kernel with struct fuse_init_out // defined (fuse version 7.6). The structure is the same from 7.6 through // 7.22. Beginning with 7.23, the structure increased in size and added @@ -124,7 +173,7 @@ private: } // We limit ourselves to 15 because we don't handle BATCH_FORGET yet - out->minor = min(in->minor, 15); + out->minor = std::min(in->minor, 15u); #if defined(FUSE_COMPAT_22_INIT_OUT_SIZE) // FUSE_KERNEL_VERSION >= 23. @@ -152,7 +201,7 @@ private: int handle_fuse_getattr(const fuse_in_header& header, const fuse_getattr_in* /* in */, fuse_attr_out* out, - size_t* /*unused*/) { + uint32_t* /*unused*/) { if (header.nodeid != 1) { return -ENOENT; } @@ -163,14 +212,67 @@ private: return 0; } + int handle_fuse_open(const fuse_in_header& header, + const fuse_open_in* /* in */, + fuse_open_out* out, + uint32_t* /*unused*/) { + if (handles_.size() >= NUM_MAX_HANDLES) { + // Too many open files. + return -EMFILE; + } + uint32_t handle; + do { + handle = handle_counter_++; + } while (handles_.count(handle) != 0); + + handles_.insert(std::make_pair(handle, header.nodeid)); + out->fh = handle; + return 0; + } + + int handle_fuse_read(const fuse_in_header& /* header */, + const fuse_read_in* in, + void* out, + uint32_t* reply_size) { + const std::map::iterator it = handles_.find(in->fh); + if (it == handles_.end()) { + return -EBADF; + } + const int64_t result = get_object_bytes( + it->second, + in->offset, + in->size, + out); + if (result < 0) { + return -EIO; + } + *reply_size = static_cast(result); + return 0; + } + + int handle_fuse_release(const fuse_in_header& /* header */, + const fuse_release_in* in, + void* /* out */, + uint32_t* /* reply_size */) { + handles_.erase(in->fh); + return 0; + } + + int handle_fuse_flush(const fuse_in_header& /* header */, + const void* /* in */, + void* /* out */, + uint32_t* /* reply_size */) { + return 0; + } + template void invoke_handler(int fd, const FuseRequest& request, int (AppFuse::*handler)(const fuse_in_header&, const T*, S*, - size_t*), - size_t reply_size = sizeof(S)) { + uint32_t*), + uint32_t reply_size = sizeof(S)) { char reply_data[reply_size]; memset(reply_data, 0, reply_size); const int reply_code = (this->*handler)( @@ -186,6 +288,39 @@ private: reply_size); } + int64_t get_file_size(int inode) { + return static_cast(env_->CallLongMethod( + self_, + app_fuse_get_file_size, + static_cast(inode))); + } + + int64_t get_object_bytes( + int inode, + uint64_t offset, + uint32_t size, + void* buf) { + const uint32_t read_size = static_cast(std::min( + static_cast(size), + get_file_size(inode) - offset)); + const jbyteArray array = (jbyteArray) env_->CallObjectMethod( + self_, + app_fuse_get_object_bytes, + inode, + offset, + read_size); + if (array == nullptr) { + return -1; + } + ScopedByteArrayRO bytes(env_, array); + if (bytes.size() != read_size || bytes.get() == nullptr) { + return -1; + } + + memcpy(buf, bytes.get(), read_size); + return read_size; + } + static void fuse_reply(int fd, int unique, int reply_code, void* reply_data, size_t reply_size) { // Don't send any data for error case. @@ -219,6 +354,7 @@ jboolean com_android_mtp_AppFuse_start_app_fuse_loop( ALOGD("Start fuse loop."); while (true) { FuseRequest request; + const ssize_t result = TEMP_FAILURE_RETRY( read(fd, request.buffer, sizeof(request.buffer))); if (result < 0) { @@ -272,12 +408,27 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { ALOGE("Can't find com/android/mtp/AppFuse"); return -1; } + app_fuse_class = static_cast(env->NewGlobalRef(clazz)); if (app_fuse_class == nullptr) { ALOGE("Can't obtain global reference for com/android/mtp/AppFuse"); return -1; } + app_fuse_get_file_size = env->GetMethodID( + app_fuse_class, "getFileSize", "(I)J"); + if (app_fuse_get_file_size == nullptr) { + ALOGE("Can't find getFileSize"); + return -1; + } + + app_fuse_get_object_bytes = env->GetMethodID( + app_fuse_class, "getObjectBytes", "(IJI)[B"); + if (app_fuse_get_object_bytes == nullptr) { + ALOGE("Can't find getObjectBytes"); + return -1; + } + const int result = android::AndroidRuntime::registerNativeMethods( env, "com/android/mtp/AppFuse", gMethods, NELEM(gMethods)); if (result < 0) { diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java index 2c09ad1352666..5ffd7cf27b5f6 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java @@ -17,16 +17,14 @@ package com.android.mtp; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.storage.StorageManager; import android.util.Log; - import com.android.internal.annotations.VisibleForTesting; - import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; -import android.os.Process; - /** * TODO: Remove VisibleForTesting class. */ @@ -37,12 +35,14 @@ public class AppFuse { } private final String mName; + private final Callback mCallback; private final Thread mMessageThread; private ParcelFileDescriptor mDeviceFd; @VisibleForTesting - AppFuse(String name) { + AppFuse(String name, Callback callback) { mName = name; + mCallback = callback; mMessageThread = new Thread(new Runnable() { @Override public void run() { @@ -72,10 +72,45 @@ public class AppFuse { } } + /** + * @param i + * @throws FileNotFoundException + */ + @VisibleForTesting + public ParcelFileDescriptor openFile(int i) throws FileNotFoundException { + return ParcelFileDescriptor.open(new File( + getMountPoint(), + Integer.toString(i)), + ParcelFileDescriptor.MODE_READ_ONLY); + } + @VisibleForTesting File getMountPoint() { return new File("/mnt/appfuse/" + Process.myUid() + "_" + mName); } + static interface Callback { + long getFileSize(int inode) throws FileNotFoundException; + byte[] getObjectBytes(int inode, long offset, int size) throws IOException; + } + + @VisibleForTesting + private long getFileSize(int inode) { + try { + return mCallback.getFileSize(inode); + } catch (IOException e) { + return -1; + } + } + + @VisibleForTesting + private byte[] getObjectBytes(int inode, long offset, int size) { + try { + return mCallback.getObjectBytes(inode, offset, size); + } catch (IOException e) { + return null; + } + } + private native boolean native_start_app_fuse_loop(int fd); } diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java index b66d8ebec212b..76bd2b546472d 100644 --- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java +++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java @@ -16,24 +16,27 @@ package com.android.mtp; +import android.os.ParcelFileDescriptor; import android.os.storage.StorageManager; import android.system.ErrnoException; import android.system.Os; import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.MediumTest; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; -@SmallTest +/** + * TODO: Enable this test after adding SELinux policies for appfuse. + */ +@MediumTest public class AppFuseTest extends AndroidTestCase { - /** - * TODO: Enable this test after adding SELinux policies for appfuse. - * @throws ErrnoException - * @throws InterruptedException - */ - public void disabled_testBasic() throws ErrnoException, InterruptedException { + + public void disabled_testMount() throws ErrnoException, InterruptedException { final StorageManager storageManager = getContext().getSystemService(StorageManager.class); - final AppFuse appFuse = new AppFuse("test"); + final AppFuse appFuse = new AppFuse("test", new TestCallback()); appFuse.mount(storageManager); final File file = appFuse.getMountPoint(); assertTrue(file.isDirectory()); @@ -41,4 +44,86 @@ public class AppFuseTest extends AndroidTestCase { appFuse.close(); assertTrue(1 != Os.stat(file.getPath()).st_ino); } + + public void disabled_testOpenFile() throws IOException { + final StorageManager storageManager = getContext().getSystemService(StorageManager.class); + final int INODE = 10; + final AppFuse appFuse = new AppFuse( + "test", + new TestCallback() { + @Override + public long getFileSize(int inode) throws FileNotFoundException { + if (INODE == inode) { + return 1024; + } + throw new FileNotFoundException(); + } + }); + appFuse.mount(storageManager); + final ParcelFileDescriptor fd = appFuse.openFile(INODE); + fd.close(); + appFuse.close(); + } + + public void disabled_testOpenFile_error() { + final StorageManager storageManager = getContext().getSystemService(StorageManager.class); + final int INODE = 10; + final AppFuse appFuse = new AppFuse("test", new TestCallback()); + appFuse.mount(storageManager); + try { + appFuse.openFile(INODE); + fail(); + } catch (Throwable t) { + assertTrue(t instanceof FileNotFoundException); + } + appFuse.close(); + } + + public void disabled_testReadFile() throws IOException { + final StorageManager storageManager = getContext().getSystemService(StorageManager.class); + final int INODE = 10; + final byte[] BYTES = new byte[] { 'a', 'b', 'c', 'd', 'e' }; + final AppFuse appFuse = new AppFuse( + "test", + new TestCallback() { + @Override + public long getFileSize(int inode) throws FileNotFoundException { + if (inode == INODE) { + return BYTES.length; + } + return super.getFileSize(inode); + } + + @Override + public byte[] getObjectBytes(int inode, long offset, int size) + throws IOException { + if (inode == INODE) { + return Arrays.copyOfRange(BYTES, (int) offset, (int) offset + size); + } + return super.getObjectBytes(inode, offset, size); + } + }); + appFuse.mount(storageManager); + final ParcelFileDescriptor fd = appFuse.openFile(INODE); + try (final ParcelFileDescriptor.AutoCloseInputStream stream = + new ParcelFileDescriptor.AutoCloseInputStream(fd)) { + final byte[] buffer = new byte[1024]; + final int size = stream.read(buffer, 0, buffer.length); + assertEquals(5, size); + } + appFuse.close(); + } + + private static class TestCallback implements AppFuse.Callback { + @Override + public long getFileSize(int inode) throws FileNotFoundException { + throw new FileNotFoundException(); + } + + @Override + public byte[] getObjectBytes(int inode, long offset, int size) + throws IOException { + throw new IOException(); + } + } } diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java index bbd0a300c6a1e..9a976591aef9f 100644 --- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java +++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java @@ -203,4 +203,9 @@ public class TestMtpManager extends MtpManager { } return Arrays.copyOf(result, count); } + + @Override + byte[] getObject(int deviceId, int objectHandle, int expectedSize) throws IOException { + return mImportFileBytes.get(pack(deviceId, objectHandle)); + } }