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