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