Skip to main content

starnix_modules_erofs/
lib.rs

1// Copyright 2026 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
5//! EROFS (Enhanced Read-Only File System) module for Starnix.
6//!
7//! Supports two mounting modes:
8//! 1. **In-process driver-mapped mode**: When the backing file supports `FileOps::get_file_token`
9//!    (e.g., an EROFS image stored in an Fxblob package blob, mounted directly or via a loop
10//!    device / `dm-verity`) and `fuchsia.storage.mapping.FileMapper` is available, Starnix parses
11//!    EROFS metadata in-process using `erofs::ErofsFilesystem` and creates pager-backed file VMOs
12//!    through a child `MapperSession` (`erofs::VolumeMapper`). This avoids per-operation FIDL
13//!    round-trips to an external filesystem component while keeping `zx::Pager` out of
14//!    `starnix_kernel` so container suspend cannot freeze pager threads and deadlock.
15//! 2. **Out-of-process component fallback**: When `FileMapper` is unavailable, the source file does
16//!    not provide a page-aligned `FileToken`, or `FileMapper.OpenSession` fails, Starnix falls back
17//!    to the out-of-process `fuchsia.erofs.Erofs` component (`Erofs.Serve`) and mounts the served
18//!    directory channel via `RemoteFs`.
19
20#![recursion_limit = "256"]
21
22use erofs::readers::VmoReader;
23use erofs::{
24    DirectoryEntry as ErofsDirEntry, DirectoryNode, ErofsFilesystem, FileNode, FileType, Node,
25    NodeInner, SymlinkNode, VolumeMapper,
26};
27use fidl_fuchsia_erofs::{ErofsMarker, ErofsServeRequest};
28use fidl_fuchsia_io as fio;
29use fidl_fuchsia_storage_mapping::FileMapperMarker;
30use fuchsia_component::client::connect_to_protocol_sync;
31use fuchsia_runtime::UtcInstant;
32use starnix_core::fs::fuchsia::RemoteFs;
33use starnix_core::mm::ProtectionFlags;
34use starnix_core::mm::memory::MemoryObject;
35use starnix_core::task::CurrentTask;
36use starnix_core::vfs::fs_args::MountParams;
37use starnix_core::vfs::{
38    CacheMode, DEFAULT_BYTES_PER_BLOCK, DirEntry, DirectoryEntryType, DirentSink, FileObject,
39    FileOps, FileSystem, FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode, FsNodeFlags,
40    FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, FsString, MemoryRegularFile, SeekTarget,
41    SymlinkTarget, ValueOrSize, default_seek, emit_dotdot, fileops_impl_directory,
42    fileops_impl_noop_sync, fs_node_impl_dir_readonly, fs_node_impl_not_dir, fs_node_impl_symlink,
43};
44use starnix_logging::{impossible_error, log_error, log_info, log_warn};
45use starnix_types::vfs::default_statfs;
46use starnix_uapi::errors::Errno;
47use starnix_uapi::file_mode::FileMode;
48use starnix_uapi::mount_flags::FileSystemFlags;
49use starnix_uapi::open_flags::OpenFlags;
50use starnix_uapi::{
51    EROFS_SUPER_MAGIC_V1, errno, error, from_status_like_fdio, ino_t, off_t, statfs,
52};
53use std::sync::atomic::{AtomicBool, Ordering};
54use std::sync::{Arc, OnceLock};
55use zx::{MonotonicInstant, VmoChildOptions};
56
57/// Caches whether the `fuchsia.storage.mapping.FileMapper` protocol is routed and active.
58///
59/// Because `connect_to_protocol_sync` only pipelines the channel open to `/svc`, an unrouted
60/// protocol manifests as `PEER_CLOSED` on the first `FileMapper.OpenSession` call. Clearing this
61/// flag on transport error avoids repeated failed connection attempts on subsequent mounts, while
62/// per-file application errors (`Result::Err(zx_status)`) fall back without disabling `FileMapper`
63/// globally.
64static FILE_MAPPER_AVAILABLE: AtomicBool = AtomicBool::new(true);
65
66/// Number of synthetic directory entries (`"."` and `".."`) emitted by `emit_dotdot` before
67/// reading entries from `ErofsFilesystem`.
68const DOT_DOTDOT_OFFSET: off_t = 2;
69
70/// Number of directory entries fetched per `ErofsFilesystem::read_directory` batch.
71const READDIR_BATCH_SIZE: usize = 16;
72
73/// Starnix `FileSystemOps` implementation for an in-process EROFS volume.
74struct ErofsFileSystem {
75    /// Parsed EROFS filesystem metadata and reader over the image VMO.
76    fs: Arc<ErofsFilesystem>,
77    /// Driver-backed child `MapperSession` used to create pager-backed VMOs for regular files.
78    mapper: VolumeMapper,
79}
80
81impl FileSystemOps for ErofsFileSystem {
82    fn name(&self) -> &'static FsStr {
83        "erofs".into()
84    }
85
86    fn uses_external_node_ids(&self) -> bool {
87        true
88    }
89
90    fn statfs(&self, _fs: &FileSystem, _current_task: &CurrentTask) -> Result<statfs, Errno> {
91        let mut stat = default_statfs(EROFS_SUPER_MAGIC_V1);
92        let block_size = self.fs.block_size();
93        stat.f_bsize = block_size as i64;
94        stat.f_frsize = block_size as i64;
95        // EROFS is strictly read-only, so free block/inode counts remain 0 from `default_statfs`.
96        stat.f_blocks = (self.fs.total_bytes() / block_size).try_into().unwrap_or(i64::MAX);
97        stat.f_files = self.fs.total_inodes().try_into().unwrap_or(i64::MAX);
98        Ok(stat)
99    }
100
101    fn reconfigure(
102        &self,
103        fs: &FileSystem,
104        _current_task: &CurrentTask,
105        new_flags: FileSystemFlags,
106        _params: &MountParams,
107    ) -> Result<(), Errno> {
108        if !new_flags.contains(FileSystemFlags::RDONLY) {
109            return error!(EROFS);
110        }
111        fs.options.flags.store(new_flags, Ordering::Relaxed);
112        Ok(())
113    }
114}
115
116/// Converts on-disk EROFS inode metadata (`NodeInner`) into Starnix's `FsNodeInfo`.
117fn fs_node_info(node: &NodeInner, block_size: u64) -> FsNodeInfo {
118    // EROFS only stores a single timestamp (mtime / build time) per inode, so all three POSIX
119    // timestamps are initialized to `mtime_ns`.
120    let time = UtcInstant::from_nanos(node.mtime_ns() as i64);
121    let storage_size = node.storage_size(block_size);
122    FsNodeInfo {
123        mode: FileMode::from_bits(node.mode().into()),
124        link_count: node.link_count() as usize,
125        uid: node.uid(),
126        gid: node.gid(),
127        size: node.size() as usize,
128        // `st_blocks` is always reported in 512-byte (`DEFAULT_BYTES_PER_BLOCK`) units.
129        blksize: DEFAULT_BYTES_PER_BLOCK,
130        blocks: storage_size.div_ceil(DEFAULT_BYTES_PER_BLOCK as u64) as usize,
131        time_status_change: time,
132        time_access: time,
133        time_modify: time,
134        ..Default::default()
135    }
136}
137
138/// Reads a single extended attribute `name` from `erofs_node`, mapping a missing attribute to
139/// `ENODATA`.
140fn get_node_xattr(
141    node: &FsNode,
142    erofs_node: &NodeInner,
143    name: &FsStr,
144) -> Result<ValueOrSize<Box<[u8]>>, Errno> {
145    let fs = node.fs();
146    let fs_ops = fs.downcast_ops::<ErofsFileSystem>().unwrap();
147    match fs_ops.fs.get_xattr(erofs_node, name.as_ref()).map_err(|e| errno!(EIO, e))? {
148        Some(val) => Ok(val.into_boxed_slice().into()),
149        None => error!(ENODATA),
150    }
151}
152
153/// Lists all extended attribute names on `erofs_node`.
154fn list_node_xattrs(
155    node: &FsNode,
156    erofs_node: &NodeInner,
157) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
158    let fs = node.fs();
159    let fs_ops = fs.downcast_ops::<ErofsFileSystem>().unwrap();
160    let names = fs_ops.fs.list_xattrs(erofs_node).map_err(|e| errno!(EIO, e))?;
161    Ok(names.into_iter().map(FsString::from).collect::<Vec<_>>().into())
162}
163
164/// `FsNodeOps` implementation for an EROFS directory inode.
165struct ErofsDirectory {
166    node: DirectoryNode,
167}
168
169impl FsNodeOps for ErofsDirectory {
170    fs_node_impl_dir_readonly!();
171
172    fn create_file_ops(
173        &self,
174        _node: &FsNode,
175        _current_task: &CurrentTask,
176        _flags: OpenFlags,
177    ) -> Result<Box<dyn FileOps>, Errno> {
178        Ok(Box::new(ErofsDirFileObject { node: self.node.clone() }))
179    }
180
181    fn lookup(
182        &self,
183        entry: &DirEntry,
184        _current_task: &CurrentTask,
185        name: &FsStr,
186    ) -> Result<FsNodeHandle, Errno> {
187        // The `erofs` parser validates directory entry names as UTF-8, so any non-UTF-8 lookup
188        // name cannot match an existing entry.
189        let name_str = std::str::from_utf8(name.as_ref()).map_err(|_| errno!(ENOENT))?;
190        let fs = entry.node.fs();
191        let fs_ops = fs.downcast_ops::<ErofsFileSystem>().unwrap();
192        let child_node = fs_ops
193            .fs
194            .lookup(&self.node, name_str)
195            .map_err(|e| errno!(EIO, e))?
196            .ok_or_else(|| errno!(ENOENT, name))?;
197        // EROFS 64-bit node IDs (`nid`) uniquely identify inodes within the volume and are used
198        // directly as Starnix inode numbers (`ino_t`).
199        let nid = child_node.nid() as ino_t;
200        fs.get_or_create_node(nid, || {
201            let info = fs_node_info(&child_node, fs_ops.fs.block_size());
202            let ops: Box<dyn FsNodeOps> = match child_node {
203                Node::Directory(dir) => Box::new(ErofsDirectory { node: dir }),
204                Node::File(file) => Box::new(ErofsFile { node: file, memory: OnceLock::new() }),
205                Node::Symlink(symlink) => {
206                    Box::new(ErofsSymlink { node: symlink, target: OnceLock::new() })
207                }
208            };
209            Ok(FsNode::new_uncached(nid, ops, &fs, info, FsNodeFlags::empty()))
210        })
211    }
212
213    fn get_xattr(
214        &self,
215        node: &FsNode,
216        _current_task: &CurrentTask,
217        name: &FsStr,
218        _max_size: usize,
219    ) -> Result<ValueOrSize<Box<[u8]>>, Errno> {
220        get_node_xattr(node, &self.node, name)
221    }
222
223    fn list_xattrs(
224        &self,
225        node: &FsNode,
226        _current_task: &CurrentTask,
227        _max_size: usize,
228    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
229        list_node_xattrs(node, &self.node)
230    }
231}
232
233/// `FileOps` implementation for an open EROFS directory descriptor.
234struct ErofsDirFileObject {
235    node: DirectoryNode,
236}
237
238impl FileOps for ErofsDirFileObject {
239    fileops_impl_directory!();
240    fileops_impl_noop_sync!();
241
242    fn seek(
243        &self,
244        _file: &FileObject,
245        _current_task: &CurrentTask,
246        current_offset: off_t,
247        target: SeekTarget,
248    ) -> Result<off_t, Errno> {
249        default_seek(current_offset, target, || error!(EINVAL))
250    }
251
252    fn readdir(
253        &self,
254        file: &FileObject,
255        _current_task: &CurrentTask,
256        sink: &mut dyn DirentSink,
257    ) -> Result<(), Errno> {
258        // `emit_dotdot` emits `"."` at `sink.offset() == 0` and `".."` at `sink.offset() == 1`
259        // (resolving `".."` via the VFS `DirEntry` parent across mount boundaries), leaving
260        // `sink.offset() >= DOT_DOTDOT_OFFSET`.
261        emit_dotdot(file, sink)?;
262
263        let fs = file.node().fs();
264        let fs_ops = fs.downcast_ops::<ErofsFileSystem>().unwrap();
265        let mut entry_offset = (sink.offset() - DOT_DOTDOT_OFFSET) as usize;
266        let mut buffer = vec![ErofsDirEntry::default(); READDIR_BATCH_SIZE];
267
268        loop {
269            let filled = fs_ops
270                .fs
271                .read_directory(&self.node, entry_offset, &mut buffer)
272                .map_err(|e| errno!(EIO, e))?;
273            for i in 0..filled {
274                let entry = &buffer[i];
275                let next_entry_offset = entry_offset + i + 1;
276                // On-disk EROFS directories include `"."` and `".."` entries, which were already
277                // emitted by `emit_dotdot` above. Skip them while still advancing
278                // `next_entry_offset` so subsequent entries map to stable offsets.
279                if entry.name == "." || entry.name == ".." {
280                    continue;
281                }
282                let entry_type = match entry.file_type {
283                    FileType::RegFile => DirectoryEntryType::REG,
284                    FileType::Dir => DirectoryEntryType::DIR,
285                    FileType::ChrDev => DirectoryEntryType::CHR,
286                    FileType::BlkDev => DirectoryEntryType::BLK,
287                    FileType::Fifo => DirectoryEntryType::FIFO,
288                    FileType::Sock => DirectoryEntryType::SOCK,
289                    FileType::Symlink => DirectoryEntryType::LNK,
290                    FileType::Unknown => DirectoryEntryType::UNKNOWN,
291                };
292                sink.add(
293                    entry.nid as ino_t,
294                    next_entry_offset as off_t + DOT_DOTDOT_OFFSET,
295                    entry_type,
296                    entry.name.as_bytes().into(),
297                )?;
298            }
299            if filled < buffer.len() {
300                break;
301            }
302            entry_offset += filled;
303        }
304        Ok(())
305    }
306}
307
308/// `FsNodeOps` implementation for an EROFS regular file inode.
309struct ErofsFile {
310    node: FileNode,
311    /// Lazily initialized `MemoryObject` wrapping the file's VMO, created on the first `open`
312    /// (`create_file_ops`) rather than during `lookup`/`stat`.
313    memory: OnceLock<Arc<MemoryObject>>,
314}
315
316impl FsNodeOps for ErofsFile {
317    fs_node_impl_not_dir!();
318
319    fn create_file_ops(
320        &self,
321        node: &FsNode,
322        _current_task: &CurrentTask,
323        flags: OpenFlags,
324    ) -> Result<Box<dyn FileOps>, Errno> {
325        if flags.can_write() {
326            return error!(EROFS);
327        }
328        let memory = if let Some(memory) = self.memory.get() {
329            memory.clone()
330        } else {
331            let fs = node.fs();
332            let fs_ops = fs.downcast_ops::<ErofsFileSystem>().unwrap();
333            let vmo = fs_ops
334                .mapper
335                .get_or_create_vmo(&fs_ops.fs, &self.node)
336                .map_err(|status| from_status_like_fdio!(status))?;
337            let memory = Arc::new(MemoryObject::from(vmo).with_zx_name(b"starnix:erofs"));
338            // If another thread raced to initialize `self.memory`, use the winning `MemoryObject`.
339            let _ = self.memory.set(memory);
340            self.memory.get().unwrap().clone()
341        };
342        Ok(Box::new(MemoryRegularFile::new(memory)))
343    }
344
345    fn get_xattr(
346        &self,
347        node: &FsNode,
348        _current_task: &CurrentTask,
349        name: &FsStr,
350        _max_size: usize,
351    ) -> Result<ValueOrSize<Box<[u8]>>, Errno> {
352        get_node_xattr(node, &self.node, name)
353    }
354
355    fn list_xattrs(
356        &self,
357        node: &FsNode,
358        _current_task: &CurrentTask,
359        _max_size: usize,
360    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
361        list_node_xattrs(node, &self.node)
362    }
363}
364
365/// `FsNodeOps` implementation for an EROFS symbolic link inode.
366struct ErofsSymlink {
367    node: SymlinkNode,
368    /// Cached symlink target string, populated on the first `readlink` call.
369    target: OnceLock<FsString>,
370}
371
372impl FsNodeOps for ErofsSymlink {
373    fs_node_impl_symlink!();
374
375    fn readlink(&self, node: &FsNode, _current_task: &CurrentTask) -> Result<SymlinkTarget, Errno> {
376        if let Some(target) = self.target.get() {
377            return Ok(SymlinkTarget::Path(target.clone()));
378        }
379        let fs = node.fs();
380        let fs_ops = fs.downcast_ops::<ErofsFileSystem>().unwrap();
381        let target: FsString =
382            fs_ops.fs.read_symlink(&self.node).map_err(|e| errno!(EIO, e))?.into();
383        let _ = self.target.set(target.clone());
384        Ok(SymlinkTarget::Path(target))
385    }
386
387    fn get_xattr(
388        &self,
389        node: &FsNode,
390        _current_task: &CurrentTask,
391        name: &FsStr,
392        _max_size: usize,
393    ) -> Result<ValueOrSize<Box<[u8]>>, Errno> {
394        get_node_xattr(node, &self.node, name)
395    }
396
397    fn list_xattrs(
398        &self,
399        node: &FsNode,
400        _current_task: &CurrentTask,
401        _max_size: usize,
402    ) -> Result<ValueOrSize<Vec<FsString>>, Errno> {
403        list_node_xattrs(node, &self.node)
404    }
405}
406
407/// Mounts an EROFS filesystem from `options.source` (with optional page-aligned `fsoffset` mount
408/// parameter), using in-process driver mapping via `fuchsia.storage.mapping.FileMapper` when
409/// available and falling back to the out-of-process `fuchsia.erofs.Erofs` component otherwise.
410pub fn new_fs(
411    current_task: &CurrentTask,
412    options: FileSystemOptions,
413) -> Result<FileSystemHandle, Errno> {
414    let kernel = current_task.kernel();
415
416    // EROFS is strictly read-only.
417    if !options.flags.load(Ordering::Relaxed).contains(FileSystemFlags::RDONLY) {
418        return Err(errno!(EROFS));
419    }
420
421    let fsoffset = options.params.get_as::<u64>(b"fsoffset")?;
422    let page_size = zx::system_get_page_size() as u64;
423
424    let source_device = current_task.open_file(options.source.as_ref(), OpenFlags::RDONLY)?;
425    let memory = source_device.get_memory(current_task, None, ProtectionFlags::READ)?;
426    let backing_vmo = memory
427        .as_vmo()
428        .ok_or_else(|| errno!(EINVAL))?
429        .duplicate_handle(zx::Rights::SAME_RIGHTS)
430        .map_err(impossible_error)?;
431
432    // If `fsoffset` is non-zero, create a `SLICE` child VMO starting at `fsoffset` so that offset
433    // 0 of `backing_vmo` corresponds to the start of the EROFS image for both `VmoReader` and the
434    // `Erofs.Serve` fallback.
435    let backing_vmo = match fsoffset {
436        None => {
437            log_info!("mounting erofs image source={:?}", options.source);
438            backing_vmo
439        }
440        Some(fsoffset) if fsoffset > 0 => {
441            if fsoffset % page_size != 0 {
442                log_warn!("fsoffset {} is not page-aligned (page size {})", fsoffset, page_size);
443                return Err(errno!(EINVAL));
444            }
445            let vmo_size = backing_vmo.get_size().map_err(impossible_error)?;
446            let clone_len =
447                vmo_size.checked_sub(fsoffset).filter(|&len| len > 0).ok_or_else(|| {
448                    log_warn!("fsoffset {} exceeds or equals vmo size {}", fsoffset, vmo_size);
449                    errno!(EINVAL)
450                })?;
451            log_info!("mounting erofs image with fsoffset={}, len={}", fsoffset, clone_len);
452            backing_vmo.create_child(VmoChildOptions::SLICE, fsoffset, clone_len).map_err(|e| {
453                log_error!("Failed to create child VMO slice for fsoffset {}: {:?}", fsoffset, e);
454                errno!(EINVAL)
455            })?
456        }
457        Some(_) => {
458            log_info!("mounting erofs image with fsoffset of zero");
459            backing_vmo
460        }
461    };
462
463    // Fast path: if the source device provides a page-aligned `FileToken` for the underlying
464    // Fuchsia blob and `FileMapper` is available, open a child `MapperSession` and serve EROFS
465    // in-process.
466    if FILE_MAPPER_AVAILABLE.load(Ordering::Relaxed) {
467        if let Ok(file_token) = source_device
468            .get_file_token(current_task)
469            .and_then(|t| t.add_offset(fsoffset.unwrap_or(0)))
470            .ok()
471            .filter(|t| t.offset % page_size == 0)
472            .ok_or(())
473        {
474            if let Ok(file_mapper) = connect_to_protocol_sync::<FileMapperMarker>() {
475                let token = file_token.token;
476                let file_offset = file_token.offset;
477                let keep_alive = file_token.keep_alive;
478                let mapper_res =
479                    VolumeMapper::open_with_session(file_offset, |mapping_vmo, session_server| {
480                        file_mapper
481                            .open_session(
482                                token,
483                                mapping_vmo,
484                                session_server,
485                                MonotonicInstant::INFINITE,
486                            )
487                            .map_err(|_| {
488                                FILE_MAPPER_AVAILABLE.store(false, Ordering::Relaxed);
489                                zx::Status::PEER_CLOSED
490                            })?
491                            .map_err(zx::Status::err_from_raw)
492                    });
493                // `keep_alive` keeps the `fuchsia.io/Node.GetToken` token registered in the server's
494                // VFS token registry until `FileMapper.OpenSession` has resolved it to a `VmoBlob`
495                // and opened the child `MapperSession`.
496                drop(keep_alive);
497                if let Ok(mapper) = mapper_res {
498                    let reader = Arc::new(
499                        VmoReader::new(Arc::new(backing_vmo)).map_err(|e| errno!(EIO, e))?,
500                    );
501                    let erofs_fs =
502                        Arc::new(ErofsFilesystem::new(reader).map_err(|e| errno!(EINVAL, e))?);
503                    let root_node = erofs_fs.root_node();
504                    let root_nid = root_node.nid() as ino_t;
505                    let root_info = fs_node_info(&root_node, erofs_fs.block_size());
506                    let fs_ops = ErofsFileSystem { fs: erofs_fs, mapper };
507                    let fs = FileSystem::new(
508                        kernel,
509                        CacheMode::Cached(kernel.fs_cache_config()),
510                        fs_ops,
511                        options,
512                    )?;
513                    fs.create_root_with_info(
514                        root_nid,
515                        ErofsDirectory { node: root_node },
516                        root_info,
517                    );
518                    return Ok(fs);
519                }
520            } else {
521                FILE_MAPPER_AVAILABLE.store(false, Ordering::Relaxed);
522            }
523        }
524    }
525
526    // Fallback path: delegate serving the EROFS image to the out-of-process `fuchsia.erofs.Erofs`
527    // component and mount the resulting `fuchsia.io/Directory` channel as a `RemoteFs`.
528    let erofs_provider = connect_to_protocol_sync::<ErofsMarker>().map_err(|_| errno!(ENOENT))?;
529
530    let (root_client, root_server) = fidl::endpoints::create_endpoints();
531    erofs_provider
532        .serve(
533            ErofsServeRequest {
534                backing_vmo: Some(backing_vmo),
535                root: Some(root_server),
536                ..Default::default()
537            },
538            MonotonicInstant::INFINITE,
539        )
540        .map_err(|e| {
541            log_error!("FIDL error on Erofs.Serve: {e:?}");
542            errno!(EIO)
543        })?
544        .map_err(|e| {
545            log_error!("Erofs.Serve failed: {e:?}");
546            from_status_like_fdio!(zx::Status::err_from_raw(e))
547        })?;
548
549    let (remotefs, root_node, info, node_id) =
550        RemoteFs::new(root_client.into_channel(), fio::PERM_READABLE, "erofs")?;
551
552    let fs =
553        FileSystem::new(kernel, CacheMode::Cached(kernel.fs_cache_config()), remotefs, options)?;
554
555    fs.create_root_with_info(node_id, root_node, info);
556
557    Ok(fs)
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use fidl_fuchsia_storage_block as fblock;
564    use futures::TryStreamExt as _;
565    use starnix_core::testing::spawn_kernel_and_run;
566    use starnix_core::vfs::buffers::VecOutputBuffer;
567    use starnix_core::vfs::{LookupContext, Namespace, NamespaceNode, SymlinkMode};
568    use std::sync::atomic::AtomicUsize;
569
570    struct TestDirentSink {
571        offset: off_t,
572        max_entries: Option<usize>,
573        entries: Vec<(String, DirectoryEntryType)>,
574    }
575
576    impl TestDirentSink {
577        fn new() -> Self {
578            Self { offset: 0, max_entries: None, entries: Vec::new() }
579        }
580
581        fn with_limit(offset: off_t, max_entries: usize) -> Self {
582            Self { offset, max_entries: Some(max_entries), entries: Vec::new() }
583        }
584    }
585
586    impl DirentSink for TestDirentSink {
587        fn add(
588            &mut self,
589            _inode_num: ino_t,
590            offset: off_t,
591            entry_type: DirectoryEntryType,
592            name: &FsStr,
593        ) -> Result<(), Errno> {
594            if let Some(max) = self.max_entries {
595                if self.entries.len() >= max {
596                    return error!(ENOSPC);
597                }
598            }
599            self.offset = offset;
600            self.entries.push((String::from_utf8_lossy(name.as_ref()).into_owned(), entry_type));
601            Ok(())
602        }
603
604        fn offset(&self) -> off_t {
605            self.offset
606        }
607    }
608
609    fn mount_test_erofs(
610        current_task: &CurrentTask,
611        create_vmo_count: Arc<AtomicUsize>,
612        fail_create_vmo: bool,
613    ) -> (FileSystemHandle, Arc<Namespace>) {
614        mount_test_erofs_from_path(
615            current_task,
616            "/pkg/data/simple.erofs",
617            create_vmo_count,
618            fail_create_vmo,
619        )
620    }
621
622    fn mount_test_erofs_from_path(
623        current_task: &CurrentTask,
624        image_path: &str,
625        create_vmo_count: Arc<AtomicUsize>,
626        fail_create_vmo: bool,
627    ) -> (FileSystemHandle, Arc<Namespace>) {
628        let erofs_bytes = std::fs::read(image_path).expect("read erofs image");
629        let expected_file1 = std::fs::read("/pkg/data/simple/file1").expect("read file1");
630        let expected_photosynthesis =
631            std::fs::read("/pkg/data/simple/photosynthesis").expect("read photosynthesis");
632
633        let mapper = VolumeMapper::open_with_session(4096, move |_mapping_vmo, server_end| {
634            std::thread::spawn(move || {
635                let mut executor = fuchsia_async::LocalExecutor::default();
636                executor.run_singlethreaded(async move {
637                    let mut stream = server_end.into_stream();
638                    let mut next_key = 1u64;
639                    while let Ok(Some(req)) = stream.try_next().await {
640                        match req {
641                            fblock::MapperSessionRequest::CreateVmo { size, responder, .. } => {
642                                create_vmo_count.fetch_add(1, Ordering::Relaxed);
643                                if fail_create_vmo {
644                                    let _ = responder.send(Err(zx::Status::IO.into_raw()));
645                                    continue;
646                                }
647                                let key = next_key;
648                                next_key += 1;
649                                let vmo = zx::Vmo::create(size).unwrap();
650                                if size == expected_file1.len() as u64 {
651                                    vmo.write(&expected_file1, 0).unwrap();
652                                } else if size == expected_photosynthesis.len() as u64 {
653                                    vmo.write(&expected_photosynthesis, 0).unwrap();
654                                }
655                                let _ = responder.send(Ok((key, vmo)));
656                            }
657                            fblock::MapperSessionRequest::CloseVmo { .. } => {}
658                            fblock::MapperSessionRequest::Close { responder } => {
659                                let _ = responder.send(Ok(()));
660                                break;
661                            }
662                            _ => unreachable!(),
663                        }
664                    }
665                });
666            });
667            Ok(())
668        })
669        .expect("open_with_session");
670
671        let vmo = zx::Vmo::create(erofs_bytes.len() as u64).unwrap();
672        vmo.write(&erofs_bytes, 0).unwrap();
673        let reader = Arc::new(VmoReader::new(Arc::new(vmo)).unwrap());
674        let erofs_fs = Arc::new(ErofsFilesystem::new(reader).unwrap());
675
676        let kernel = current_task.kernel();
677        let root_node = erofs_fs.root_node();
678        let root_nid = root_node.nid() as ino_t;
679        let root_info = fs_node_info(&root_node, erofs_fs.block_size());
680        let fs_ops = ErofsFileSystem { fs: erofs_fs, mapper };
681        let fs = FileSystem::new(
682            kernel,
683            CacheMode::Cached(kernel.fs_cache_config()),
684            fs_ops,
685            FileSystemOptions::default(),
686        )
687        .expect("FileSystem::new");
688        fs.create_root_with_info(root_nid, ErofsDirectory { node: root_node }, root_info);
689        let ns = Namespace::new(fs.clone());
690        (fs, ns)
691    }
692
693    fn lookup_node(
694        current_task: &CurrentTask,
695        parent: &NamespaceNode,
696        name: &[u8],
697    ) -> NamespaceNode {
698        let mut context = LookupContext::new(SymlinkMode::NoFollow);
699        parent.lookup_child(current_task, &mut context, name.into()).expect("lookup_child")
700    }
701
702    #[::fuchsia::test]
703    async fn test_statfs_and_node_metadata() {
704        spawn_kernel_and_run(async move |current_task| {
705            let expected_file1 = std::fs::read("/pkg/data/simple/file1").expect("read file1");
706            let (fs, ns) = mount_test_erofs(current_task, Arc::new(AtomicUsize::new(0)), false);
707
708            let stat = fs.statfs(current_task).expect("statfs");
709            assert_eq!(stat.f_type, EROFS_SUPER_MAGIC_V1 as i64);
710            assert_eq!(stat.f_bsize, 4096);
711            assert!(stat.f_blocks > 0);
712            assert!(stat.f_files > 0);
713
714            // Remounting without RDONLY must fail with EROFS, whereas keeping RDONLY succeeds.
715            assert_eq!(
716                fs.reconfigure(current_task, FileSystemFlags::empty(), &MountParams::default()),
717                error!(EROFS)
718            );
719            assert!(
720                fs.reconfigure(current_task, FileSystemFlags::RDONLY, &MountParams::default())
721                    .is_ok()
722            );
723
724            // `new_fs` without RDONLY must fail with EROFS.
725            assert_eq!(
726                new_fs(current_task, FileSystemOptions::default()).map(|_| ()),
727                error!(EROFS)
728            );
729
730            // Create an 8192-byte backing file in TmpFs to test `new_fs` `fsoffset` validation.
731            let img_file = current_task
732                .open_file_at(
733                    starnix_core::vfs::FdNumber::AT_FDCWD,
734                    "/image.bin".into(),
735                    OpenFlags::CREAT | OpenFlags::RDWR,
736                    FileMode::from_bits(0o644),
737                    starnix_uapi::vfs::ResolveFlags::empty(),
738                )
739                .expect("create /image.bin");
740            img_file
741                .node()
742                .truncate(current_task, &current_task.fs().root().mount, 8192)
743                .expect("truncate /image.bin");
744            for bad_param in [b"fsoffset=100".as_slice(), b"fsoffset=8192", b"fsoffset=16384"] {
745                let options = FileSystemOptions {
746                    source: "/image.bin".into(),
747                    flags: FileSystemFlags::RDONLY.into(),
748                    params: MountParams::parse(bad_param.into()).expect("parse MountParams"),
749                };
750                assert_eq!(new_fs(current_task, options).map(|_| ()), error!(EINVAL));
751            }
752
753            let root = ns.root();
754            {
755                let root_info = root.entry.node.info();
756                assert!(root_info.mode.is_dir());
757                assert!(root_info.link_count >= 2);
758            }
759
760            let file1_entry = lookup_node(current_task, &root, b"file1");
761            {
762                let file1_info = file1_entry.entry.node.info();
763                assert!(file1_info.mode.is_reg());
764                assert_eq!(file1_info.size, expected_file1.len());
765                assert_eq!(file1_info.blksize, DEFAULT_BYTES_PER_BLOCK);
766                assert_eq!(
767                    file1_info.blocks,
768                    expected_file1.len().div_ceil(DEFAULT_BYTES_PER_BLOCK)
769                );
770            }
771
772            let symlink_entry = lookup_node(current_task, &root, b"symlink_to_file1");
773            assert!(symlink_entry.entry.node.info().mode.is_lnk());
774            let SymlinkTarget::Path(target) =
775                symlink_entry.readlink(current_task).expect("readlink")
776            else {
777                panic!("expected SymlinkTarget::Path");
778            };
779            assert_eq!(target, "file1");
780        })
781        .await;
782    }
783
784    #[::fuchsia::test]
785    async fn test_directory_readdir_pagination_and_seek() {
786        spawn_kernel_and_run(async move |current_task| {
787            let (_fs, ns) = mount_test_erofs(current_task, Arc::new(AtomicUsize::new(0)), false);
788            let root = ns.root();
789
790            // 1. Root directory readdir
791            let root_dir = root.open(current_task, OpenFlags::RDONLY).expect("open root");
792            let mut sink = TestDirentSink::new();
793            root_dir.readdir(current_task, &mut sink).expect("readdir root");
794            let names: Vec<&str> = sink.entries.iter().map(|(n, _)| n.as_str()).collect();
795            assert_eq!(
796                names,
797                vec![
798                    ".",
799                    "..",
800                    "file1",
801                    "large_dir",
802                    "mixed_compression",
803                    "photosynthesis",
804                    "quantum",
805                    "symlink_to_file1",
806                ]
807            );
808
809            // 2. Directory seek (Set, Cur, and negative offset error)
810            assert_eq!(root_dir.seek(current_task, SeekTarget::Set(0)).unwrap(), 0);
811            assert_eq!(root_dir.seek(current_task, SeekTarget::Cur(3)).unwrap(), 3);
812            assert_eq!(root_dir.seek(current_task, SeekTarget::Set(-1)), error!(EINVAL));
813
814            // 3. Multi-batch readdir on `large_dir` (> 16 entries across multiple blocks)
815            let large_dir_node = lookup_node(current_task, &root, b"large_dir");
816            let large_dir =
817                large_dir_node.open(current_task, OpenFlags::RDONLY).expect("open large_dir");
818            let mut full_sink = TestDirentSink::new();
819            large_dir.readdir(current_task, &mut full_sink).expect("readdir large_dir");
820            assert!(
821                full_sink.entries.len() > 32,
822                "expected > 32 entries in large_dir, got {}",
823                full_sink.entries.len()
824            );
825            assert_eq!(full_sink.entries[0], (".".to_string(), DirectoryEntryType::DIR));
826            assert_eq!(full_sink.entries[1], ("..".to_string(), DirectoryEntryType::DIR));
827            for (name, kind) in &full_sink.entries[2..] {
828                assert!(name.starts_with("file_number_"), "unexpected entry: {name}");
829                assert_eq!(*kind, DirectoryEntryType::REG);
830            }
831
832            // 4. Resuming readdir after partial-buffer ENOSPC (5 entries at a time)
833            let mut paged_entries = Vec::new();
834            let mut offset = 0;
835            loop {
836                let mut page_sink = TestDirentSink::with_limit(offset, 5);
837                let res = large_dir.readdir(current_task, &mut page_sink);
838                let count = page_sink.entries.len();
839                offset = page_sink.offset();
840                paged_entries.extend(page_sink.entries);
841                if res.is_ok() {
842                    break;
843                }
844                assert_eq!(res, error!(ENOSPC));
845                assert_eq!(count, 5);
846            }
847            assert_eq!(paged_entries, full_sink.entries);
848        })
849        .await;
850    }
851
852    #[::fuchsia::test]
853    async fn test_file_read_get_memory_and_caching() {
854        spawn_kernel_and_run(async move |current_task| {
855            let expected_file1 = std::fs::read("/pkg/data/simple/file1").expect("read file1");
856            let expected_photosynthesis =
857                std::fs::read("/pkg/data/simple/photosynthesis").expect("read photosynthesis");
858            let create_vmo_count = Arc::new(AtomicUsize::new(0));
859            let (_fs, ns) = mount_test_erofs(current_task, create_vmo_count.clone(), false);
860            let root = ns.root();
861
862            // Lookup alone must not create any VMOs.
863            let file1_entry = lookup_node(current_task, &root, b"file1");
864            assert_eq!(create_vmo_count.load(Ordering::Relaxed), 0);
865
866            // Opening for write must fail with EROFS.
867            assert_eq!(file1_entry.open(current_task, OpenFlags::RDWR).unwrap_err(), errno!(EROFS));
868            assert_eq!(create_vmo_count.load(Ordering::Relaxed), 0);
869
870            // First open creates the VMO via VolumeMapper.
871            let file1 = file1_entry.open(current_task, OpenFlags::RDONLY).expect("open file1");
872            assert_eq!(create_vmo_count.load(Ordering::Relaxed), 1);
873
874            // Second open reuses the cached MemoryObject without another CreateVmo RPC.
875            let _file1_again =
876                file1_entry.open(current_task, OpenFlags::RDONLY).expect("re-open file1");
877            assert_eq!(create_vmo_count.load(Ordering::Relaxed), 1);
878
879            let mut buf = VecOutputBuffer::new(expected_file1.len() + 64);
880            let n = file1.read(current_task, &mut buf).expect("read file1");
881            assert_eq!(n, expected_file1.len());
882            assert_eq!(buf.data(), expected_file1.as_slice());
883
884            // Test get_memory (READ, READ|EXEC, and WRITE rejected with EACCES).
885            let mem_ro =
886                file1.get_memory(current_task, None, ProtectionFlags::READ).expect("get_memory RO");
887            assert_eq!(mem_ro.read_to_vec(0, expected_file1.len() as u64).unwrap(), expected_file1);
888            let mem_exec = file1
889                .get_memory(current_task, None, ProtectionFlags::READ | ProtectionFlags::EXEC)
890                .expect("get_memory EXEC");
891            assert!(mem_exec.get_rights().contains(zx::Rights::EXECUTE));
892            assert_eq!(
893                file1.get_memory(current_task, None, ProtectionFlags::WRITE).unwrap_err(),
894                errno!(EACCES)
895            );
896
897            // Test multi-block photosynthesis file.
898            let photo_file = lookup_node(current_task, &root, b"photosynthesis")
899                .open(current_task, OpenFlags::RDONLY)
900                .expect("open photosynthesis");
901            assert_eq!(create_vmo_count.load(Ordering::Relaxed), 2);
902            let mut buf = VecOutputBuffer::new(expected_photosynthesis.len() + 64);
903            let n = photo_file.read(current_task, &mut buf).expect("read photosynthesis");
904            assert_eq!(n, expected_photosynthesis.len());
905            assert_eq!(buf.data(), expected_photosynthesis.as_slice());
906
907            // Test compressed file (`mixed_compression` in `simple_lz4.erofs`), which is
908            // decompressed in-process into an anonymous VMO without calling
909            // `MapperSession.CreateVmo`.
910            let expected_mixed = std::fs::read("/pkg/data/simple/mixed_compression")
911                .expect("read mixed_compression");
912            let lz4_vmo_count = Arc::new(AtomicUsize::new(0));
913            let (_lz4_fs, lz4_ns) = mount_test_erofs_from_path(
914                current_task,
915                "/pkg/data/simple_lz4.erofs",
916                lz4_vmo_count.clone(),
917                false,
918            );
919            let mixed_file = lookup_node(current_task, &lz4_ns.root(), b"mixed_compression")
920                .open(current_task, OpenFlags::RDONLY)
921                .expect("open mixed_compression");
922            assert_eq!(lz4_vmo_count.load(Ordering::Relaxed), 0);
923            let mut buf = VecOutputBuffer::new(expected_mixed.len() + 64);
924            let n = mixed_file.read(current_task, &mut buf).expect("read mixed_compression");
925            assert_eq!(n, expected_mixed.len());
926            assert_eq!(buf.data(), expected_mixed.as_slice());
927        })
928        .await;
929    }
930
931    #[::fuchsia::test]
932    async fn test_xattrs_and_error_paths() {
933        spawn_kernel_and_run(async move |current_task| {
934            let (_fs, ns) = mount_test_erofs(current_task, Arc::new(AtomicUsize::new(0)), false);
935            let root = ns.root();
936
937            // Non-existent and invalid UTF-8 lookups must return ENOENT.
938            let mut context = LookupContext::new(SymlinkMode::NoFollow);
939            assert_eq!(
940                root.lookup_child(current_task, &mut context, "does_not_exist".into()).unwrap_err(),
941                errno!(ENOENT)
942            );
943            let mut context = LookupContext::new(SymlinkMode::NoFollow);
944            assert_eq!(
945                root.lookup_child(current_task, &mut context, b"\xff\xfe".into()).unwrap_err(),
946                errno!(ENOENT)
947            );
948
949            // File xattrs: present and absent (ENODATA).
950            let file1_node = &lookup_node(current_task, &root, b"file1").entry.node;
951            let ValueOrSize::Value(flavor) = file1_node
952                .ops()
953                .get_xattr(file1_node, current_task, "user.flavor".into(), usize::MAX)
954                .expect("get_xattr user.flavor")
955            else {
956                panic!("expected Value");
957            };
958            assert_eq!(&*flavor, b"vanilla");
959            assert_eq!(
960                file1_node
961                    .ops()
962                    .get_xattr(file1_node, current_task, "user.missing".into(), usize::MAX)
963                    .unwrap_err(),
964                errno!(ENODATA)
965            );
966
967            let ValueOrSize::Value(mut xattr_list) = file1_node
968                .ops()
969                .list_xattrs(file1_node, current_task, usize::MAX)
970                .expect("list_xattrs")
971            else {
972                panic!("expected Value");
973            };
974            xattr_list.sort();
975            assert_eq!(
976                xattr_list,
977                vec![
978                    FsString::from("security.selinux"),
979                    FsString::from("user.flavor"),
980                    FsString::from("user.security"),
981                    FsString::from("user.shared"),
982                ]
983            );
984
985            // Directory and symlink xattr calls.
986            let root_node = &root.entry.node;
987            assert!(root_node.ops().list_xattrs(root_node, current_task, usize::MAX).is_ok());
988            assert_eq!(
989                root_node
990                    .ops()
991                    .get_xattr(root_node, current_task, "user.missing".into(), usize::MAX)
992                    .unwrap_err(),
993                errno!(ENODATA)
994            );
995
996            let symlink_node = &lookup_node(current_task, &root, b"symlink_to_file1").entry.node;
997            assert!(symlink_node.ops().list_xattrs(symlink_node, current_task, usize::MAX).is_ok());
998            assert_eq!(
999                symlink_node
1000                    .ops()
1001                    .get_xattr(symlink_node, current_task, "user.missing".into(), usize::MAX)
1002                    .unwrap_err(),
1003                errno!(ENODATA)
1004            );
1005
1006            // MapperSession.CreateVmo failure propagation when opening a file.
1007            let (_bad_fs, bad_ns) =
1008                mount_test_erofs(current_task, Arc::new(AtomicUsize::new(0)), true);
1009            let bad_file1 = lookup_node(current_task, &bad_ns.root(), b"file1");
1010            assert_eq!(bad_file1.open(current_task, OpenFlags::RDONLY).unwrap_err(), errno!(EIO));
1011        })
1012        .await;
1013    }
1014}