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::FileSystemFlags;
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 |= FileSystemFlags::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    fn is_readonly(&self) -> bool {
177        true
178    }
179}
180
181struct File {
182    inner: Mutex<Inner>,
183}
184
185enum Inner {
186    NeedsVmo(fio::FileSynchronousProxy),
187    Memory(Arc<MemoryObject>),
188}
189
190impl Inner {
191    fn get_memory(&mut self) -> Result<Arc<MemoryObject>, Errno> {
192        if let Inner::NeedsVmo(file) = &*self {
193            let memory = Arc::new(MemoryObject::from(
194                file.get_backing_memory(fio::VmoFlags::READ, zx::MonotonicInstant::INFINITE)
195                    .map_err(|err| errno!(EIO, format!("Error {err} on GetBackingMemory")))?
196                    .map_err(|s| from_status_like_fdio!(zx::Status::from_raw(s)))?,
197            ));
198            *self = Inner::Memory(memory);
199        }
200        let Inner::Memory(memory) = &*self else { unreachable!() };
201        Ok(memory.clone())
202    }
203}
204
205impl FsNodeOps for File {
206    fs_node_impl_not_dir!();
207
208    fn create_file_ops(
209        &self,
210        _locked: &mut Locked<FileOpsCore>,
211        _node: &FsNode,
212        _current_task: &CurrentTask,
213        _flags: OpenFlags,
214    ) -> Result<Box<dyn FileOps>, Errno> {
215        let memory = self.inner.lock().get_memory()?;
216        let size = usize::try_from(memory.get_content_size()).unwrap();
217        Ok(Box::new(MemoryFile { memory, size }))
218    }
219
220    fn fetch_and_refresh_info<'a>(
221        &self,
222        _locked: &mut Locked<FileOpsCore>,
223        _node: &FsNode,
224        _current_task: &CurrentTask,
225        info: &'a RwLock<FsNodeInfo>,
226    ) -> Result<RwLockReadGuard<'a, FsNodeInfo>, Errno> {
227        let memory = self.inner.lock().get_memory()?;
228        let content_size = memory.get_content_size();
229        let attrs = zxio_node_attributes_t {
230            content_size: content_size,
231            // TODO(https://fxbug.dev/293607051): Plumb through storage size from underlying connection.
232            storage_size: content_size,
233            link_count: 1,
234            has: zxio_node_attr_has_t {
235                content_size: true,
236                storage_size: true,
237                link_count: true,
238                ..Default::default()
239            },
240            ..Default::default()
241        };
242        let mut info = info.write();
243        update_info_from_attrs(&mut info, &attrs);
244        Ok(RwLockWriteGuard::downgrade(info))
245    }
246
247    fn get_xattr(
248        &self,
249        _locked: &mut Locked<FileOpsCore>,
250        node: &FsNode,
251        _current_task: &CurrentTask,
252        name: &FsStr,
253        _size: usize,
254    ) -> Result<ValueOrSize<FsString>, Errno> {
255        let fs = node.fs();
256        let bundle = RemoteBundle::from_fs(&fs);
257        bundle.get_xattr(node, name)
258    }
259
260    fn list_xattrs(
261        &self,
262        _locked: &mut Locked<FileOpsCore>,
263        node: &FsNode,
264        _current_task: &CurrentTask,
265        _size: usize,
266    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
267        let fs = node.fs();
268        let bundle = RemoteBundle::from_fs(&fs);
269        bundle.list_xattrs(node)
270    }
271}
272
273// NB: This is different from MemoryRegularFile, which is designed to wrap a VMO that is owned and
274// managed by Starnix.  This struct is a wrapper around a pager-backed VMO received from the
275// filesystem backing the remote bundle.
276// MemoryRegularFile does its own content size management, which is (a) incompatible with the content
277// size management done for us by the remote filesystem, and (b) the content size is based on file
278// attributes in the case of MemoryRegularFile, which we've intentionally avoided querying here for
279// performance.  Specifically, MemoryFile is designed to be opened as fast as possible, and requiring
280// that we stat the file whilst opening it is counter to that goal.
281// Note that MemoryFile assumes that the underlying file is read-only and not resizable (which is the
282// case for remote bundles since they're stored as blobs).
283struct MemoryFile {
284    memory: Arc<MemoryObject>,
285    size: usize,
286}
287
288impl FileOps for MemoryFile {
289    fileops_impl_seekable!();
290    fileops_impl_noop_sync!();
291
292    fn read(
293        &self,
294        _locked: &mut Locked<FileOpsCore>,
295        _file: &FileObject,
296        _current_task: &CurrentTask,
297        mut offset: usize,
298        data: &mut dyn OutputBuffer,
299    ) -> Result<usize, Errno> {
300        data.write_each(&mut |buf| {
301            let buflen = buf.len();
302            let buf = &mut buf[..std::cmp::min(self.size.saturating_sub(offset), buflen)];
303            if !buf.is_empty() {
304                self.memory
305                    .read_uninit(buf, offset as u64)
306                    .map_err(|status| from_status_like_fdio!(status))?;
307                offset += buf.len();
308            }
309            Ok(buf.len())
310        })
311    }
312
313    fn write(
314        &self,
315        _locked: &mut Locked<FileOpsCore>,
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        _locked: &mut Locked<FileOpsCore>,
327        _file: &FileObject,
328        _current_task: &CurrentTask,
329        _length: Option<usize>,
330        prot: ProtectionFlags,
331    ) -> Result<Arc<MemoryObject>, Errno> {
332        Ok(if prot.contains(ProtectionFlags::EXEC) {
333            Arc::new(
334                self.memory
335                    .duplicate_handle(zx::Rights::SAME_RIGHTS)
336                    .map_err(impossible_error)?
337                    .replace_as_executable(&VMEX_RESOURCE)
338                    .map_err(impossible_error)?,
339            )
340        } else {
341            self.memory.clone()
342        })
343    }
344
345    fn wait_async(
346        &self,
347        _locked: &mut Locked<FileOpsCore>,
348        _file: &FileObject,
349        _current_task: &CurrentTask,
350        _waiter: &Waiter,
351        _events: FdEvents,
352        _handler: EventHandler,
353    ) -> Option<WaitCanceler> {
354        None
355    }
356
357    fn query_events(
358        &self,
359        _locked: &mut Locked<FileOpsCore>,
360        _file: &FileObject,
361        _current_task: &CurrentTask,
362    ) -> Result<FdEvents, Errno> {
363        Ok(FdEvents::POLLIN)
364    }
365}
366
367struct DirectoryObject;
368
369impl FileOps for DirectoryObject {
370    fileops_impl_directory!();
371    fileops_impl_noop_sync!();
372
373    fn seek(
374        &self,
375        _locked: &mut Locked<FileOpsCore>,
376        _file: &FileObject,
377        _current_task: &CurrentTask,
378        current_offset: off_t,
379        target: SeekTarget,
380    ) -> Result<off_t, Errno> {
381        default_seek(current_offset, target, || error!(EINVAL))
382    }
383
384    fn readdir(
385        &self,
386        _locked: &mut Locked<FileOpsCore>,
387        file: &FileObject,
388        _current_task: &CurrentTask,
389        sink: &mut dyn DirentSink,
390    ) -> Result<(), Errno> {
391        emit_dotdot(file, sink)?;
392
393        let bundle = RemoteBundle::from_fs(&file.fs);
394        let child_iter = bundle
395            .get_node(file.node().ino)
396            .directory()
397            .ok_or_else(|| errno!(EIO))?
398            .children
399            .iter();
400
401        for (name, inode_num) in child_iter.skip(sink.offset() as usize - 2) {
402            let node = bundle.metadata.get(*inode_num).ok_or_else(|| errno!(EIO))?;
403            sink.add(
404                *inode_num,
405                sink.offset() + 1,
406                DirectoryEntryType::from_mode(FileMode::from_bits(node.mode.into())),
407                name.as_str().into(),
408            )?;
409        }
410
411        Ok(())
412    }
413}
414
415impl FsNodeOps for DirectoryObject {
416    fs_node_impl_dir_readonly!();
417
418    fn create_file_ops(
419        &self,
420        _locked: &mut Locked<FileOpsCore>,
421        _node: &FsNode,
422        _current_task: &CurrentTask,
423        _flags: OpenFlags,
424    ) -> Result<Box<dyn FileOps>, Errno> {
425        Ok(Box::new(DirectoryObject))
426    }
427
428    fn lookup(
429        &self,
430        _locked: &mut Locked<FileOpsCore>,
431        node: &FsNode,
432        _current_task: &CurrentTask,
433        name: &FsStr,
434    ) -> Result<FsNodeHandle, Errno> {
435        let name = std::str::from_utf8(name).map_err(|_| {
436            log_warn!("bad utf8 in pathname! remote filesystems can't handle this");
437            errno!(EINVAL)
438        })?;
439
440        let fs = node.fs();
441        let bundle = RemoteBundle::from_fs(&fs);
442        let metadata = &bundle.metadata;
443        let ino = metadata
444            .lookup(node.ino, name)
445            .map_err(|e| errno!(ENOENT, format!("Error: {e:?} opening {name}")))?;
446        let metadata_node = metadata.get(ino).ok_or_else(|| errno!(EIO))?;
447        let info = to_fs_node_info(metadata_node);
448
449        match metadata_node.info() {
450            NodeInfo::Symlink(_) => Ok(fs.create_node(ino, SymlinkObject, info)),
451            NodeInfo::Directory(_) => Ok(fs.create_node(ino, DirectoryObject, info)),
452            NodeInfo::File(_) => {
453                let (file, server_end) = fidl::endpoints::create_sync_proxy::<fio::FileMarker>();
454                bundle
455                    .root
456                    .open(
457                        &format!("{ino}"),
458                        bundle.rights,
459                        &Default::default(),
460                        server_end.into_channel(),
461                    )
462                    .map_err(|_| errno!(EIO))?;
463                Ok(fs.create_node(ino, File { inner: Mutex::new(Inner::NeedsVmo(file)) }, info))
464            }
465        }
466    }
467
468    fn get_xattr(
469        &self,
470        _locked: &mut Locked<FileOpsCore>,
471        node: &FsNode,
472        _current_task: &CurrentTask,
473        name: &FsStr,
474        _size: usize,
475    ) -> Result<ValueOrSize<FsString>, Errno> {
476        let fs = node.fs();
477        let bundle = RemoteBundle::from_fs(&fs);
478        bundle.get_xattr(node, name)
479    }
480
481    fn list_xattrs(
482        &self,
483        _locked: &mut Locked<FileOpsCore>,
484        node: &FsNode,
485        _current_task: &CurrentTask,
486        _size: usize,
487    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
488        let fs = node.fs();
489        let bundle = RemoteBundle::from_fs(&fs);
490        bundle.list_xattrs(node)
491    }
492}
493
494struct SymlinkObject;
495
496impl FsNodeOps for SymlinkObject {
497    fs_node_impl_symlink!();
498
499    fn readlink(
500        &self,
501        _locked: &mut Locked<FileOpsCore>,
502        node: &FsNode,
503        _current_task: &CurrentTask,
504    ) -> Result<SymlinkTarget, Errno> {
505        let fs = node.fs();
506        let bundle = RemoteBundle::from_fs(&fs);
507        let target = bundle.get_node(node.ino).symlink().ok_or_else(|| errno!(EIO))?.target.clone();
508        Ok(SymlinkTarget::Path(target.as_str().into()))
509    }
510
511    fn get_xattr(
512        &self,
513        _locked: &mut Locked<FileOpsCore>,
514        node: &FsNode,
515        _current_task: &CurrentTask,
516        name: &FsStr,
517        _size: usize,
518    ) -> Result<ValueOrSize<FsString>, Errno> {
519        let fs = node.fs();
520        let bundle = RemoteBundle::from_fs(&fs);
521        bundle.get_xattr(node, name)
522    }
523
524    fn list_xattrs(
525        &self,
526        _locked: &mut Locked<FileOpsCore>,
527        node: &FsNode,
528        _current_task: &CurrentTask,
529        _size: usize,
530    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
531        let fs = node.fs();
532        let bundle = RemoteBundle::from_fs(&fs);
533        bundle.list_xattrs(node)
534    }
535}
536
537fn to_fs_node_info(metadata_node: &ext4_metadata::Node) -> FsNodeInfo {
538    let mode = FileMode::from_bits(metadata_node.mode.into());
539    let owner = FsCred { uid: metadata_node.uid.into(), gid: metadata_node.gid.into() };
540    let mut info = FsNodeInfo::new(mode, owner);
541    // Set the information for directory and links. For file, they will be overwritten
542    // by the FsNodeOps on first access.
543    // For now, we just use some made up values. We might need to revisit this.
544    info.size = 1;
545    info.blocks = 1;
546    info.blksize = DEFAULT_BYTES_PER_BLOCK;
547    info.link_count = 1;
548    info
549}
550
551#[cfg(test)]
552mod test {
553    use crate::fs::fuchsia::RemoteBundle;
554    use crate::testing::spawn_kernel_and_run_with_pkgfs;
555    use crate::vfs::buffers::VecOutputBuffer;
556    use crate::vfs::{
557        DirectoryEntryType, DirentSink, FileSystemOptions, FsStr, LookupContext, Namespace,
558        SymlinkMode, SymlinkTarget,
559    };
560    use fidl_fuchsia_io as fio;
561    use starnix_uapi::errors::Errno;
562    use starnix_uapi::file_mode::{AccessCheck, FileMode};
563    use starnix_uapi::open_flags::OpenFlags;
564    use starnix_uapi::{ino_t, off_t};
565    use std::collections::{HashMap, HashSet};
566    use zx;
567
568    #[::fuchsia::test]
569    async fn test_read_image() {
570        spawn_kernel_and_run_with_pkgfs(async |locked, current_task| {
571            let kernel = current_task.kernel();
572            let rights = fio::PERM_READABLE | fio::PERM_EXECUTABLE;
573            let (server, client) = zx::Channel::create();
574            fdio::open("/pkg", rights, server).expect("failed to open /pkg");
575            let fs = RemoteBundle::new_fs_in_base(
576                locked,
577                &kernel,
578                &fio::DirectorySynchronousProxy::new(client),
579                FileSystemOptions { source: "data/test-image".into(), ..Default::default() },
580                rights,
581            )
582            .expect("new_fs failed");
583            let ns = Namespace::new(fs);
584            let root = ns.root();
585            let mut context = LookupContext::default().with(SymlinkMode::NoFollow);
586
587            let test_dir = root
588                .lookup_child(locked, &current_task, &mut context, "foo".into())
589                .expect("lookup failed");
590
591            let test_file = test_dir
592                .lookup_child(locked, &current_task, &mut context, "file".into())
593                .expect("lookup failed")
594                .open(locked, &current_task, OpenFlags::RDONLY, AccessCheck::default())
595                .expect("open failed");
596
597            let mut buffer = VecOutputBuffer::new(64);
598            assert_eq!(test_file.read(locked, &current_task, &mut buffer).expect("read failed"), 6);
599            let buffer: Vec<u8> = buffer.into();
600            assert_eq!(&buffer[..6], b"hello\n");
601
602            assert_eq!(
603                &test_file
604                    .node()
605                    .get_xattr(locked, &current_task, &test_dir.mount, "user.a".into(), usize::MAX)
606                    .expect("get_xattr failed")
607                    .unwrap(),
608                "apple"
609            );
610            assert_eq!(
611                &test_file
612                    .node()
613                    .get_xattr(locked, &current_task, &test_dir.mount, "user.b".into(), usize::MAX)
614                    .expect("get_xattr failed")
615                    .unwrap(),
616                "ball"
617            );
618            assert_eq!(
619                test_file
620                    .node()
621                    .list_xattrs(locked, &current_task, usize::MAX)
622                    .expect("list_xattr failed")
623                    .unwrap()
624                    .into_iter()
625                    .collect::<HashSet<_>>(),
626                ["user.a".into(), "user.b".into()].into_iter().collect::<HashSet<_>>(),
627            );
628
629            {
630                let info = test_file.node().info();
631                assert_eq!(info.mode, FileMode::from_bits(0o100640));
632                assert_eq!(info.uid, 49152); // These values come from the test image generated in
633                assert_eq!(info.gid, 24403); // ext4_to_pkg.
634            }
635
636            let test_symlink = test_dir
637                .lookup_child(locked, &current_task, &mut context, "symlink".into())
638                .expect("lookup failed");
639
640            if let SymlinkTarget::Path(target) =
641                test_symlink.readlink(locked, &current_task).expect("readlink failed")
642            {
643                assert_eq!(&target, "file");
644            } else {
645                panic!("unexpected symlink type");
646            }
647
648            let opened_dir = test_dir
649                .open(locked, &current_task, OpenFlags::RDONLY, AccessCheck::default())
650                .expect("open failed");
651
652            struct Sink {
653                offset: off_t,
654                entries: HashMap<Vec<u8>, (ino_t, DirectoryEntryType)>,
655            }
656
657            impl DirentSink for Sink {
658                fn add(
659                    &mut self,
660                    inode_num: ino_t,
661                    offset: off_t,
662                    entry_type: DirectoryEntryType,
663                    name: &FsStr,
664                ) -> Result<(), Errno> {
665                    assert_eq!(offset, self.offset + 1);
666                    self.entries.insert(name.to_vec(), (inode_num, entry_type));
667                    self.offset = offset;
668                    Ok(())
669                }
670
671                fn offset(&self) -> off_t {
672                    self.offset
673                }
674            }
675
676            let mut sink = Sink { offset: 0, entries: HashMap::new() };
677            opened_dir.readdir(locked, &current_task, &mut sink).expect("readdir failed");
678
679            assert_eq!(
680                sink.entries,
681                [
682                    (b".".into(), (test_dir.entry.node.ino, DirectoryEntryType::DIR)),
683                    (b"..".into(), (root.entry.node.ino, DirectoryEntryType::DIR)),
684                    (b"file".into(), (test_file.node().ino, DirectoryEntryType::REG)),
685                    (b"symlink".into(), (test_symlink.entry.node.ino, DirectoryEntryType::LNK))
686                ]
687                .into()
688            );
689        })
690        .await;
691    }
692}