Implement directory listing

Bug: 205715172
Test: AuthFsHostTest
Change-Id: I9ba4589636a7e1ef1261c7db14b5ecdea691e63c
diff --git a/authfs/src/file/dir.rs b/authfs/src/file/dir.rs
index 7f26fd7..f3cc6f8 100644
--- a/authfs/src/file/dir.rs
+++ b/authfs/src/file/dir.rs
@@ -17,18 +17,20 @@
 use log::warn;
 use nix::sys::stat::Mode;
 use std::collections::{hash_map, HashMap};
+use std::ffi::{CString, OsString};
 use std::io;
+use std::os::unix::ffi::OsStringExt;
 use std::path::{Path, PathBuf};
 
 use super::attr::Attr;
 use super::remote_file::RemoteFileEditor;
 use super::{validate_basename, VirtFdService, VirtFdServiceStatus};
 use crate::fsverity::VerifiedFileEditor;
-use crate::fusefs::Inode;
+use crate::fusefs::{AuthFsDirEntry, Inode};
 
 const MAX_ENTRIES: u16 = 100; // Arbitrary limit
 
-struct DirEntry {
+struct InodeInfo {
     inode: Inode,
 
     // This information is duplicated since it is also available in `AuthFs::inode_table` via the
@@ -58,7 +60,7 @@
 
     /// Mapping of entry names to the corresponding inode. The actual file/directory is stored in
     /// the global pool in fusefs.
-    entries: HashMap<PathBuf, DirEntry>,
+    entries: HashMap<PathBuf, InodeInfo>,
 }
 
 impl RemoteDirEditor {
@@ -88,7 +90,7 @@
 
         let new_remote_file =
             VerifiedFileEditor::new(RemoteFileEditor::new(self.service.clone(), new_fd));
-        self.entries.insert(basename.to_path_buf(), DirEntry { inode, is_dir: false });
+        self.entries.insert(basename.to_path_buf(), InodeInfo { inode, is_dir: false });
         let new_attr = Attr::new_file_with_mode(self.service.clone(), new_fd, mode);
         Ok((new_remote_file, new_attr))
     }
@@ -110,7 +112,7 @@
             .map_err(into_io_error)?;
 
         let new_remote_dir = RemoteDirEditor::new(self.service.clone(), new_fd);
-        self.entries.insert(basename.to_path_buf(), DirEntry { inode, is_dir: true });
+        self.entries.insert(basename.to_path_buf(), InodeInfo { inode, is_dir: true });
         let new_attr = Attr::new_dir_with_mode(self.service.clone(), new_fd, mode);
         Ok((new_remote_dir, new_attr))
     }
@@ -156,6 +158,15 @@
         self.entries.contains_key(name)
     }
 
+    pub fn retrieve_entries(&self) -> io::Result<Vec<AuthFsDirEntry>> {
+        self.entries
+            .iter()
+            .map(|(name, InodeInfo { inode, is_dir })| {
+                Ok(AuthFsDirEntry { inode: *inode, name: path_to_cstring(name)?, is_dir: *is_dir })
+            })
+            .collect::<io::Result<Vec<_>>>()
+    }
+
     fn force_delete_entry(&mut self, basename: &Path, expect_dir: bool) -> io::Result<Inode> {
         // Kernel should only give us a basename.
         debug_assert!(validate_basename(basename).is_ok());
@@ -192,7 +203,7 @@
 }
 
 /// An in-memory directory representation of a directory structure.
-pub struct InMemoryDir(HashMap<PathBuf, Inode>);
+pub struct InMemoryDir(HashMap<PathBuf, InodeInfo>);
 
 impl InMemoryDir {
     /// Creates an empty instance of `InMemoryDir`.
@@ -206,16 +217,26 @@
         self.0.len() as u16 // limited to MAX_ENTRIES
     }
 
-    /// Adds an entry (name and the inode number) to the directory. Fails if already exists. The
+    /// Adds a directory name and its inode number to the directory. Fails if already exists. The
     /// caller is responsible for ensure the inode uniqueness.
