Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fff12cd7 authored by Daichi Hirono's avatar Daichi Hirono Committed by Android (Google) Code Review
Browse files

Merge "Implement FUSE operations in AppFuse JNI."

parents 1830c2aa cc9a7d78
Loading
Loading
Loading
Loading
+162 −11
Original line number Diff line number Diff line
@@ -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<uint32_t, uint64_t> 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<uint32_t, uint64_t>::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<size_t>(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 <typename T, typename S>
    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<int64_t>(env_->CallLongMethod(
                self_,
                app_fuse_get_file_size,
                static_cast<int>(inode)));
    }

    int64_t get_object_bytes(
            int inode,
            uint64_t offset,
            uint32_t size,
            void* buf) {
        const uint32_t read_size = static_cast<uint32_t>(std::min(
                static_cast<uint64_t>(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<jclass>(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) {
+40 −5
Original line number Diff line number Diff line
@@ -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);
}
+94 −9
Original line number Diff line number Diff line
@@ -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
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 {
@MediumTest
public class AppFuseTest extends AndroidTestCase {

    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();
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -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));
    }
}