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