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

Commit cc9a7d78 authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Implement FUSE operations in AppFuse JNI.

The CL adds the following operations.

 * FUSE_LOOKUP
 * FUSE_OPEN
 * FUSE_READ
 * FUSE_RELEASE
 * FUSE_FLUSH

BUG=25756145

Change-Id: Ib57d7d0ade3343a604a1c40e4b2c2a2d089f3715
parent e442872e
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));
    }
}