-    pub fn add_entry(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+    pub fn add_dir(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+        self.add_entry(basename, InodeInfo { inode, is_dir: true })
+    }
+
+    /// Adds a file name and its inode number to the directory. Fails if already exists. The
+    /// caller is responsible for ensure the inode uniqueness.
+    pub fn add_file(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+        self.add_entry(basename, InodeInfo { inode, is_dir: false })
+    }
+
+    fn add_entry(&mut self, basename: &Path, dir_entry: InodeInfo) -> io::Result<()> {
         validate_basename(basename)?;
         if self.0.len() >= MAX_ENTRIES.into() {
             return Err(io::Error::from_raw_os_error(libc::EMLINK));
         }
 
         if let hash_map::Entry::Vacant(entry) = self.0.entry(basename.to_path_buf()) {
-            entry.insert(inode);
+            entry.insert(dir_entry);
             Ok(())
         } else {
             Err(io::Error::from_raw_os_error(libc::EEXIST))
@@ -224,8 +245,22 @@
 
     /// Looks up an entry inode by name. `None` if not found.
     pub fn lookup_inode(&self, basename: &Path) -> Option<Inode> {
-        self.0.get(basename).copied()
+        self.0.get(basename).map(|entry| entry.inode)
     }
+
+    pub fn retrieve_entries(&self) -> io::Result<Vec<AuthFsDirEntry>> {
+        self.0
+            .iter()
+            .map(|(name, InodeInfo { inode, is_dir })| {
+                Ok(AuthFsDirEntry { inode: *inode, name: path_to_cstring(name)?, is_dir: *is_dir })
+            })
+            .collect::<io::Result<Vec<_>>>()
+    }
+}
+
+fn path_to_cstring(path: &Path) -> io::Result<CString> {
+    let bytes = OsString::from(path).into_vec();
+    CString::new(bytes).map_err(|_| io::Error::from_raw_os_error(libc::EILSEQ))
 }
 
 fn into_io_error(e: VirtFdServiceStatus) -> io::Error {
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
index 85a8371..cbd24a9 100644
--- a/authfs/src/fusefs.rs
+++ b/authfs/src/fusefs.rs
@@ -17,24 +17,24 @@
 mod mount;
 
 use anyhow::{anyhow, bail, Result};
+use fuse::filesystem::{
+    Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, GetxattrReply,
+    SetattrValid, ZeroCopyReader, ZeroCopyWriter,
+};
+use fuse::sys::OpenOptions as FuseOpenOptions;
 use log::{debug, error, warn};
 use std::collections::{btree_map, BTreeMap};
-use std::convert::TryFrom;
-use std::ffi::{CStr, OsStr};
+use std::convert::{TryFrom, TryInto};
+use std::ffi::{CStr, CString, OsStr};
 use std::io;
 use std::mem::{zeroed, MaybeUninit};
 use std::option::Option;
 use std::os::unix::ffi::OsStrExt;
 use std::path::{Component, Path, PathBuf};
 use std::sync::atomic::{AtomicU64, Ordering};
-use std::sync::Mutex;
+use std::sync::{Arc, Mutex};
 use std::time::Duration;
 
-use fuse::filesystem::{
-    Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, GetxattrReply,
-    SetattrValid, ZeroCopyReader, ZeroCopyWriter,
-};
-
 use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
 use crate::file::{
     validate_basename, Attr, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor,
@@ -119,6 +119,69 @@
     }
 }
 
+/// Data type that a directory implementation should be able to present its entry to `AuthFs`.
+#[derive(Clone)]
+pub struct AuthFsDirEntry {
+    pub inode: Inode,
+    pub name: CString,
+    pub is_dir: bool,
+}
+
+/// A snapshot of a directory entries for supporting `readdir` operation.
+///
+/// The `readdir` implementation is required by FUSE to not return any entries that have been
+/// returned previously (while it's fine to not return new entries). Snapshot is the easiest way to
+/// be compliant. See `fuse::filesystem::readdir` for more details.
+///
+/// A `DirEntriesSnapshot` is created on `opendir`, and is associated with the returned
+/// `Handle`/FD. The snapshot is deleted when the handle is released in `releasedir`.
+type DirEntriesSnapshot = Vec<AuthFsDirEntry>;
+
+/// An iterator for reading from `DirEntriesSnapshot`.
+pub struct DirEntriesSnapshotIterator {
+    /// A reference to the `DirEntriesSnapshot` in `AuthFs`.
+    snapshot: Arc<DirEntriesSnapshot>,
+
+    /// A value determined by `Self` to identify the last entry. 0 is a reserved value by FUSE to
+    /// mean reading from the beginning.
+    prev_offset: usize,
+}
+
+impl<'a> DirectoryIterator for DirEntriesSnapshotIterator {
+    fn next(&mut self) -> Option<DirEntry> {
+        // This iterator should not be the only reference to the snapshot. The snapshot should
+        // still be hold in `dir_handle_table`, i.e. when the FD is not yet closed.
+        //
+        // This code is unreachable when `readdir` is called with a closed FD. Only when the FD is
+        // not yet closed, `DirEntriesSnapshotIterator` can be created (but still short-lived
+        // during `readdir`).
+        debug_assert!(Arc::strong_count(&self.snapshot) >= 2);
+
+        // Since 0 is reserved, let's use 1-based index for the offset. This allows us to
+        // resume from the previous read in the snapshot easily.
+        let current_offset = if self.prev_offset == 0 {
+            1 // first element in the vector
+        } else {
+            self.prev_offset + 1 // next element in the vector
+        };
+        if current_offset > self.snapshot.len() {
+            None
+        } else {
+            let AuthFsDirEntry { inode, name, is_dir } = &self.snapshot[current_offset - 1];
+            let entry = DirEntry {
+                offset: current_offset as u64,
+                ino: *inode,
+                name,
+                type_: if *is_dir { libc::DT_DIR.into() } else { libc::DT_REG.into() },
+            };
+            self.prev_offset = current_offset;
+            Some(entry)
+        }
+    }
+}
+
+type DirHandleTable = BTreeMap<Handle, Arc<DirEntriesSnapshot>>;
+
 // AuthFS needs to be `Sync` to be accepted by fuse::worker::start_message_loop as a `FileSystem`.
 pub struct AuthFs {
     /// Table for `Inode` to `InodeState` lookup. This needs to be `Sync` to be used in
@@ -128,6 +191,18 @@
     /// The next available inode number.
     next_inode: AtomicU64,
 
+    /// Table for `Handle` to `Arc<DirEntriesSnapshot>` lookup. On `opendir`, a new directory handle
+    /// is created and the snapshot of the current directory is created. This is not super
+    /// efficient, but is the simplest way to be compliant to the FUSE contract (see
+    /// `fuse::filesystem::readdir`).
+    ///
+    /// Currently, no code locks `dir_handle_table` and `inode_table` at the same time to avoid
+    /// deadlock.
+    dir_handle_table: Mutex<DirHandleTable>,
+
+    /// The next available handle number.
+    next_handle: AtomicU64,
+
     /// A reader to access the remote filesystem stats, which is supposed to be of "the" output
     /// directory. We assume all output are stored in the same partition.
     remote_fs_stats_reader: RemoteFsStatsReader,
@@ -147,6 +222,8 @@
         AuthFs {
             inode_table: Mutex::new(inode_table),
             next_inode: AtomicU64::new(ROOT_INODE + 1),
+            dir_handle_table: Mutex::new(BTreeMap::new()),
+            next_handle: AtomicU64::new(1),
             remote_fs_stats_reader,
         }
     }
@@ -199,7 +276,7 @@
                                 AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() };
 
                             // Actually update the tables.
