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,
23};
24use starnix_types::vfs::default_statfs;
25use starnix_uapi::auth::FsCred;
26use starnix_uapi::errors::{Errno, SourceContext};
27use starnix_uapi::file_mode::FileMode;
28use starnix_uapi::mount_flags::MountFlags;
29use starnix_uapi::open_flags::OpenFlags;
30use starnix_uapi::vfs::FdEvents;
31use starnix_uapi::{errno, error, from_status_like_fdio, off_t, statfs};
32use std::io::Read;
33use std::sync::Arc;
34use syncio::{zxio_node_attr_has_t, zxio_node_attributes_t};
35use zx::{
36    HandleBased, {self as zx},
37};
38
39const REMOTE_BUNDLE_NODE_LRU_CAPACITY: usize = 1024;
40
41/// RemoteBundle is a remote, immutable filesystem that stores additional metadata that would
42/// otherwise not be available.  The metadata exists in the "metadata.v1" file, which contains
43/// directory, symbolic link and extended attribute information.  Only the content for files are
44/// accessed remotely as normal.
45pub struct RemoteBundle {
46    metadata: Metadata,
47    root: fio::DirectorySynchronousProxy,
48    rights: fio::Flags,
49}
50
51impl RemoteBundle {
52    /// Returns a new RemoteBundle filesystem that can be found at `path` relative to `base`.
53    pub fn new_fs<L>(
54        locked: &mut Locked<L>,
55        kernel: &Kernel,
56        base: &fio::DirectorySynchronousProxy,
57        mut options: FileSystemOptions,
58        rights: fio::Flags,
59    ) -> Result<FileSystemHandle, Error>
60    where
61        L: LockEqualOrBefore<FileOpsCore>,
62    {
63        let (root, server_end) = fidl::endpoints::create_sync_proxy::<fio::DirectoryMarker>();
64        let path =
65            std::str::from_utf8(&options.source).map_err(|_| anyhow!("Source path is not utf8"))?;
66        base.open(path, rights, &Default::default(), server_end.into_channel())
67            .map_err(|e| anyhow!("Failed to open root: {}", e))?;
68
69        let metadata = {
70            let (file, server_end) = fidl::endpoints::create_endpoints::<fio::FileMarker>();
71            root.open(
72                "metadata.v1",
73                fio::PERM_READABLE,
74                &Default::default(),
75                server_end.into_channel(),
76            )
77            .source_context("open metadata file")?;
78            let mut file: std::fs::File = fdio::create_fd(file.into_channel().into_handle())
79                .source_context("create fd from metadata file (wrong mount path?)")?
80                .into();
81            let mut buf = Vec::new();
82            file.read_to_end(&mut buf).source_context("read metadata file")?;
83            Metadata::deserialize(&buf).source_context("deserialize metadata file")?
84        };
85
86        // Make sure the root node exists.
87        ensure!(
88            metadata.get(ext4_metadata::ROOT_INODE_NUM).is_some(),
89            "Root node does not exist in remote bundle"
90        );
91
92        if !rights.contains(fio::PERM_WRITABLE) {
93            options.flags |= MountFlags::RDONLY;
94        }
95
96        let fs = FileSystem::new(
97            locked,
98            kernel,
99            CacheMode::Cached(CacheConfig { capacity: REMOTE_BUNDLE_NODE_LRU_CAPACITY }),
100            RemoteBundle { metadata, root, rights },
101            options,
102        )?;
103        fs.create_root(ext4_metadata::ROOT_INODE_NUM, DirectoryObject);
104        Ok(fs)
105    }
106
107    // Returns the bundle from the filesystem.  Panics if the filesystem isn't associated with a
108    // RemoteBundle.
109    fn from_fs(fs: &FileSystem) -> &RemoteBundle {
110        fs.downcast_ops::<RemoteBundle>().unwrap()
111    }
112
113    // Returns a reference to the node identified by `inode_num`.  Panics if the node is not found
114    // so this should only be used if the node is known to exist (e.g. the node must exist after
115    // `lookup` has run for the relevant node).
116    fn get_node(&self, inode_num: u64) -> &Node {
117        self.metadata.get(inode_num).unwrap()
118    }
119
120    fn get_xattr(&self, node: &FsNode, name: &FsStr) -> Result<ValueOrSize<FsString>, Errno> {
121        let value = &self
122            .get_node(node.ino)
123            .extended_attributes
124            .get(&**name)
125            .ok_or_else(|| errno!(ENODATA))?[..];
126        Ok(FsString::from(value).into())
127    }
128
129    fn list_xattrs(&self, node: &FsNode) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
130        Ok(self
131            .get_node(node.ino)
132            .extended_attributes
133            .keys()
134            .map(|k| FsString::from(&k[..]))
135            .collect::<Vec<_>>()
136            .into())
137    }
138}
139
140impl FileSystemOps for RemoteBundle {
141    fn statfs(
142        &self,
143        _locked: &mut Locked<FileOpsCore>,
144        _fs: &FileSystem,
145        _current_task: &CurrentTask,
146    ) -> Result<statfs, Errno> {
147        const REMOTE_BUNDLE_FS_MAGIC: u32 = u32::from_be_bytes(*b"bndl");
148        Ok(default_statfs(REMOTE_BUNDLE_FS_MAGIC))
149    }
150    fn name(&self) -> &'static FsStr {
151        "remote_bundle".into()
152    }
153}
154
155struct File {
156    inner: Mutex<Inner>,
157}
158
159enum Inner {
160    NeedsVmo(fio::FileSynchronousProxy),
161    Memory(Arc<MemoryObject>),
162}
163
164impl Inner {
165    fn get_memory(&mut self) -> Result<Arc<MemoryObject>, Errno> {
166        if let Inner::NeedsVmo(file) = &*self {
167            let memory = Arc::new(MemoryObject::from(
168                file.get_backing_memory(fio::VmoFlags::READ, zx::MonotonicInstant::INFINITE)
169                    .map_err(|err| errno!(EIO, format!("Error {err} on GetBackingMemory")))?
170                    .map_err(|s| from_status_like_fdio!(zx::Status::from_raw(s)))?,
171            ));
172            *self = Inner::Memory(memory);
173        }
174        let Inner::Memory(memory) = &*self else { unreachable!() };
175        Ok(memory.clone())
176    }
177}
178
179impl FsNodeOps for File {
180    fs_node_impl_not_dir!();
181
182    fn create_file_ops(
183        &self,
184        _locked: &mut Locked<FileOpsCore>,
185        _node: &FsNode,
186        _current_task: &CurrentTask,
187        _flags: OpenFlags,
188    ) -> Result<Box<dyn FileOps>, Errno> {
189        let memory = self.inner.lock().get_memory()?;
190        let size = usize::try_from(memory.get_content_size()).unwrap();
191        Ok(Box::new(MemoryFile { memory, size }))
192    }
193
194    fn fetch_and_refresh_info<'a>(
195        &self,
196        _locked: &mut Locked<FileOpsCore>,
197        _node: &FsNode,
198        _current_task: &CurrentTask,
199        info: &'a RwLock<FsNodeInfo>,
200    ) -> Result<RwLockReadGuard<'a, FsNodeInfo>, Errno> {
201        let memory = self.inner.lock().get_memory()?;
202        let content_size = memory.get_content_size();
203        let attrs = zxio_node_attributes_t {
204            content_size: content_size,
205            // TODO(https://fxbug.dev/293607051): Plumb through storage size from underlying connection.
206            storage_size: content_size,
207            link_count: 1,
208            has: zxio_node_attr_has_t {
209                content_size: true,
210                storage_size: true,
211                link_count: true,
212                ..Default::default()
213            },
214            ..Default::default()
215        };
216        let mut info = info.write();
217        update_info_from_attrs(&mut info, &attrs);
218        Ok(RwLockWriteGuard::downgrade(info))
219    }
220
221    fn get_xattr(
222        &self,
223        _locked: &mut Locked<FileOpsCore>,
224        node: &FsNode,
225        _current_task: &CurrentTask,
226        name: &FsStr,
227        _size: usize,
228    ) -> Result<ValueOrSize<FsString>, Errno> {
229        let fs = node.fs();
230        let bundle = RemoteBundle::from_fs(&fs);
231        bundle.get_xattr(node, name)
232    }
233
234    fn list_xattrs(
235        &self,
236        _locked: &mut Locked<FileOpsCore>,
237        node: &FsNode,
238        _current_task: &CurrentTask,
239        _size: usize,
240    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
241        let fs = node.fs();
242        let bundle = RemoteBundle::from_fs(&fs);
243        bundle.list_xattrs(node)
244    }
245}
246
247// NB: This is different from MemoryRegularFile, which is designed to wrap a VMO that is owned and
248// managed by Starnix.  This struct is a wrapper around a pager-backed VMO received from the
249// filesystem backing the remote bundle.
250// MemoryRegularFile does its own content size management, which is (a) incompatible with the content
251// size management done for us by the remote filesystem, and (b) the content size is based on file
252// attributes in the case of MemoryRegularFile, which we've intentionally avoided querying here for
253// performance.  Specifically, MemoryFile is designed to be opened as fast as possible, and requiring
254// that we stat the file whilst opening it is counter to that goal.
255// Note that MemoryFile assumes that the underlying file is read-only and not resizable (which is the
256// case for remote bundles since they're stored as blobs).
257struct MemoryFile {
258    memory: Arc<MemoryObject>,
259    size: usize,
260}
261
262impl FileOps for MemoryFile {
263    fileops_impl_seekable!();
264    fileops_impl_noop_sync!();
265
266    fn read(
267        &self,
268        _locked: &mut Locked<FileOpsCore>,
269        _file: &FileObject,
270        _current_task: &CurrentTask,
271        mut offset: usize,
272        data: &mut dyn OutputBuffer,
273    ) -> Result<usize, Errno> {
274        data.write_each(&mut |buf| {
275            let buflen = buf.len();
276            let buf = &mut buf[..std::cmp::min(self.size.saturating_sub(offset), buflen)];
277            if !buf.is_empty() {
278                self.memory
279                    .read_uninit(buf, offset as u64)
280                    .map_err(|status| from_status_like_fdio!(status))?;
281                offset += buf.len();
282            }
283            Ok(buf.len())
284        })
285    }
286
287    fn write(
288        &self,
289        _locked: &mut Locked<FileOpsCore>,
290        _file: &FileObject,
291        _current_task: &CurrentTask,
292        _offset: usize,
293        _data: &mut dyn InputBuffer,
294    ) -> Result<usize, Errno> {
295        error!(EPERM)
296    }
297
298    fn get_memory(
299        &self,
300        _locked: &mut Locked<FileOpsCore>,
301        _file: &FileObject,
302        _current_task: &CurrentTask,
303        _length: Option<usize>,
304        prot: ProtectionFlags,
305    ) -> Result<Arc<MemoryObject>, Errno> {
306        Ok(if prot.contains(ProtectionFlags::EXEC) {
307            Arc::new(
308                self.memory
309                    .duplicate_handle(zx::Rights::SAME_RIGHTS)
310                    .map_err(impossible_error)?
311                    .replace_as_executable(&VMEX_RESOURCE)
312                    .map_err(impossible_error)?,
313            )
314        } else {
315            self.memory.clone()
316        })
317    }
318
319    fn wait_async(
320        &self,
321        _locked: &mut Locked<FileOpsCore>,
322        _file: &FileObject,
323        _current_task: &CurrentTask,
324        _waiter: &Waiter,
325        _events: FdEvents,
326        _handler: EventHandler,
327    ) -> Option<WaitCanceler> {
328        None
329    }
330
331    fn query_events(
332        &self,
333        _locked: &mut Locked<FileOpsCore>,
334        _file: &FileObject,
335        _current_task: &CurrentTask,
336    ) -> Result<FdEvents, Errno> {
337        Ok(FdEvents::POLLIN)
338    }
339}
340
341struct DirectoryObject;
342
343impl FileOps for DirectoryObject {
344    fileops_impl_directory!();
345    fileops_impl_noop_sync!();
346
347    fn seek(
348        &self,
349        _locked: &mut Locked<FileOpsCore>,
350        _file: &FileObject,
351        _current_task: &CurrentTask,
352        current_offset: off_t,
353        target: SeekTarget,
354    ) -> Result<off_t, Errno> {
355        default_seek(current_offset, target, || error!(EINVAL))
356    }
357
358    fn readdir(
359        &self,
360        _locked: &mut Locked<FileOpsCore>,
361        file: &FileObject,
362        _current_task: &CurrentTask,
363        sink: &mut dyn DirentSink,
364    ) -> Result<(), Errno> {
365        emit_dotdot(file, sink)?;
366
367        let bundle = RemoteBundle::from_fs(&file.fs);
368        let child_iter = bundle
369            .get_node(file.node().ino)
370            .directory()
371            .ok_or_else(|| errno!(EIO))?
372            .children
373            .iter();
374
375        for (name, inode_num) in child_iter.skip(sink.offset() as usize - 2) {
376            let node = bundle.metadata.get(*inode_num).ok_or_else(|| errno!(EIO))?;
377            sink.add(
378                *inode_num,
379                sink.offset() + 1,
380                DirectoryEntryType::from_mode(FileMode::from_bits(node.mode.into())),
381                name.as_str().into(),
382            )?;
383        }
384
385        Ok(())
386    }
387}
388
389impl FsNodeOps for DirectoryObject {
390    fs_node_impl_dir_readonly!();
391
392    fn create_file_ops(
393        &self,
394        _locked: &mut Locked<FileOpsCore>,
395        _node: &FsNode,
396        _current_task: &CurrentTask,
397        _flags: OpenFlags,
398    ) -> Result<Box<dyn FileOps>, Errno> {
399        Ok(Box::new(DirectoryObject))
400    }
401
402    fn lookup(
403        &self,
404        _locked: &mut Locked<FileOpsCore>,
405        node: &FsNode,
406        _current_task: &CurrentTask,
407        name: &FsStr,
408    ) -> Result<FsNodeHandle, Errno> {
409        let name = std::str::from_utf8(name).map_err(|_| {
410            log_warn!("bad utf8 in pathname! remote filesystems can't handle this");
411            errno!(EINVAL)
412        })?;
413
414        let fs = node.fs();
415        let bundle = RemoteBundle::from_fs(&fs);
416        let metadata = &bundle.metadata;
417        let ino = metadata
418            .lookup(node.ino, name)
419            .map_err(|e| errno!(ENOENT, format!("Error: {e:?} opening {name}")))?;
420        let metadata_node = metadata.get(ino).ok_or_else(|| errno!(EIO))?;
421        let info = to_fs_node_info(metadata_node);
422
423        match metadata_node.info() {
424            NodeInfo::Symlink(_) => Ok(fs.create_node(ino, SymlinkObject, info)),
425            NodeInfo::Directory(_) => Ok(fs.create_node(ino, DirectoryObject, info)),
426            NodeInfo::File(_) => {
427                let (file, server_end) = fidl::endpoints::create_sync_proxy::<fio::FileMarker>();
428                bundle
429                    .root
430                    .open(
431                        &format!("{ino}"),
432                        bundle.rights,
433                        &Default::default(),
434                        server_end.into_channel(),
435                    )
436                    .map_err(|_| errno!(EIO))?;
437                Ok(fs.create_node(ino, File { inner: Mutex::new(Inner::NeedsVmo(file)) }, info))
438            }
439        }
440    }
441
442    fn get_xattr(
443        &self,
444        _locked: &mut Locked<FileOpsCore>,
445        node: &FsNode,
446        _current_task: &CurrentTask,
447        name: &FsStr,
448        _size: usize,
449    ) -> Result<ValueOrSize<FsString>, Errno> {
450        let fs = node.fs();
451        let bundle = RemoteBundle::from_fs(&fs);
452        bundle.get_xattr(node, name)
453    }
454
455    fn list_xattrs(
456        &self,
457        _locked: &mut Locked<FileOpsCore>,
458        node: &FsNode,
459        _current_task: &CurrentTask,
460        _size: usize,
461    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
462        let fs = node.fs();
463        let bundle = RemoteBundle::from_fs(&fs);
464        bundle.list_xattrs(node)
465    }
466}
467
468struct SymlinkObject;
469
470impl FsNodeOps for SymlinkObject {
471    fs_node_impl_symlink!();
472
473    fn readlink(
474        &self,
475        _locked: &mut Locked<FileOpsCore>,
476        node: &FsNode,
477        _current_task: &CurrentTask,
478    ) -> Result<SymlinkTarget, Errno> {
479        let fs = node.fs();
480        let bundle = RemoteBundle::from_fs(&fs);
481        let target = bundle.get_node(node.ino).symlink().ok_or_else(|| errno!(EIO))?.target.clone();
482        Ok(SymlinkTarget::Path(target.as_str().into()))
483    }
484
485    fn get_xattr(
486        &self,
487        _locked: &mut Locked<FileOpsCore>,
488        node: &FsNode,
489        _current_task: &CurrentTask,
490        name: &FsStr,
491        _size: usize,
492    ) -> Result<ValueOrSize<FsString>, Errno> {
493        let fs = node.fs();
494        let bundle = RemoteBundle::from_fs(&fs);
495        bundle.get_xattr(node, name)
496    }
497
498    fn list_xattrs(
499        &self,
500        _locked: &mut Locked<FileOpsCore>,
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 starnix_uapi::errors::Errno;
535    use starnix_uapi::file_mode::{AccessCheck, FileMode};
536    use starnix_uapi::open_flags::OpenFlags;
537    use starnix_uapi::{ino_t, off_t};
538    use std::collections::{HashMap, HashSet};
539    use {fidl_fuchsia_io as fio, zx};
540
541    #[::fuchsia::test]
542    async fn test_read_image() {
543        spawn_kernel_and_run_with_pkgfs(async |locked, current_task| {
544            let kernel = current_task.kernel();
545            let rights = fio::PERM_READABLE | fio::PERM_EXECUTABLE;
546            let (server, client) = zx::Channel::create();
547            fdio::open("/pkg", rights, server).expect("failed to open /pkg");
548            let fs = RemoteBundle::new_fs(
549                locked,
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(locked, &current_task, &mut context, "foo".into())
562                .expect("lookup failed");
563
564            let test_file = test_dir
565                .lookup_child(locked, &current_task, &mut context, "file".into())
566                .expect("lookup failed")
567                .open(locked, &current_task, OpenFlags::RDONLY, AccessCheck::default())
568                .expect("open failed");
569
570            let mut buffer = VecOutputBuffer::new(64);
571            assert_eq!(test_file.read(locked, &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(locked, &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(locked, &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(locked, &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(locked, &current_task, &mut context, "symlink".into())
611                .expect("lookup failed");
612
613            if let SymlinkTarget::Path(target) =
614                test_symlink.readlink(locked, &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(locked, &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(locked, &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}