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