-                            dir.add_entry(name.as_ref(), new_inode)?;
+                            dir.add_dir(name.as_ref(), new_inode)?;
                             if inode_table
                                 .insert(new_inode, InodeState::new(new_dir_entry))
                                 .is_some()
@@ -223,7 +300,7 @@
                 let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
 
                 // Actually update the tables.
-                dir.add_entry(basename.as_ref(), new_inode)?;
+                dir.add_file(basename.as_ref(), new_inode)?;
                 if inode_table.insert(new_inode, InodeState::new(entry)).is_some() {
                     bail!("Unexpected to find a duplicated inode");
                 }
@@ -283,6 +360,20 @@
             unreachable!("Unexpected duplication of inode {}", new_inode);
         }
     }
+
+    fn open_dir_store_snapshot(
+        &self,
+        dir_entries: Vec<AuthFsDirEntry>,
+    ) -> io::Result<(Option<Handle>, FuseOpenOptions)> {
+        let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
+        let mut dir_handle_table = self.dir_handle_table.lock().unwrap();
+        if let btree_map::Entry::Vacant(value) = dir_handle_table.entry(handle) {
+            value.insert(Arc::new(dir_entries));
+            Ok((Some(handle), FuseOpenOptions::empty()))
+        } else {
+            unreachable!("Unexpected to see new handle {} to existing in the table", handle);
+        }
+    }
 }
 
 fn check_access_mode(flags: u32, mode: libc::c_int) -> io::Result<()> {
@@ -402,19 +493,10 @@
     Ok(total)
 }
 
-// TODO(205715172): Support enumerating directory entries.
-pub struct EmptyDirectoryIterator {}
-
-impl DirectoryIterator for EmptyDirectoryIterator {
-    fn next(&mut self) -> Option<DirEntry> {
-        None
-    }
-}
-
 impl FileSystem for AuthFs {
     type Inode = Inode;
     type Handle = Handle;
-    type DirIter = EmptyDirectoryIterator;
+    type DirIter = DirEntriesSnapshotIterator;
 
     fn max_buffer_size(&self) -> u32 {
         MAX_WRITE_BYTES
@@ -546,7 +628,7 @@
         _ctx: Context,
         inode: Self::Inode,
         flags: u32,
-    ) -> io::Result<(Option<Self::Handle>, fuse::sys::OpenOptions)> {
+    ) -> io::Result<(Option<Self::Handle>, FuseOpenOptions)> {
         // Since file handle is not really used in later operations (which use Inode directly),
         // return None as the handle.
         self.handle_inode(&inode, |config| {
@@ -566,7 +648,7 @@
             }
             // Always cache the file content. There is currently no need to support direct I/O or
             // avoid the cache buffer. Memory mapping is only possible with cache enabled.
-            Ok((None, fuse::sys::OpenOptions::KEEP_CACHE))
+            Ok((None, FuseOpenOptions::KEEP_CACHE))
         })
     }
 
