Skip to main content

vfs/posix/
root_fs.rs

1use super::overlay_fs::{OverlayFileSystem, OverlayMode};
2use super::usage::{
3    RootFilesystemResourceLimits, DEFAULT_MAX_FILESYSTEM_BYTES, DEFAULT_MAX_INODE_COUNT,
4};
5use super::vfs::{
6    normalize_path, MemoryFileSystem, VfsError, VfsResult, VirtualFileSystem, VirtualStat,
7    VirtualUtimeSpec, MAX_PATH_LENGTH,
8};
9use crate::posix::vfs::VirtualDirEntry;
10use base64::Engine;
11use serde::Deserialize;
12use std::collections::BTreeSet;
13
14// The base filesystem fixture is staged into OUT_DIR by build.rs: copied from
15// the canonical `packages/agentos-core/fixtures/base-filesystem.json`
16// during in-tree builds, or from the vendored `assets/base-filesystem.json`
17// copy bundled in the published crate.
18const BUNDLED_BASE_FILESYSTEM_JSON: &str =
19    include_str!(concat!(env!("OUT_DIR"), "/base-filesystem.json"));
20pub const ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "agentos_filesystem_snapshot_v1";
21const LEGACY_AGENTOS_ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "agentos_filesystem_snapshot_v1";
22const ROOT_FILESYSTEM_SNAPSHOT_FIXED_OVERHEAD_BYTES: usize = 4 * 1024;
23const ROOT_FILESYSTEM_SNAPSHOT_ENTRY_OVERHEAD_BYTES: usize = MAX_PATH_LENGTH + 1024;
24const DEFAULT_ROOT_DIRECTORIES: &[&str] = &[
25    "/",
26    "/dev",
27    "/proc",
28    "/tmp",
29    "/bin",
30    "/lib",
31    "/sbin",
32    "/boot",
33    "/etc",
34    "/root",
35    "/run",
36    "/srv",
37    "/sys",
38    "/opt",
39    "/mnt",
40    "/media",
41    "/home",
42    "/usr",
43    "/usr/bin",
44    "/usr/games",
45    "/usr/include",
46    "/usr/lib",
47    "/usr/libexec",
48    "/usr/man",
49    "/usr/local",
50    "/usr/local/bin",
51    "/usr/sbin",
52    "/usr/share",
53    "/usr/share/man",
54    "/var",
55    "/var/cache",
56    "/var/empty",
57    "/var/lib",
58    "/var/lock",
59    "/var/log",
60    "/var/run",
61    "/var/spool",
62    "/var/tmp",
63    "/etc/agentos",
64];
65const KERNEL_RESERVED_BOOTSTRAP_PATH_PREFIXES: &[&str] = &["/dev", "/proc", "/sys"];
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct RootFilesystemError {
69    message: String,
70}
71
72impl RootFilesystemError {
73    fn new(message: impl Into<String>) -> Self {
74        Self {
75            message: message.into(),
76        }
77    }
78}
79
80impl std::fmt::Display for RootFilesystemError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.write_str(&self.message)
83    }
84}
85
86impl std::error::Error for RootFilesystemError {}
87
88impl From<VfsError> for RootFilesystemError {
89    fn from(error: VfsError) -> Self {
90        Self::new(error.to_string())
91    }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum FilesystemEntryKind {
96    File,
97    Directory,
98    Symlink,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct FilesystemEntry {
103    pub path: String,
104    pub kind: FilesystemEntryKind,
105    pub mode: u32,
106    pub uid: u32,
107    pub gid: u32,
108    pub content: Option<Vec<u8>>,
109    pub target: Option<String>,
110}
111
112impl FilesystemEntry {
113    pub fn directory(path: impl Into<String>) -> Self {
114        Self {
115            path: path.into(),
116            kind: FilesystemEntryKind::Directory,
117            mode: 0o755,
118            uid: 0,
119            gid: 0,
120            content: None,
121            target: None,
122        }
123    }
124
125    pub fn file(path: impl Into<String>, content: impl Into<Vec<u8>>) -> Self {
126        Self {
127            path: path.into(),
128            kind: FilesystemEntryKind::File,
129            mode: 0o644,
130            uid: 0,
131            gid: 0,
132            content: Some(content.into()),
133            target: None,
134        }
135    }
136
137    pub fn symlink(path: impl Into<String>, target: impl Into<String>) -> Self {
138        Self {
139            path: path.into(),
140            kind: FilesystemEntryKind::Symlink,
141            mode: 0o777,
142            uid: 0,
143            gid: 0,
144            content: None,
145            target: Some(target.into()),
146        }
147    }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct RootFilesystemSnapshot {
152    pub entries: Vec<FilesystemEntry>,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct RootFilesystemImportLimits {
157    pub max_encoded_snapshot_bytes: Option<usize>,
158    pub max_filesystem_bytes: Option<u64>,
159    pub max_inode_count: Option<usize>,
160}
161
162impl RootFilesystemImportLimits {
163    pub fn from_resource_limits(limits: &impl RootFilesystemResourceLimits) -> Self {
164        Self {
165            max_encoded_snapshot_bytes: encoded_snapshot_limit(
166                limits.max_filesystem_bytes(),
167                limits.max_inode_count(),
168            ),
169            max_filesystem_bytes: limits.max_filesystem_bytes(),
170            max_inode_count: limits.max_inode_count(),
171        }
172    }
173}
174
175impl Default for RootFilesystemImportLimits {
176    fn default() -> Self {
177        Self {
178            max_encoded_snapshot_bytes: encoded_snapshot_limit(
179                Some(DEFAULT_MAX_FILESYSTEM_BYTES),
180                Some(DEFAULT_MAX_INODE_COUNT),
181            ),
182            max_filesystem_bytes: Some(DEFAULT_MAX_FILESYSTEM_BYTES),
183            max_inode_count: Some(DEFAULT_MAX_INODE_COUNT),
184        }
185    }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum RootFilesystemMode {
190    Ephemeral,
191    ReadOnly,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct RootFilesystemDescriptor {
196    pub mode: RootFilesystemMode,
197    pub disable_default_base_layer: bool,
198    pub lowers: Vec<RootFilesystemSnapshot>,
199    pub bootstrap_entries: Vec<FilesystemEntry>,
200}
201
202impl Default for RootFilesystemDescriptor {
203    fn default() -> Self {
204        Self {
205            mode: RootFilesystemMode::Ephemeral,
206            disable_default_base_layer: false,
207            lowers: Vec::new(),
208            bootstrap_entries: Vec::new(),
209        }
210    }
211}
212
213#[derive(Debug)]
214pub struct RootFileSystem {
215    overlay: OverlayFileSystem,
216    mode: RootFilesystemMode,
217    bootstrap_finished: bool,
218}
219
220impl RootFileSystem {
221    pub fn from_descriptor(
222        descriptor: RootFilesystemDescriptor,
223    ) -> Result<Self, RootFilesystemError> {
224        Self::from_descriptor_with_import_limits(descriptor, &RootFilesystemImportLimits::default())
225    }
226
227    pub fn from_descriptor_with_import_limits(
228        descriptor: RootFilesystemDescriptor,
229        limits: &RootFilesystemImportLimits,
230    ) -> Result<Self, RootFilesystemError> {
231        let mut lower_snapshots = descriptor.lowers.clone();
232        if !descriptor.disable_default_base_layer {
233            lower_snapshots.push(load_bundled_base_snapshot_with_limits(limits)?);
234        } else if lower_snapshots.is_empty() {
235            lower_snapshots.push(minimal_root_snapshot());
236        }
237        validate_descriptor_import_limits(
238            &lower_snapshots,
239            &descriptor.bootstrap_entries,
240            limits,
241            "root filesystem descriptor",
242        )?;
243
244        let lowers = lower_snapshots
245            .iter()
246            .map(snapshot_to_memory_filesystem)
247            .collect::<Result<Vec<_>, _>>()?;
248
249        let mut root = Self {
250            overlay: OverlayFileSystem::new(lowers, OverlayMode::Ephemeral),
251            mode: descriptor.mode,
252            bootstrap_finished: false,
253        };
254        root.apply_bootstrap_entries(&descriptor.bootstrap_entries)?;
255        Ok(root)
256    }
257
258    pub fn apply_bootstrap_entries(
259        &mut self,
260        entries: &[FilesystemEntry],
261    ) -> Result<(), RootFilesystemError> {
262        if self.bootstrap_finished {
263            return Err(RootFilesystemError::new(
264                "root filesystem bootstrap is already finished",
265            ));
266        }
267
268        for entry in sort_entries(entries.to_vec()) {
269            if is_kernel_reserved_bootstrap_path(&entry.path) {
270                continue;
271            }
272            apply_entry(&mut self.overlay, &entry)?;
273        }
274        Ok(())
275    }
276
277    pub fn finish_bootstrap(&mut self) {
278        if self.bootstrap_finished {
279            return;
280        }
281        self.bootstrap_finished = true;
282        if self.mode == RootFilesystemMode::ReadOnly {
283            self.overlay.lock_writes();
284        }
285    }
286
287    pub fn snapshot(&mut self) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
288        Ok(RootFilesystemSnapshot {
289            entries: snapshot_virtual_filesystem(&mut self.overlay, "/")?,
290        })
291    }
292
293    pub fn check_rename_copy_up_limits(
294        &mut self,
295        old_path: &str,
296        new_path: &str,
297        max_bytes: Option<u64>,
298        max_inodes: Option<usize>,
299    ) -> VfsResult<()> {
300        self.overlay
301            .check_rename_copy_up_limits(old_path, new_path, max_bytes, max_inodes)
302    }
303}
304
305impl VirtualFileSystem for RootFileSystem {
306    fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
307        self.overlay.read_file(path)
308    }
309
310    fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
311        self.overlay.read_dir(path)
312    }
313
314    fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
315        self.overlay.read_dir_limited(path, max_entries)
316    }
317
318    fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
319        self.overlay.read_dir_with_types(path)
320    }
321
322    fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
323        self.overlay.write_file(path, content.into())
324    }
325
326    fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
327        self.overlay.create_file_exclusive(path, content.into())
328    }
329
330    fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
331        self.overlay.append_file(path, content.into())
332    }
333
334    fn create_dir(&mut self, path: &str) -> VfsResult<()> {
335        self.overlay.create_dir(path)
336    }
337
338    fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
339        self.overlay.mkdir(path, recursive)
340    }
341
342    fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
343        self.overlay.mknod(path, mode, rdev)
344    }
345
346    fn exists(&self, path: &str) -> bool {
347        self.overlay.exists(path)
348    }
349
350    fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
351        self.overlay.stat(path)
352    }
353
354    fn remove_file(&mut self, path: &str) -> VfsResult<()> {
355        self.overlay.remove_file(path)
356    }
357
358    fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
359        self.overlay.remove_dir(path)
360    }
361
362    fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
363        self.overlay.rename(old_path, new_path)
364    }
365
366    fn realpath(&self, path: &str) -> VfsResult<String> {
367        self.overlay.realpath(path)
368    }
369
370    fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
371        self.overlay.symlink(target, link_path)
372    }
373
374    fn read_link(&self, path: &str) -> VfsResult<String> {
375        self.overlay.read_link(path)
376    }
377
378    fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
379        self.overlay.lstat(path)
380    }
381
382    fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
383        self.overlay.link(old_path, new_path)
384    }
385
386    fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
387        self.overlay.chmod(path, mode)
388    }
389
390    fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
391        self.overlay.chown(path, uid, gid)
392    }
393
394    fn chown_spec(
395        &mut self,
396        path: &str,
397        uid: u32,
398        gid: u32,
399        follow_symlinks: bool,
400    ) -> VfsResult<()> {
401        self.overlay.chown_spec(path, uid, gid, follow_symlinks)
402    }
403
404    fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
405        self.overlay.lchown(path, uid, gid)
406    }
407
408    fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
409        self.overlay.get_xattr(path, name, follow_symlinks)
410    }
411
412    fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
413        self.overlay.list_xattrs(path, follow_symlinks)
414    }
415
416    fn set_xattr(
417        &mut self,
418        path: &str,
419        name: &str,
420        value: Vec<u8>,
421        flags: u32,
422        follow_symlinks: bool,
423    ) -> VfsResult<()> {
424        self.overlay
425            .set_xattr(path, name, value, flags, follow_symlinks)
426    }
427
428    fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
429        self.overlay.remove_xattr(path, name, follow_symlinks)
430    }
431
432    fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
433        self.overlay.utimes(path, atime_ms, mtime_ms)
434    }
435
436    fn utimes_spec(
437        &mut self,
438        path: &str,
439        atime: VirtualUtimeSpec,
440        mtime: VirtualUtimeSpec,
441        follow_symlinks: bool,
442    ) -> VfsResult<()> {
443        self.overlay
444            .utimes_spec(path, atime, mtime, follow_symlinks)
445    }
446
447    fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
448        self.overlay.truncate(path, length)
449    }
450
451    fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
452        self.overlay.allocate(path, offset, length)
453    }
454
455    fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
456        self.overlay.insert_range(path, offset, length)
457    }
458
459    fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
460        self.overlay.collapse_range(path, offset, length)
461    }
462
463    fn zero_range(
464        &mut self,
465        path: &str,
466        offset: u64,
467        length: u64,
468        keep_size: bool,
469    ) -> VfsResult<()> {
470        self.overlay.zero_range(path, offset, length, keep_size)
471    }
472
473    fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
474        self.overlay.punch_hole(path, offset, length)
475    }
476
477    fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
478        self.overlay.allocated_ranges(path)
479    }
480
481    fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
482        self.overlay.unwritten_ranges(path)
483    }
484
485    fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
486        self.overlay.pread(path, offset, length)
487    }
488
489    fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
490        self.overlay.pwrite(path, content, offset)
491    }
492}
493
494#[derive(Debug, Deserialize)]
495struct RawBaseFilesystemSnapshot {
496    filesystem: RawFilesystemEntries,
497}
498
499#[derive(Debug, Deserialize)]
500struct RawFilesystemEntries {
501    entries: Vec<RawFilesystemEntry>,
502}
503
504#[derive(Debug, Deserialize)]
505struct RawFilesystemEntry {
506    path: String,
507    #[serde(rename = "type")]
508    kind: RawFilesystemEntryKind,
509    mode: String,
510    uid: u32,
511    gid: u32,
512    #[serde(default)]
513    content: Option<String>,
514    #[serde(default)]
515    encoding: Option<String>,
516    #[serde(default)]
517    target: Option<String>,
518}
519
520#[derive(Debug, Deserialize)]
521#[serde(rename_all = "snake_case")]
522enum RawFilesystemEntryKind {
523    File,
524    Directory,
525    Symlink,
526}
527
528#[derive(Debug, Deserialize)]
529struct RawSnapshotExport {
530    format: String,
531    filesystem: RawFilesystemEntries,
532}
533
534#[derive(Debug, serde::Serialize)]
535struct SnapshotExport<'a> {
536    format: &'static str,
537    filesystem: SnapshotFilesystem<'a>,
538}
539
540#[derive(Debug, serde::Serialize)]
541struct SnapshotFilesystem<'a> {
542    entries: Vec<SerializedFilesystemEntry<'a>>,
543}
544
545#[derive(Debug, serde::Serialize)]
546struct SerializedFilesystemEntry<'a> {
547    path: &'a str,
548    #[serde(rename = "type")]
549    kind: &'static str,
550    mode: String,
551    uid: u32,
552    gid: u32,
553    #[serde(skip_serializing_if = "Option::is_none")]
554    content: Option<String>,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    encoding: Option<&'static str>,
557    #[serde(skip_serializing_if = "Option::is_none")]
558    target: Option<&'a str>,
559}
560
561pub fn encode_snapshot(snapshot: &RootFilesystemSnapshot) -> Result<Vec<u8>, RootFilesystemError> {
562    let serialized_entries = snapshot
563        .entries
564        .iter()
565        .map(|entry| SerializedFilesystemEntry {
566            path: &entry.path,
567            kind: match entry.kind {
568                FilesystemEntryKind::File => "file",
569                FilesystemEntryKind::Directory => "directory",
570                FilesystemEntryKind::Symlink => "symlink",
571            },
572            mode: format!("{:o}", entry.mode),
573            uid: entry.uid,
574            gid: entry.gid,
575            content: entry
576                .content
577                .as_ref()
578                .map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes)),
579            encoding: entry.content.as_ref().map(|_| "base64"),
580            target: entry.target.as_deref(),
581        })
582        .collect::<Vec<_>>();
583
584    serde_json::to_vec(&SnapshotExport {
585        format: ROOT_FILESYSTEM_SNAPSHOT_FORMAT,
586        filesystem: SnapshotFilesystem {
587            entries: serialized_entries,
588        },
589    })
590    .map_err(|error| RootFilesystemError::new(format!("serialize root snapshot: {error}")))
591}
592
593pub fn decode_snapshot(bytes: &[u8]) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
594    decode_snapshot_with_import_limits(bytes, &RootFilesystemImportLimits::default())
595}
596
597pub fn decode_snapshot_with_import_limits(
598    bytes: &[u8],
599    limits: &RootFilesystemImportLimits,
600) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
601    validate_encoded_snapshot_size(bytes, limits, "root snapshot")?;
602    let raw: RawSnapshotExport = serde_json::from_slice(bytes)
603        .map_err(|error| RootFilesystemError::new(format!("parse root snapshot: {error}")))?;
604    if !is_supported_root_filesystem_snapshot_format(&raw.format) {
605        return Err(RootFilesystemError::new(format!(
606            "unsupported root snapshot format: {}",
607            raw.format
608        )));
609    }
610    raw_entries_to_snapshot(raw.filesystem.entries, limits, "root snapshot")
611}
612
613pub fn is_supported_root_filesystem_snapshot_format(format: &str) -> bool {
614    format == ROOT_FILESYSTEM_SNAPSHOT_FORMAT
615        || format == LEGACY_AGENTOS_ROOT_FILESYSTEM_SNAPSHOT_FORMAT
616}
617
618pub fn load_bundled_base_snapshot_with_limits(
619    limits: &RootFilesystemImportLimits,
620) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
621    validate_encoded_snapshot_size(
622        BUNDLED_BASE_FILESYSTEM_JSON.as_bytes(),
623        limits,
624        "bundled base filesystem",
625    )?;
626    let raw: RawBaseFilesystemSnapshot = serde_json::from_str(BUNDLED_BASE_FILESYSTEM_JSON)
627        .map_err(|error| {
628            RootFilesystemError::new(format!("parse bundled base filesystem: {error}"))
629        })?;
630    raw_entries_to_snapshot(raw.filesystem.entries, limits, "bundled base filesystem")
631}
632
633fn minimal_root_snapshot() -> RootFilesystemSnapshot {
634    let mut entries = DEFAULT_ROOT_DIRECTORIES
635        .iter()
636        .map(|path| FilesystemEntry::directory(*path))
637        .collect::<Vec<_>>();
638    entries.push(FilesystemEntry::file("/usr/bin/env", Vec::new()));
639    RootFilesystemSnapshot { entries }
640}
641
642fn convert_raw_entry(raw: RawFilesystemEntry) -> Result<FilesystemEntry, RootFilesystemError> {
643    let content = match raw.content {
644        Some(content) => match raw.encoding.as_deref() {
645            Some("base64") => Some(
646                base64::engine::general_purpose::STANDARD
647                    .decode(content)
648                    .map_err(|error| {
649                        RootFilesystemError::new(format!(
650                            "decode base64 content for {}: {error}",
651                            raw.path
652                        ))
653                    })?,
654            ),
655            Some("utf8") | None => Some(content.into_bytes()),
656            Some(other) => {
657                return Err(RootFilesystemError::new(format!(
658                    "unsupported content encoding for {}: {other}",
659                    raw.path
660                )));
661            }
662        },
663        None => None,
664    };
665
666    Ok(FilesystemEntry {
667        path: raw.path,
668        kind: match raw.kind {
669            RawFilesystemEntryKind::File => FilesystemEntryKind::File,
670            RawFilesystemEntryKind::Directory => FilesystemEntryKind::Directory,
671            RawFilesystemEntryKind::Symlink => FilesystemEntryKind::Symlink,
672        },
673        mode: u32::from_str_radix(&raw.mode, 8).map_err(|error| {
674            RootFilesystemError::new(format!("parse mode {}: {error}", raw.mode))
675        })?,
676        uid: raw.uid,
677        gid: raw.gid,
678        content,
679        target: raw.target,
680    })
681}
682
683fn raw_entries_to_snapshot(
684    raw_entries: Vec<RawFilesystemEntry>,
685    limits: &RootFilesystemImportLimits,
686    context: &str,
687) -> Result<RootFilesystemSnapshot, RootFilesystemError> {
688    if let Some(limit) = limits.max_inode_count {
689        if raw_entries.len() > limit {
690            return Err(RootFilesystemError::new(format!(
691                "{context} contains {} entries, exceeding limit {limit}",
692                raw_entries.len()
693            )));
694        }
695    }
696
697    let entries = raw_entries
698        .into_iter()
699        .map(convert_raw_entry)
700        .collect::<Result<Vec<_>, _>>()?;
701    validate_entry_import_limits(&entries, limits, context)?;
702    Ok(RootFilesystemSnapshot { entries })
703}
704
705pub fn validate_snapshot_import_limits(
706    snapshot: &RootFilesystemSnapshot,
707    limits: &RootFilesystemImportLimits,
708    context: &str,
709) -> Result<(), RootFilesystemError> {
710    validate_entry_import_limits(&snapshot.entries, limits, context)
711}
712
713fn validate_descriptor_import_limits(
714    lowers: &[RootFilesystemSnapshot],
715    bootstrap_entries: &[FilesystemEntry],
716    limits: &RootFilesystemImportLimits,
717    context: &str,
718) -> Result<(), RootFilesystemError> {
719    let explicit_entry_count = lowers
720        .iter()
721        .map(|snapshot| snapshot.entries.len())
722        .sum::<usize>()
723        .saturating_add(bootstrap_entries.len());
724    let mut inode_paths = BTreeSet::new();
725    for snapshot in lowers {
726        collect_materialized_entry_paths(&snapshot.entries, &mut inode_paths);
727    }
728    collect_materialized_entry_paths(bootstrap_entries, &mut inode_paths);
729    let inode_count = inode_paths.len();
730    if let Some(limit) = limits.max_inode_count {
731        if explicit_entry_count > limit {
732            return Err(RootFilesystemError::new(format!(
733                "{context} contains {explicit_entry_count} entries, exceeding limit {limit}"
734            )));
735        }
736
737        if inode_count > limit {
738            return Err(RootFilesystemError::new(format!(
739                "{context} contains {inode_count} entries, exceeding limit {limit}"
740            )));
741        }
742    }
743
744    let mut bytes = 0_u64;
745    for snapshot in lowers {
746        bytes = bytes.saturating_add(entry_content_bytes(&snapshot.entries));
747    }
748    bytes = bytes.saturating_add(entry_content_bytes(bootstrap_entries));
749    if let Some(limit) = limits.max_filesystem_bytes {
750        if bytes > limit {
751            return Err(RootFilesystemError::new(format!(
752                "{context} contains {bytes} bytes, exceeding limit {limit}"
753            )));
754        }
755    }
756    Ok(())
757}
758
759fn validate_entry_import_limits(
760    entries: &[FilesystemEntry],
761    limits: &RootFilesystemImportLimits,
762    context: &str,
763) -> Result<(), RootFilesystemError> {
764    if let Some(limit) = limits.max_inode_count {
765        if entries.len() > limit {
766            return Err(RootFilesystemError::new(format!(
767                "{context} contains {} entries, exceeding limit {limit}",
768                entries.len()
769            )));
770        }
771
772        let inode_count = materialized_entry_inode_count(entries);
773        if inode_count > limit {
774            return Err(RootFilesystemError::new(format!(
775                "{context} contains {inode_count} entries, exceeding limit {limit}"
776            )));
777        }
778    }
779
780    let bytes = entry_content_bytes(entries);
781    if let Some(limit) = limits.max_filesystem_bytes {
782        if bytes > limit {
783            return Err(RootFilesystemError::new(format!(
784                "{context} contains {bytes} bytes, exceeding limit {limit}"
785            )));
786        }
787    }
788    Ok(())
789}
790
791fn validate_encoded_snapshot_size(
792    bytes: &[u8],
793    limits: &RootFilesystemImportLimits,
794    context: &str,
795) -> Result<(), RootFilesystemError> {
796    if let Some(limit) = limits.max_encoded_snapshot_bytes {
797        if bytes.len() > limit {
798            return Err(RootFilesystemError::new(format!(
799                "{context} contains {} encoded bytes, exceeding limit {limit}",
800                bytes.len()
801            )));
802        }
803    }
804    Ok(())
805}
806
807fn entry_content_bytes(entries: &[FilesystemEntry]) -> u64 {
808    entries.iter().fold(0_u64, |total, entry| {
809        total.saturating_add(match entry.kind {
810            FilesystemEntryKind::File => entry
811                .content
812                .as_ref()
813                .map(|content| usize_to_u64(content.len()))
814                .unwrap_or(0),
815            FilesystemEntryKind::Directory => 0,
816            FilesystemEntryKind::Symlink => entry
817                .target
818                .as_ref()
819                .map(|target| usize_to_u64(target.len()))
820                .unwrap_or(0),
821        })
822    })
823}
824
825fn materialized_entry_inode_count(entries: &[FilesystemEntry]) -> usize {
826    let mut paths = BTreeSet::new();
827    collect_materialized_entry_paths(entries, &mut paths);
828    paths.len()
829}
830
831fn collect_materialized_entry_paths(entries: &[FilesystemEntry], paths: &mut BTreeSet<String>) {
832    for entry in entries {
833        collect_materialized_path(&entry.path, paths);
834    }
835}
836
837fn collect_materialized_path(path: &str, paths: &mut BTreeSet<String>) {
838    let normalized = normalize_path(path);
839    paths.insert(normalized.clone());
840
841    let mut parent = String::new();
842    let segments = normalized
843        .split('/')
844        .filter(|segment| !segment.is_empty())
845        .collect::<Vec<_>>();
846    for segment in segments.iter().take(segments.len().saturating_sub(1)) {
847        parent.push('/');
848        parent.push_str(segment);
849        paths.insert(parent.clone());
850    }
851}
852
853fn usize_to_u64(value: usize) -> u64 {
854    u64::try_from(value).unwrap_or(u64::MAX)
855}
856
857const fn u64_limit_to_usize(value: u64) -> usize {
858    if value > usize::MAX as u64 {
859        usize::MAX
860    } else {
861        value as usize
862    }
863}
864
865const fn encoded_snapshot_limit(
866    max_filesystem_bytes: Option<u64>,
867    max_inode_count: Option<usize>,
868) -> Option<usize> {
869    let Some(max_filesystem_bytes) = max_filesystem_bytes else {
870        return None;
871    };
872
873    Some(
874        u64_limit_to_usize(max_filesystem_bytes)
875            .saturating_mul(2)
876            .saturating_add(match max_inode_count {
877                Some(max_inode_count) => {
878                    max_inode_count.saturating_mul(ROOT_FILESYSTEM_SNAPSHOT_ENTRY_OVERHEAD_BYTES)
879                }
880                None => 0,
881            })
882            .saturating_add(ROOT_FILESYSTEM_SNAPSHOT_FIXED_OVERHEAD_BYTES),
883    )
884}
885
886fn snapshot_to_memory_filesystem(
887    snapshot: &RootFilesystemSnapshot,
888) -> Result<MemoryFileSystem, RootFilesystemError> {
889    let mut filesystem = MemoryFileSystem::new();
890    for entry in sort_entries(snapshot.entries.clone()) {
891        apply_entry_to_memory_filesystem(&mut filesystem, &entry)?;
892    }
893    Ok(filesystem)
894}
895
896fn apply_entry_to_memory_filesystem(
897    filesystem: &mut MemoryFileSystem,
898    entry: &FilesystemEntry,
899) -> Result<(), RootFilesystemError> {
900    ensure_parent_directories(filesystem, &entry.path)?;
901
902    match entry.kind {
903        FilesystemEntryKind::Directory => {
904            filesystem.mkdir(&entry.path, true)?;
905            filesystem.chmod(&entry.path, entry.mode)?;
906            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
907        }
908        FilesystemEntryKind::File => {
909            filesystem.write_file(&entry.path, entry.content.clone().unwrap_or_default())?;
910            filesystem.chmod(&entry.path, entry.mode)?;
911            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
912        }
913        FilesystemEntryKind::Symlink => {
914            let Some(target) = entry.target.as_deref() else {
915                return Err(RootFilesystemError::new(format!(
916                    "missing symlink target for {}",
917                    entry.path
918                )));
919            };
920            filesystem.symlink_with_metadata(
921                target,
922                &entry.path,
923                entry.mode,
924                entry.uid,
925                entry.gid,
926            )?;
927        }
928    }
929
930    Ok(())
931}
932
933fn apply_entry(
934    filesystem: &mut impl VirtualFileSystem,
935    entry: &FilesystemEntry,
936) -> Result<(), RootFilesystemError> {
937    ensure_parent_directories(filesystem, &entry.path)?;
938
939    match entry.kind {
940        FilesystemEntryKind::Directory => {
941            filesystem.mkdir(&entry.path, true)?;
942            filesystem.chmod(&entry.path, entry.mode)?;
943            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
944        }
945        FilesystemEntryKind::File => {
946            filesystem.write_file(&entry.path, entry.content.clone().unwrap_or_default())?;
947            filesystem.chmod(&entry.path, entry.mode)?;
948            filesystem.chown(&entry.path, entry.uid, entry.gid)?;
949        }
950        FilesystemEntryKind::Symlink => {
951            let Some(target) = entry.target.as_deref() else {
952                return Err(RootFilesystemError::new(format!(
953                    "missing symlink target for {}",
954                    entry.path
955                )));
956            };
957            filesystem.symlink(target, &entry.path)?;
958        }
959    }
960
961    Ok(())
962}
963
964fn ensure_parent_directories(
965    filesystem: &mut impl VirtualFileSystem,
966    path: &str,
967) -> Result<(), RootFilesystemError> {
968    let normalized = normalize_path(path);
969    let mut current = String::new();
970    let segments = normalized
971        .split('/')
972        .filter(|segment| !segment.is_empty())
973        .collect::<Vec<_>>();
974
975    for segment in segments.iter().take(segments.len().saturating_sub(1)) {
976        current.push('/');
977        current.push_str(segment);
978
979        if filesystem.exists(&current) {
980            continue;
981        }
982
983        filesystem.create_dir(&current)?;
984        filesystem.chmod(&current, 0o755)?;
985        filesystem.chown(&current, 0, 0)?;
986    }
987
988    Ok(())
989}
990
991fn sort_entries(mut entries: Vec<FilesystemEntry>) -> Vec<FilesystemEntry> {
992    entries.sort_by(|left, right| {
993        let depth_left = if left.path == "/" {
994            0
995        } else {
996            left.path.split('/').filter(|part| !part.is_empty()).count()
997        };
998        let depth_right = if right.path == "/" {
999            0
1000        } else {
1001            right
1002                .path
1003                .split('/')
1004                .filter(|part| !part.is_empty())
1005                .count()
1006        };
1007        depth_left
1008            .cmp(&depth_right)
1009            .then_with(|| left.path.cmp(&right.path))
1010    });
1011    entries
1012}
1013
1014fn snapshot_virtual_filesystem(
1015    filesystem: &mut impl VirtualFileSystem,
1016    root_path: &str,
1017) -> Result<Vec<FilesystemEntry>, RootFilesystemError> {
1018    let mut entries = Vec::new();
1019    snapshot_path(filesystem, root_path, &mut entries)?;
1020    Ok(entries)
1021}
1022
1023fn snapshot_path(
1024    filesystem: &mut impl VirtualFileSystem,
1025    path: &str,
1026    entries: &mut Vec<FilesystemEntry>,
1027) -> Result<(), RootFilesystemError> {
1028    let stat = if path == "/" {
1029        filesystem.stat(path)?
1030    } else {
1031        filesystem.lstat(path)?
1032    };
1033
1034    if stat.is_symbolic_link {
1035        entries.push(FilesystemEntry {
1036            path: path.to_owned(),
1037            kind: FilesystemEntryKind::Symlink,
1038            mode: stat.mode,
1039            uid: stat.uid,
1040            gid: stat.gid,
1041            content: None,
1042            target: Some(filesystem.read_link(path)?),
1043        });
1044        return Ok(());
1045    }
1046
1047    if stat.is_directory {
1048        entries.push(FilesystemEntry {
1049            path: path.to_owned(),
1050            kind: FilesystemEntryKind::Directory,
1051            mode: stat.mode,
1052            uid: stat.uid,
1053            gid: stat.gid,
1054            content: None,
1055            target: None,
1056        });
1057
1058        let mut children = filesystem
1059            .read_dir_with_types(path)?
1060            .into_iter()
1061            .map(|entry| entry.name)
1062            .filter(|name| name != "." && name != "..")
1063            .collect::<Vec<_>>();
1064        children.sort();
1065
1066        for child in children {
1067            let child_path = if path == "/" {
1068                format!("/{child}")
1069            } else {
1070                format!("{path}/{child}")
1071            };
1072            snapshot_path(filesystem, &child_path, entries)?;
1073        }
1074        return Ok(());
1075    }
1076
1077    entries.push(FilesystemEntry {
1078        path: path.to_owned(),
1079        kind: FilesystemEntryKind::File,
1080        mode: stat.mode,
1081        uid: stat.uid,
1082        gid: stat.gid,
1083        content: Some(filesystem.read_file(path)?),
1084        target: None,
1085    });
1086    Ok(())
1087}
1088
1089fn is_kernel_reserved_bootstrap_path(path: &str) -> bool {
1090    let normalized = normalize_path(path);
1091    KERNEL_RESERVED_BOOTSTRAP_PATH_PREFIXES
1092        .iter()
1093        .any(|prefix| normalized == *prefix || normalized.starts_with(&format!("{prefix}/")))
1094}