Skip to main content

starnix_core/fs/fuchsia/
remote_bundle.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::fs::fuchsia::update_info_from_attrs;
6use crate::mm::memory::MemoryObject;
7use crate::mm::{ProtectionFlags, VMEX_RESOURCE};
8use crate::task::{CurrentTask, EventHandler, Kernel, WaitCanceler, Waiter};
9use crate::vfs::fs_args::MountParams;
10use crate::vfs::{
11    CacheConfig, CacheMode, DEFAULT_BYTES_PER_BLOCK, DirEntry, DirectoryEntryType, DirentSink,
12    FileObject, FileOps, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode,
13    FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, FsString, InputBuffer, OutputBuffer, SeekTarget,
14    StatxFlags, SymlinkTarget, ValueOrSize, default_seek, emit_dotdot, fileops_impl_directory,
15    fileops_impl_noop_sync, fileops_impl_seekable, fs_node_impl_dir_readonly, fs_node_impl_not_dir,
16    fs_node_impl_symlink,
17};
18use anyhow::{Error, anyhow, ensure};
19use ext4_metadata::{Metadata, Node, NodeInfo};
20use fidl_fuchsia_io as fio;
21use starnix_logging::{impossible_error, log_warn, track_stub};
22use starnix_sync::{
23    DynamicLockDepRwLock, LockDepMutex, LockDepReadGuard, LockDepWriteGuard, RemoteBundleInnerLock,
24};
25use starnix_types::vfs::default_statfs;
26use starnix_uapi::auth::FsCred;
27use starnix_uapi::errors::{Errno, SourceContext};
28use starnix_uapi::file_mode::FileMode;
29use starnix_uapi::mount_flags::FileSystemFlags;
30use starnix_uapi::open_flags::OpenFlags;
31use starnix_uapi::vfs::FdEvents;
32use starnix_uapi::{errno, error, from_status_like_fdio, off_t, statfs};
33use std::io::Read;
34use std::sync::Arc;
35use std::sync::atomic::Ordering;
36use syncio::{zxio_node_attr_has_t, zxio_node_attributes_t};
37
38const REMOTE_BUNDLE_NODE_LRU_CAPACITY: usize = 1024;
39
40/// RemoteBundle is a remote, immutable filesystem that stores additional metadata that would
41/// otherwise not be available.  The metadata exists in the "metadata.v1" file, which contains
42/// directory, symbolic link and extended attribute information.  Only the content for files are
43/// accessed remotely as normal.
44pub struct RemoteBundle {
45    metadata: Metadata,
46    root: fio::DirectorySynchronousProxy,
47    rights: fio::Flags,
48}
49
50impl RemoteBundle {
51    /// Returns a new RemoteBundle filesystem whose path is looked up in the incoming namespace.
52    pub fn new_fs(
53        current_task: &CurrentTask,
54        mut options: FileSystemOptions,
55    ) -> Result<FileSystemHandle, Errno> {
56        let kernel = current_task.kernel();
57        let requested_path = std::str::from_utf8(&options.source)
58            .map_err(|_| errno!(EINVAL, "source path is not utf8"))?;
59        let (root_proxy, subdir) =
60            kernel.open_ns_dir(requested_path, fio::Flags::PROTOCOL_DIRECTORY)?;
61        options.source = subdir.into();
62        Self::new_fs_in_base(
63            current_task.kernel(),
64            &root_proxy,
65            options,
66            fio::PERM_READABLE | fio::PERM_EXECUTABLE,
67        )
68        .map_err(|e| errno!(EIO, format!("failed to mount remote bundle: {e}")))
69    }
70
71    /// Returns a new RemoteBundle filesystem that can be found at `options.source` relative to `base`.
72    pub fn new_fs_in_base(
73        kernel: &Kernel,
74        base: &fio::DirectorySynchronousProxy,
75        options: FileSystemOptions,
76        rights: fio::Flags,
77    ) -> Result<FileSystemHandle, Error> {
78        let (root, server_end) = fidl::endpoints::create_sync_proxy::<fio::DirectoryMarker>();
79        let path =
80            std::str::from_utf8(&options.source).map_err(|_| anyhow!("Source path is not utf8"))?;
81        base.open(path, rights, &Default::default(), server_end.into_channel())
82            .map_err(|e| anyhow!("Failed to open root: {}", e))?;
83
84        let metadata = {
85            let (file, server_end) = fidl::endpoints::create_endpoints::<fio::FileMarker>();
86            root.open(
87                "metadata.v1",
88                fio::PERM_READABLE,
89                &Default::default(),
90                server_end.into_channel(),
91            )
92            .source_context("open metadata file")?;
93            let mut file: std::fs::File = fdio::create_fd(file.into_channel().into_handle())
94                .source_context("create fd from metadata file (wrong mount path?)")?
95                .into();
96            let mut buf = Vec::new();
97            file.read_to_end(&mut buf).source_context("read metadata file")?;
98            Metadata::deserialize(&buf).source_context("deserialize metadata file")?
99        };
100
101        // Make sure the root node exists.
102        ensure!(
103            metadata.get(ext4_metadata::ROOT_INODE_NUM).is_some(),
104            "Root node does not exist in remote bundle"
105        );
106
107        if !rights.contains(fio::PERM_WRITABLE) {
108            options.flags.fetch_or(FileSystemFlags::RDONLY, Ordering::Relaxed);
109        }
110
111        let fs = FileSystem::new(
112            kernel,
113            CacheMode::Cached(CacheConfig { capacity: REMOTE_BUNDLE_NODE_LRU_CAPACITY }),
114            RemoteBundle { metadata, root, rights },
115            options,
116        )?;
117        fs.create_root(ext4_metadata::ROOT_INODE_NUM, DirectoryObject);
118        Ok(fs)
119    }
120
121    // Returns the bundle from the filesystem.  Panics if the filesystem isn't associated with a
122    // RemoteBundle.
123    fn from_fs(fs: &FileSystem) -> &RemoteBundle {
124        fs.downcast_ops::<RemoteBundle>().unwrap()
125    }
126
127    // Returns a reference to the node identified by `inode_num`.  Panics if the node is not found
128    // so this should only be used if the node is known to exist (e.g. the node must exist after
129    // `lookup` has run for the relevant node).
130    fn get_node(&self, inode_num: u64) -> &Node {
131        self.metadata.get(inode_num).unwrap()
132    }
133
134    fn get_xattr(&self, node: &FsNode, name: &FsStr) -> Result<ValueOrSize<FsString>, Errno> {
135        let value = &self
136            .get_node(node.ino)
137            .extended_attributes
138            .get(&**name)
139            .ok_or_else(|| errno!(ENODATA))?[..];
140        Ok(FsString::from(value).into())
141    }
142
143    fn list_xattrs(&self, node: &FsNode) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
144        Ok(self
145            .get_node(node.ino)
146            .extended_attributes
147            .keys()
148            .map(|k| FsString::from(&k[..]))
149            .collect::<Vec<_>>()
150            .into())
151    }
152}
153
154impl FileSystemOps for RemoteBundle {
155    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
156        const REMOTE_BUNDLE_FS_MAGIC: u32 = u32::from_be_bytes(*b"bndl");
157        let mut stats = default_statfs(REMOTE_BUNDLE_FS_MAGIC);
158        // Provide a placeholder non-zero block count to satisfy CTS StatFsTest.
159        // 100,000 blocks (approx 400MB with 4KB page size) is safe and adequate
160        // to prevent other applications from thinking the disk is full/too small.
161        track_stub!(TODO("https://fxbug.dev/540853536"), "RemoteBundle statfs block count");
162        stats.f_blocks = 100000;
163        Ok(stats)
164    }
165    fn name(&self) -> &'static FsStr {
166        "remote_bundle".into()
167    }
168    fn reconfigure(
169        &self,
170        fs: &FileSystem,
171        _current_task: &CurrentTask,
172        flags: FileSystemFlags,
173        params: &MountParams,
174    ) -> Result<(), Errno> {
175        if !params.is_empty() {
176            track_stub!(TODO("https://fxbug.dev/322875506"), "MS_REMOUNT: Updating data");
177        }
178        fs.options.flags.store(flags | FileSystemFlags::RDONLY, Ordering::Relaxed);
179        Ok(())
180    }
181}
182
183struct File {
184    inner: LockDepMutex<Inner, RemoteBundleInnerLock>,
185}
186
187enum Inner {
188    NeedsVmo(fio::FileSynchronousProxy),
189    Memory(Arc<MemoryObject>),
190}
191
192impl Inner {
193    fn get_memory(&mut self) -> Result<Arc<MemoryObject>, Errno> {
194        if let Inner::NeedsVmo(file) = &*self {
195            let memory = Arc::new(MemoryObject::from(
196                file.get_backing_memory(fio::VmoFlags::READ, zx::MonotonicInstant::INFINITE)
197                    .map_err(|err| errno!(EIO, format!("Error {err} on GetBackingMemory")))?
198                    .map_err(|s| from_status_like_fdio!(zx::Status::err_from_raw(s)))?,
199            ));
200            *self = Inner::Memory(memory);
201        }
202        let Inner::Memory(memory) = &*self else { unreachable!() };
203        Ok(memory.clone())
204    }
205}
206
207impl FsNodeOps for File {
208    fs_node_impl_not_dir!();
209
210    fn create_file_ops(
211        &self,
212        _node: &FsNode,
213        _current_task: &CurrentTask,
214        _flags: OpenFlags,
215    ) -> Result<Box<dyn FileOps>, Errno> {
216        let memory = self.inner.lock().get_memory()?;
217        let size = usize::try_from(memory.get_content_size()).unwrap();
218        Ok(Box::new(MemoryFile { memory, size }))
219    }
220
221    fn fetch_and_refresh_info<'a>(
222        &self,
223        _node: &FsNode,
224        _current_task: &CurrentTask,
225        info: &'a DynamicLockDepRwLock<FsNodeInfo>,
226        flags: StatxFlags,
227    ) -> Result<LockDepReadGuard<'a, FsNodeInfo>, Errno> {
228        if flags.contains(StatxFlags::AT_STATX_DONT_SYNC) {
229            return Ok(info.read());
230        }
231        let memory = self.inner.lock().get_memory()?;
232        let content_size = memory.get_content_size();
233        let attrs = zxio_node_attributes_t {
234            content_size: content_size,
235            // TODO(https://fxbug.dev/293607051): Plumb through storage size from underlying connection.
236            storage_size: content_size,
237            link_count: 1,
238            has: zxio_node_attr_has_t {
239                content_size: true,
240                storage_size: true,
241                link_count: true,
242                ..Default::default()
243            },
244            ..Default::default()
245        };
246        let mut info = info.write();
247        update_info_from_attrs(&mut info, &attrs);
248        Ok(LockDepWriteGuard::downgrade(info))
249    }
250
251    fn get_xattr(
252        &self,
253        node: &FsNode,
254        _current_task: &CurrentTask,
255        name: &FsStr,
256        _size: usize,
257    ) -> Result<ValueOrSize<FsString>, Errno> {
258        let fs = node.fs();
259        let bundle = RemoteBundle::from_fs(&fs);
260        bundle.get_xattr(node, name)
261    }
262
263    fn list_xattrs(
264        &self,
265        node: &FsNode,
266        _current_task: &CurrentTask,
267        _size: usize,
268    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
269        let fs = node.fs();
270        let bundle = RemoteBundle::from_fs(&fs);
271        bundle.list_xattrs(node)
272    }
273}
274
275// NB: This is different from MemoryRegularFile, which is designed to wrap a VMO that is owned and
276// managed by Starnix.  This struct is a wrapper around a pager-backed VMO received from the
277// filesystem backing the remote bundle.
278// MemoryRegularFile does its own content size management, which is (a) incompatible with the content
279// size management done for us by the remote filesystem, and (b) the content size is based on file
280// attributes in the case of MemoryRegularFile, which we've intentionally avoided querying here for
281// performance.  Specifically, MemoryFile is designed to be opened as fast as possible, and requiring
282// that we stat the file whilst opening it is counter to that goal.
283// Note that MemoryFile assumes that the underlying file is read-only and not resizable (which is the
284// case for remote bundles since they're stored as blobs).
285struct MemoryFile {
286    memory: Arc<MemoryObject>,
287    size: usize,
288}
289
290impl FileOps for MemoryFile {
291    fileops_impl_seekable!();
292    fileops_impl_noop_sync!();
293
294    fn read(
295        &self,
296        _file: &FileObject,
297        _current_task: &CurrentTask,
298        mut offset: usize,
299        data: &mut dyn OutputBuffer,
300    ) -> Result<usize, Errno> {
301        data.write_each(&mut |buf| {
302            let buflen = buf.len();
303            let buf = &mut buf[..std::cmp::min(self.size.saturating_sub(offset), buflen)];
304            if !buf.is_empty() {
305                self.memory
306                    .read_uninit(buf, offset as u64)
307                    .map_err(|status| from_status_like_fdio!(status))?;
308                offset += buf.len();
309            }
310            Ok(buf.len())
311        })
312    }
313
314    fn write(
315        &self,
316        _file: &FileObject,
317        _current_task: &CurrentTask,
318        _offset: usize,
319        _data: &mut dyn InputBuffer,
320    ) -> Result<usize, Errno> {
321        error!(EPERM)
322    }
323
324    fn get_memory(
325        &self,
326        _file: &FileObject,
327        _current_task: &CurrentTask,
328        _length: Option<usize>,
329        prot: ProtectionFlags,
330    ) -> Result<Arc<MemoryObject>, Errno> {
331        Ok(if prot.contains(ProtectionFlags::EXEC) {
332            Arc::new(
333                self.memory
334                    .duplicate_handle(zx::Rights::SAME_RIGHTS)
335                    .map_err(impossible_error)?
336                    .replace_as_executable(&VMEX_RESOURCE)
337                    .map_err(impossible_error)?,
338            )
339        } else {
340            self.memory.clone()
341        })
342    }
343
344    fn wait_async(
345        &self,
346        _file: &FileObject,
347        _current_task: &CurrentTask,
348        _waiter: &Waiter,
349        _events: FdEvents,
350        _handler: EventHandler,
351    ) -> Option<WaitCanceler> {
352        None
353    }
354
355    fn query_events(
356        &self,
357        _file: &FileObject,
358        _current_task: &CurrentTask,
359    ) -> Result<FdEvents, Errno> {
360        Ok(FdEvents::POLLIN)
361    }
362}
363
364struct DirectoryObject;
365
366impl FileOps for DirectoryObject {
367    fileops_impl_directory!();
368    fileops_impl_noop_sync!();
369
370    fn seek(
371        &self,
372        _file: &FileObject,
373        _current_task: &CurrentTask,
374        current_offset: off_t,
375        target: SeekTarget,
376    ) -> Result<off_t, Errno> {
377        default_seek(current_offset, target, || error!(EINVAL))
378    }
379
380    fn readdir(
381        &self,
382        file: &FileObject,
383        _current_task: &CurrentTask,
384        sink: &mut dyn DirentSink,
385    ) -> Result<(), Errno> {
386        emit_dotdot(file, sink)?;
387
388        let bundle = RemoteBundle::from_fs(&file.fs);
389        let child_iter = bundle
390            .get_node(file.node().ino)
391            .directory()
392            .ok_or_else(|| errno!(EIO))?
393            .children
394            .iter();
395
396        for (name, inode_num) in child_iter.skip(sink.offset() as usize - 2) {
397            let node = bundle.metadata.get(*inode_num).ok_or_else(|| errno!(EIO))?;
398            sink.add(
399                *inode_num,
400                sink.offset() + 1,
401                DirectoryEntryType::from_mode(FileMode::from_bits(node.mode.into())),
402                name.as_str().into(),
403            )?;
404        }
405
406        Ok(())
407    }
408}
409
410impl FsNodeOps for DirectoryObject {
411    fs_node_impl_dir_readonly!();
412
413    fn create_file_ops(
414        &self,
415        _node: &FsNode,
416        _current_task: &CurrentTask,
417        _flags: OpenFlags,
418    ) -> Result<Box<dyn FileOps>, Errno> {
419        Ok(Box::new(DirectoryObject))
420    }
421
422    fn lookup(
423        &self,
424        entry: &DirEntry,
425        _current_task: &CurrentTask,
426        name: &FsStr,
427    ) -> Result<FsNodeHandle, Errno> {
428        let name = std::str::from_utf8(name).map_err(|_| {
429            log_warn!("bad utf8 in pathname! remote filesystems can't handle this");
430            errno!(EINVAL)
431        })?;
432
433        let fs = entry.node.fs();
434        let bundle = RemoteBundle::from_fs(&fs);
435        let metadata = &bundle.metadata;
436        let ino = metadata
437            .lookup(entry.node.ino, name)
438            .map_err(|e| errno!(ENOENT, format!("Error: {e:?} opening {name}")))?;
439        let metadata_node = metadata.get(ino).ok_or_else(|| errno!(EIO))?;
440        let info = to_fs_node_info(metadata_node);
441
442        match metadata_node.info() {
443            NodeInfo::Symlink(_) => Ok(fs.create_node(ino, SymlinkObject, info)),
444            NodeInfo::Directory(_) => Ok(fs.create_node(ino, DirectoryObject, info)),
445            NodeInfo::File(_) => {
446                let (file, server_end) = fidl::endpoints::create_sync_proxy::<fio::FileMarker>();
447                bundle
448                    .root
449                    .open(
450                        &format!("{ino}"),
451                        bundle.rights,
452                        &Default::default(),
453                        server_end.into_channel(),
454                    )
455                    .map_err(|_| errno!(EIO))?;
456                Ok(fs.create_node(ino, File { inner: Inner::NeedsVmo(file).into() }, info))
457            }
458        }
459    }
460
461    fn get_xattr(
462        &self,
463        node: &FsNode,
464        _current_task: &CurrentTask,
465        name: &FsStr,
466        _size: usize,
467    ) -> Result<ValueOrSize<FsString>, Errno> {
468        let fs = node.fs();
469        let bundle = RemoteBundle::from_fs(&fs);
470        bundle.get_xattr(node, name)
471    }
472
473    fn list_xattrs(
474        &self,
475        node: &FsNode,
476        _current_task: &CurrentTask,
477        _size: usize,
478    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
479        let fs = node.fs();
480        let bundle = RemoteBundle::from_fs(&fs);
481        bundle.list_xattrs(node)
482    }
483}
484
485struct SymlinkObject;
486
487impl FsNodeOps for SymlinkObject {
488    fs_node_impl_symlink!();
489
490    fn readlink(&self, node: &FsNode, _current_task: &CurrentTask) -> Result<SymlinkTarget, Errno> {
491        let fs = node.fs();
492        let bundle = RemoteBundle::from_fs(&fs);
493        let target = bundle.get_node(node.ino).symlink().ok_or_else(|| errno!(EIO))?.target.clone();
494        Ok(SymlinkTarget::Path(target.as_str().into()))
495    }
496
497    fn get_xattr(
498        &self,
499        node: &FsNode,
500        _current_task: &CurrentTask,
501        name: &FsStr,
502        _size: usize,
503    ) -> Result<ValueOrSize<FsString>, Errno> {
504        let fs = node.fs();
505        let bundle = RemoteBundle::from_fs(&fs);
506        bundle.get_xattr(node, name)
507    }
508
509    fn list_xattrs(
510        &self,
511        node: &FsNode,
512        _current_task: &CurrentTask,
513        _size: usize,
514    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
515        let fs = node.fs();
516        let bundle = RemoteBundle::from_fs(&fs);
517        bundle.list_xattrs(node)
518    }
519}
520
521fn to_fs_node_info(metadata_node: &ext4_metadata::Node) -> FsNodeInfo {
522    let mode = FileMode::from_bits(metadata_node.mode.into());
523    let owner = FsCred { uid: metadata_node.uid.into(), gid: metadata_node.gid.into() };
524    let mut info = FsNodeInfo::new(mode, owner);
525    // Set the information for directory and links. For file, they will be overwritten
526    // by the FsNodeOps on first access.
527    // For now, we just use some made up values. We might need to revisit this.
528    info.size = 1;
529    info.blocks = 1;
530    info.blksize = DEFAULT_BYTES_PER_BLOCK;
531    info.link_count = 1;
532    info
533}
534
535#[cfg(test)]
536mod test {
537    use crate::fs::fuchsia::RemoteBundle;
538    use crate::testing::spawn_kernel_and_run_with_pkgfs;
539    use crate::vfs::buffers::VecOutputBuffer;
540    use crate::vfs::{
541        DirectoryEntryType, DirentSink, FileSystemOptions, FsStr, LookupContext, Namespace,
542        SymlinkMode, SymlinkTarget,
543    };
544    use fidl_fuchsia_io as fio;
545    use starnix_uapi::errors::Errno;
546    use starnix_uapi::file_mode::FileMode;
547    use starnix_uapi::open_flags::OpenFlags;
548    use starnix_uapi::{ino_t, off_t};
549    use std::collections::{HashMap, HashSet};
550    use zx;
551
552    #[::fuchsia::test]
553    async fn test_read_image() {
554        spawn_kernel_and_run_with_pkgfs(async |current_task| {
555            let kernel = current_task.kernel();
556            let rights = fio::PERM_READABLE | fio::PERM_EXECUTABLE;
557            let (server, client) = zx::Channel::create();
558            fdio::open("/pkg", rights, server).expect("failed to open /pkg");
559            let fs = RemoteBundle::new_fs_in_base(
560                &kernel,
561                &fio::DirectorySynchronousProxy::new(client),
562                FileSystemOptions { source: "data/test-image".into(), ..Default::default() },
563                rights,
564            )
565            .expect("new_fs failed");
566            let ns = Namespace::new(fs);
567            let root = ns.root();
568            let mut context = LookupContext::new(SymlinkMode::NoFollow);
569
570            let test_dir = root
571                .lookup_child(&current_task, &mut context, "foo".into())
572                .expect("lookup failed");
573
574            let test_file = test_dir
575                .lookup_child(&current_task, &mut context, "file".into())
576                .expect("lookup failed")
577                .open(&current_task, OpenFlags::RDONLY)
578                .expect("open failed");
579
580            let mut buffer = VecOutputBuffer::new(64);
581            assert_eq!(test_file.read(&current_task, &mut buffer).expect("read failed"), 6);
582            let buffer: Vec<u8> = buffer.into();
583            assert_eq!(&buffer[..6], b"hello\n");
584
585            assert_eq!(
586                &test_file
587                    .node()
588                    .get_xattr(&current_task, &test_dir.mount, "user.a".into(), usize::MAX)
589                    .expect("get_xattr failed")
590                    .unwrap(),
591                "apple"
592            );
593            assert_eq!(
594                &test_file
595                    .node()
596                    .get_xattr(&current_task, &test_dir.mount, "user.b".into(), usize::MAX)
597                    .expect("get_xattr failed")
598                    .unwrap(),
599                "ball"
600            );
601            assert_eq!(
602                test_file
603                    .node()
604                    .list_xattrs(&current_task, usize::MAX)
605                    .expect("list_xattr failed")
606                    .unwrap()
607                    .into_iter()
608                    .collect::<HashSet<_>>(),
609                ["user.a".into(), "user.b".into()].into_iter().collect::<HashSet<_>>(),
610            );
611
612            {
613                let info = test_file.node().info();
614                assert_eq!(info.mode, FileMode::from_bits(0o100640));
615                assert_eq!(info.uid, 49152); // These values come from the test image generated in
616                assert_eq!(info.gid, 24403); // ext4_to_pkg.
617            }
618
619            let test_symlink = test_dir
620                .lookup_child(&current_task, &mut context, "symlink".into())
621                .expect("lookup failed");
622
623            if let SymlinkTarget::Path(target) =
624                test_symlink.readlink(&current_task).expect("readlink failed")
625            {
626                assert_eq!(&target, "file");
627            } else {
628                panic!("unexpected symlink type");
629            }
630
631            let opened_dir = test_dir.open(&current_task, OpenFlags::RDONLY).expect("open failed");
632
633            struct Sink {
634                offset: off_t,
635                entries: HashMap<Vec<u8>, (ino_t, DirectoryEntryType)>,
636            }
637
638            impl DirentSink for Sink {
639                fn add(
640                    &mut self,
641                    inode_num: ino_t,
642                    offset: off_t,
643                    entry_type: DirectoryEntryType,
644                    name: &FsStr,
645                ) -> Result<(), Errno> {
646                    assert_eq!(offset, self.offset + 1);
647                    self.entries.insert(name.to_vec(), (inode_num, entry_type));
648                    self.offset = offset;
649                    Ok(())
650                }
651
652                fn offset(&self) -> off_t {
653                    self.offset
654                }
655            }
656
657            let mut sink = Sink { offset: 0, entries: HashMap::new() };
658            opened_dir.readdir(&current_task, &mut sink).expect("readdir failed");
659
660            assert_eq!(
661                sink.entries,
662                [
663                    (b".".into(), (test_dir.entry.node.ino, DirectoryEntryType::DIR)),
664                    (b"..".into(), (root.entry.node.ino, DirectoryEntryType::DIR)),
665                    (b"file".into(), (test_file.node().ino, DirectoryEntryType::REG)),
666                    (b"symlink".into(), (test_symlink.entry.node.ino, DirectoryEntryType::LNK))
667                ]
668                .into()
669            );
670        })
671        .await;
672    }
673}