@@ -578,7 +660,7 @@
         mode: u32,
         _flags: u32,
         umask: u32,
-    ) -> io::Result<(Entry, Option<Self::Handle>, fuse::sys::OpenOptions)> {
+    ) -> io::Result<(Entry, Option<Self::Handle>, FuseOpenOptions)> {
         // TODO(205172873): handle O_TRUNC and O_EXCL properly.
         let new_inode = self.create_new_entry_with_ref_count(
             parent,
@@ -606,7 +688,7 @@
             },
             // See also `open`.
             /* handle */ None,
-            fuse::sys::OpenOptions::KEEP_CACHE,
+            FuseOpenOptions::KEEP_CACHE,
         ))
     }
 
@@ -853,6 +935,53 @@
         )
     }
 
+    fn opendir(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        _flags: u32,
+    ) -> io::Result<(Option<Self::Handle>, FuseOpenOptions)> {
+        let entries = self.handle_inode(&inode, |config| match config {
+            AuthFsEntry::VerifiedNewDirectory { dir, .. } => dir.retrieve_entries(),
+            AuthFsEntry::ReadonlyDirectory { dir } => dir.retrieve_entries(),
+            _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+        })?;
+        self.open_dir_store_snapshot(entries)
+    }
+
+    fn readdir(
+        &self,
+        _ctx: Context,
+        _inode: Self::Inode,
+        handle: Self::Handle,
+        _size: u32,
+        offset: u64,
+    ) -> io::Result<Self::DirIter> {
+        let dir_handle_table = self.dir_handle_table.lock().unwrap();
+        if let Some(entry) = dir_handle_table.get(&handle) {
+            Ok(DirEntriesSnapshotIterator {
+                snapshot: entry.clone(),
+                prev_offset: offset.try_into().unwrap(),
+            })
+        } else {
+            Err(io::Error::from_raw_os_error(libc::EBADF))
+        }
+    }
+
+    fn releasedir(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        _flags: u32,
+        handle: Self::Handle,
+    ) -> io::Result<()> {
+        let mut dir_handle_table = self.dir_handle_table.lock().unwrap();
+        if dir_handle_table.remove(&handle).is_none() {
+            unreachable!("Unknown directory handle {}, inode {}", handle, inode);
+        }
+        Ok(())
+    }
+
     fn statfs(&self, _ctx: Context, _inode: Self::Inode) -> io::Result<libc::statvfs64> {
         let remote_stat = self.remote_fs_stats_reader.statfs()?;
 
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 494b082..101a349 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -519,6 +519,47 @@
     }
 
     @Test
+    public void testReadOutputDirectory() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+        // Action
+        String authfsOutputDir = MOUNT_DIR + "/3";
+        runOnMicrodroid("mkdir -p " + authfsOutputDir + "/dir/dir2/dir3");
+        runOnMicrodroid("touch " + authfsOutputDir + "/dir/dir2/dir3/file1");
+        runOnMicrodroid("touch " + authfsOutputDir + "/dir/dir2/dir3/file2");
+        runOnMicrodroid("touch " + authfsOutputDir + "/dir/dir2/dir3/file3");
+        runOnMicrodroid("touch " + authfsOutputDir + "/file");
+
+        // Verify
+        String[] actual = runOnMicrodroid("cd " + authfsOutputDir + "; find |sort").split("\n");
+        String[] expected = new String[] {
+                ".",
+                "./dir",
+                "./dir/dir2",
+                "./dir/dir2/dir3",
+                "./dir/dir2/dir3/file1",
+                "./dir/dir2/dir3/file2",
+                "./dir/dir2/dir3/file3",
+                "./file"};
+        assertEquals(expected, actual);
+
+        // Add more entries.
+        runOnMicrodroid("mkdir -p " + authfsOutputDir + "/dir2");
+        runOnMicrodroid("touch " + authfsOutputDir + "/file2");
+        // Check new entries. Also check that the types are correct.
+        actual = runOnMicrodroid(
+                "cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort").split("\n");
+        expected = new String[] {"./file", "./file2"};
+        assertEquals(expected, actual);
+        actual = runOnMicrodroid(
+                "cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort").split("\n");
+        expected = new String[] {".", "./dir", "./dir2"};
+        assertEquals(expected, actual);
+    }
+
+    @Test
     public void testChmod_File() throws Exception {
         // Setup
         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/file", "--rw-fds 3");