Merge "Implement FUSE operations in AppFuse JNI."
This commit is contained in:
committed by
Android (Google) Code Review
commit
fff12cd70a
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user