content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.os;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.os.ProxyFileDescriptorCallback;
  import android.os.Handler;
  import android.os.Message;
  import android.os.ParcelFileDescriptor;
  import android.system.ErrnoException;
  import android.system.OsConstants;
  import android.util.Log;
  import android.util.SparseArray;
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.util.Preconditions;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.Map;
  import java.util.concurrent.ThreadFactory;
  
  public class FuseAppLoop implements Handler.Callback {
      private static final String TAG = "FuseAppLoop";
      private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
      public static final int ROOT_INODE = 1;
      private static final int MIN_INODE = 2;
      private static final ThreadFactory sDefaultThreadFactory = new ThreadFactory() {
          @Override
          public Thread newThread(Runnable r) {
              return new Thread(r, TAG);
          }
      };
      private static final int FUSE_OK = 0;
      private static final int ARGS_POOL_SIZE = 50;
  
      private final Object mLock = new Object();
      private final int mMountPointId;
      private final Thread mThread;
  
      @GuardedBy("mLock")
      private final SparseArray<CallbackEntry> mCallbackMap = new SparseArray<>();
  
      @GuardedBy("mLock")
      private final BytesMap mBytesMap = new BytesMap();
  
      @GuardedBy("mLock")
      private final LinkedList<Args> mArgsPool = new LinkedList<>();
  
      /**
       * Sequential number can be used as file name and inode in AppFuse.
       * 0 is regarded as an error, 1 is mount point. So we start the number from 2.
       */
      @GuardedBy("mLock")
      private int mNextInode = MIN_INODE;
  
      @GuardedBy("mLock")
      private long mInstance;
  
      public FuseAppLoop(
              int mountPointId, @NonNull ParcelFileDescriptor fd, @Nullable ThreadFactory factory) {
          mMountPointId = mountPointId;
          if (factory == null) {
              factory = sDefaultThreadFactory;
          }
          mInstance = native_new(fd.detachFd());
          mThread = factory.newThread(() -> {
              native_start(mInstance);
              synchronized (mLock) {
                  native_delete(mInstance);
                  mInstance = 0;
                  mBytesMap.clear();
              }
          });
          mThread.start();
      }
  
      public int registerCallback(@NonNull ProxyFileDescriptorCallback callback,
              @NonNull Handler handler) throws FuseUnavailableMountException {
          synchronized (mLock) {
              Preconditions.checkNotNull(callback);
              Preconditions.checkNotNull(handler);
              Preconditions.checkState(
                      mCallbackMap.size() < Integer.MAX_VALUE - MIN_INODE, "Too many opened files.");
              Preconditions.checkArgument(
                      Thread.currentThread().getId() != handler.getLooper().getThread().getId(),
                      "Handler must be different from the current thread");
              if (mInstance == 0) {
                  throw new FuseUnavailableMountException(mMountPointId);
              }
              int id;
              while (true) {
                  id = mNextInode;
                  mNextInode++;
                  if (mNextInode < 0) {
                      mNextInode = MIN_INODE;
                  }
                  if (mCallbackMap.get(id) == null) {
                      break;
                  }
              }
              mCallbackMap.put(id, new CallbackEntry(
                      callback, new Handler(handler.getLooper(), this)));
              return id;
          }
      }
  
      public void unregisterCallback(int id) {
          synchronized (mLock) {
              mCallbackMap.remove(id);
          }
      }
  
      public int getMountPointId() {
          return mMountPointId;
      }
  
      // Defined in fuse.h
      private static final int FUSE_LOOKUP = 1;
      private static final int FUSE_GETATTR = 3;
      private static final int FUSE_OPEN = 14;
      private static final int FUSE_READ = 15;
      private static final int FUSE_WRITE = 16;
      private static final int FUSE_RELEASE = 18;
      private static final int FUSE_FSYNC = 20;
  
      // Defined in FuseBuffer.h
      private static final int FUSE_MAX_WRITE = 128 * 1024;
  
      @Override
      public boolean handleMessage(Message msg) {
          final Args args = (Args) msg.obj;
          final CallbackEntry entry = args.entry;
          final long inode = args.inode;
          final long unique = args.unique;
          final int size = args.size;
          final long offset = args.offset;
          final byte[] data = args.data;
  
          try {
              switch (msg.what) {
                  case FUSE_LOOKUP: {
                      final long fileSize = entry.callback.onGetSize();
                      synchronized (mLock) {
                          if (mInstance != 0) {
                              native_replyLookup(mInstance, unique, inode, fileSize);
                          }
                          recycleLocked(args);
                      }
                      break;
                  }
                  case FUSE_GETATTR: {
                      final long fileSize = entry.callback.onGetSize();
                      synchronized (mLock) {
                          if (mInstance != 0) {
                              native_replyGetAttr(mInstance, unique, inode, fileSize);
                          }
                          recycleLocked(args);
                      }
                      break;
                  }
                  case FUSE_READ:
                      final int readSize = entry.callback.onRead(
                              offset, size, data);
                      synchronized (mLock) {
                          if (mInstance != 0) {
                              native_replyRead(mInstance, unique, readSize, data);
                          }
                          recycleLocked(args);
                      }
                      break;
                  case FUSE_WRITE:
                      final int writeSize = entry.callback.onWrite(offset, size, data);
                      synchronized (mLock) {
                          if (mInstance != 0) {
                              native_replyWrite(mInstance, unique, writeSize);
                          }
                          recycleLocked(args);
                      }
                      break;
                  case FUSE_FSYNC:
                      entry.callback.onFsync();
                      synchronized (mLock) {
                          if (mInstance != 0) {
                              native_replySimple(mInstance, unique, FUSE_OK);
                          }
                          recycleLocked(args);
                      }
                      break;
                  case FUSE_RELEASE:
                      entry.callback.onRelease();
                      synchronized (mLock) {
                          if (mInstance != 0) {
                              native_replySimple(mInstance, unique, FUSE_OK);
                          }
                          mBytesMap.stopUsing(entry.getThreadId());
                          recycleLocked(args);
                      }
                      break;
                  default:
                      throw new IllegalArgumentException("Unknown FUSE command: " + msg.what);
              }
          } catch (Exception error) {
              synchronized (mLock) {
                  Log.e(TAG, "", error);
                  replySimpleLocked(unique, getError(error));
                  recycleLocked(args);
              }
          }
  
          return true;
      }
  
      // Called by JNI.
      @SuppressWarnings("unused")
      @UnsupportedAppUsage
      private void onCommand(int command, long unique, long inode, long offset, int size,
              byte[] data) {
          synchronized (mLock) {
              try {
                  final Args args;
                  if (mArgsPool.size() == 0) {
                      args = new Args();
                  } else {
                      args = mArgsPool.pop();
                  }
                  args.unique = unique;
                  args.inode = inode;
                  args.offset = offset;
                  args.size = size;
                  args.data = data;
                  args.entry = getCallbackEntryOrThrowLocked(inode);
                  if (!args.entry.handler.sendMessage(
                          Message.obtain(args.entry.handler, command, 0, 0, args))) {
                      throw new ErrnoException("onCommand", OsConstants.EBADF);
                  }
              } catch (Exception error) {
                  replySimpleLocked(unique, getError(error));
              }
          }
      }
  
      // Called by JNI.
      @SuppressWarnings("unused")
      @UnsupportedAppUsage
      private byte[] onOpen(long unique, long inode) {
          synchronized (mLock) {
              try {
                  final CallbackEntry entry = getCallbackEntryOrThrowLocked(inode);
                  if (entry.opened) {
                      throw new ErrnoException("onOpen", OsConstants.EMFILE);
                  }
                  if (mInstance != 0) {
                      native_replyOpen(mInstance, unique, /* fh */ inode);
                      entry.opened = true;
                      return mBytesMap.startUsing(entry.getThreadId());
                  }
              } catch (ErrnoException error) {
                  replySimpleLocked(unique, getError(error));
              }
              return null;
          }
      }
  
      private static int getError(@NonNull Exception error) {
          if (error instanceof ErrnoException) {
              final int errno = ((ErrnoException) error).errno;
              if (errno != OsConstants.ENOSYS) {
                  return -errno;
              }
          }
          return -OsConstants.EBADF;
      }
  
      @GuardedBy("mLock")
      private CallbackEntry getCallbackEntryOrThrowLocked(long inode) throws ErrnoException {
          final CallbackEntry entry = mCallbackMap.get(checkInode(inode));
          if (entry == null) {
              throw new ErrnoException("getCallbackEntryOrThrowLocked", OsConstants.ENOENT);
          }
          return entry;
      }
  
      @GuardedBy("mLock")
      private void recycleLocked(Args args) {
          if (mArgsPool.size() < ARGS_POOL_SIZE) {
              mArgsPool.add(args);
          }
      }
  
      @GuardedBy("mLock")
      private void replySimpleLocked(long unique, int result) {
          if (mInstance != 0) {
              native_replySimple(mInstance, unique, result);
          }
      }
  
      native long native_new(int fd);
      native void native_delete(long ptr);
      native void native_start(long ptr);
  
      native void native_replySimple(long ptr, long unique, int result);
      native void native_replyOpen(long ptr, long unique, long fh);
      native void native_replyLookup(long ptr, long unique, long inode, long size);
      native void native_replyGetAttr(long ptr, long unique, long inode, long size);
      native void native_replyWrite(long ptr, long unique, int size);
      native void native_replyRead(long ptr, long unique, int size, byte[] bytes);
  
      private static int checkInode(long inode) {
          Preconditions.checkArgumentInRange(inode, MIN_INODE, Integer.MAX_VALUE, "checkInode");
          return (int) inode;
      }
  
      public static class UnmountedException extends Exception {}
  
      private static class CallbackEntry {
          final ProxyFileDescriptorCallback callback;
          final Handler handler;
          boolean opened;
  
          CallbackEntry(ProxyFileDescriptorCallback callback, Handler handler) {
              this.callback = Preconditions.checkNotNull(callback);
              this.handler = Preconditions.checkNotNull(handler);
          }
  
          long getThreadId() {
              return handler.getLooper().getThread().getId();
          }
      }
  
      /**
       * Entry for bytes map.
       */
      private static class BytesMapEntry {
          int counter = 0;
          byte[] bytes = new byte[FUSE_MAX_WRITE];
      }
  
      /**
       * Map between Thread ID and byte buffer.
       */
      private static class BytesMap {
          final Map<Long, BytesMapEntry> mEntries = new HashMap<>();
  
          byte[] startUsing(long threadId) {
              BytesMapEntry entry = mEntries.get(threadId);
              if (entry == null) {
                  entry = new BytesMapEntry();
                  mEntries.put(threadId, entry);
              }
              entry.counter++;
              return entry.bytes;
          }
  
          void stopUsing(long threadId) {
              final BytesMapEntry entry = mEntries.get(threadId);
              Preconditions.checkNotNull(entry);
              entry.counter--;
              if (entry.counter <= 0) {
                  mEntries.remove(threadId);
              }
          }
  
          void clear() {
              mEntries.clear();
          }
      }
  
      private static class Args {
          long unique;
          long inode;
          long offset;
          int size;
          byte[] data;
          CallbackEntry entry;
      }
  }
  