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