1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::error::Error;
4use std::fmt;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8pub const S_IFREG: u32 = 0o100000;
9pub const S_IFDIR: u32 = 0o040000;
10pub const S_IFLNK: u32 = 0o120000;
11
12static NEXT_MEMORY_FILESYSTEM_DEVICE_ID: AtomicU64 = AtomicU64::new(256);
18
19fn allocate_memory_filesystem_device_id() -> u64 {
20 NEXT_MEMORY_FILESYSTEM_DEVICE_ID.fetch_add(1, Ordering::Relaxed)
21}
22
23const DEFAULT_UID: u32 = 1000;
24const DEFAULT_GID: u32 = 1000;
25const DIRECTORY_SIZE: u64 = 4096;
26pub const MAX_PATH_LENGTH: usize = 4096;
27const MAX_SYMLINK_DEPTH: usize = 40;
28
29pub type VfsResult<T> = Result<T, VfsError>;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct VfsError {
33 code: &'static str,
34 message: String,
35}
36
37impl VfsError {
38 pub fn new(code: &'static str, message: impl Into<String>) -> Self {
39 Self {
40 code,
41 message: message.into(),
42 }
43 }
44
45 pub fn io(message: impl Into<String>) -> Self {
46 Self::new("EIO", message)
47 }
48
49 pub fn unsupported(message: impl Into<String>) -> Self {
50 Self::new("ENOSYS", message)
51 }
52
53 pub fn code(&self) -> &'static str {
54 self.code
55 }
56
57 pub fn message(&self) -> &str {
58 &self.message
59 }
60
61 fn not_found(op: &'static str, path: &str) -> Self {
62 Self::new(
63 "ENOENT",
64 format!("no such file or directory, {op} '{path}'"),
65 )
66 }
67
68 fn already_exists(op: &'static str, path: &str) -> Self {
69 Self::new("EEXIST", format!("file already exists, {op} '{path}'"))
70 }
71
72 fn is_directory(op: &'static str, path: &str) -> Self {
73 Self::new(
74 "EISDIR",
75 format!("illegal operation on a directory, {op} '{path}'"),
76 )
77 }
78
79 fn not_directory(op: &'static str, path: &str) -> Self {
80 Self::new("ENOTDIR", format!("not a directory, {op} '{path}'"))
81 }
82
83 fn path_too_long(path: &str) -> Self {
84 Self::new("ENAMETOOLONG", format!("file name too long: {path}"))
85 }
86
87 fn not_empty(path: &str) -> Self {
88 Self::new("ENOTEMPTY", format!("directory not empty, rmdir '{path}'"))
89 }
90
91 pub fn permission_denied(op: &'static str, path: &str) -> Self {
92 Self::new("EPERM", format!("operation not permitted, {op} '{path}'"))
93 }
94
95 pub fn access_denied(op: &'static str, path: &str, reason: Option<&str>) -> Self {
96 let message = match reason {
97 Some(reason) => format!("permission denied, {op} '{path}': {reason}"),
98 None => format!("permission denied, {op} '{path}'"),
99 };
100
101 Self::new("EACCES", message)
102 }
103
104 fn symlink_loop(path: &str) -> Self {
105 Self::new(
106 "ELOOP",
107 format!("too many levels of symbolic links, '{path}'"),
108 )
109 }
110
111 fn invalid_input(message: impl Into<String>) -> Self {
112 Self::new("EINVAL", message)
113 }
114
115 fn invalid_utf8(path: &str) -> Self {
116 Self::new("EINVAL", format!("file contains invalid UTF-8, '{path}'"))
117 }
118}
119
120impl fmt::Display for VfsError {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(f, "{}: {}", self.code, self.message)
123 }
124}
125
126impl Error for VfsError {}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum FileType {
130 File,
131 Directory,
132 SymbolicLink,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct VirtualDirEntry {
137 pub name: String,
138 pub is_directory: bool,
139 pub is_symbolic_link: bool,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct VirtualStat {
144 pub mode: u32,
145 pub size: u64,
146 pub blocks: u64,
147 pub dev: u64,
148 pub rdev: u64,
149 pub is_directory: bool,
150 pub is_symbolic_link: bool,
151 pub atime_ms: u64,
152 pub atime_nsec: u32,
153 pub mtime_ms: u64,
154 pub mtime_nsec: u32,
155 pub ctime_ms: u64,
156 pub ctime_nsec: u32,
157 pub birthtime_ms: u64,
158 pub ino: u64,
159 pub nlink: u64,
160 pub uid: u32,
161 pub gid: u32,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
165pub struct VirtualTimeSpec {
166 pub sec: i64,
167 pub nsec: u32,
168}
169
170impl VirtualTimeSpec {
171 pub fn new(sec: i64, nsec: u32) -> VfsResult<Self> {
172 if nsec >= 1_000_000_000 {
173 return Err(VfsError::new(
174 "EINVAL",
175 format!("timespec nanoseconds out of range: {nsec}"),
176 ));
177 }
178 Ok(Self { sec, nsec })
179 }
180
181 pub fn from_millis(ms: u64) -> Self {
182 Self {
183 sec: (ms / 1_000) as i64,
184 nsec: ((ms % 1_000) * 1_000_000) as u32,
185 }
186 }
187
188 pub fn to_truncated_millis(self) -> VfsResult<u64> {
189 if self.sec < 0 {
190 return Err(VfsError::new(
191 "EINVAL",
192 format!(
193 "negative timestamps are not supported by this filesystem: {}",
194 self.sec
195 ),
196 ));
197 }
198 let seconds = u64::try_from(self.sec).map_err(|_| {
199 VfsError::new("EINVAL", format!("timestamp is out of range: {}", self.sec))
200 })?;
201 Ok(seconds.saturating_mul(1_000) + (self.nsec as u64 / 1_000_000))
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum VirtualUtimeSpec {
207 Set(VirtualTimeSpec),
208 Now,
209 Omit,
210}
211
212pub trait VirtualFileSystem {
213 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>>;
214 fn read_text_file(&mut self, path: &str) -> VfsResult<String> {
215 String::from_utf8(self.read_file(path)?).map_err(|_| VfsError::invalid_utf8(path))
216 }
217 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>>;
218 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
219 let entries = self.read_dir(path)?;
220 if entries.len() > max_entries {
221 return Err(VfsError::new(
222 "ENOMEM",
223 format!(
224 "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
225 ),
226 ));
227 }
228 Ok(entries)
229 }
230 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>>;
231 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()>;
236 fn write_file_with_mode(
237 &mut self,
238 path: &str,
239 content: impl Into<Vec<u8>>,
240 mode: Option<u32>,
241 ) -> VfsResult<()> {
242 let _ = mode;
243 self.write_file(path, content)
244 }
245 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
246 let content = content.into();
247 if self.exists(path) {
248 return Err(VfsError::already_exists("open", path));
249 }
250 self.write_file(path, content)
251 }
252 fn create_file_exclusive_with_mode(
253 &mut self,
254 path: &str,
255 content: impl Into<Vec<u8>>,
256 mode: Option<u32>,
257 ) -> VfsResult<()> {
258 let _ = mode;
259 self.create_file_exclusive(path, content)
260 }
261 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
264 let content = content.into();
265 let mut existing = self.read_file(path)?;
266 reserve_file_growth(&mut existing, content.len())?;
267 existing.extend_from_slice(&content);
268 let new_len = existing.len() as u64;
269 self.write_file(path, existing)?;
270 Ok(new_len)
271 }
272 fn create_dir(&mut self, path: &str) -> VfsResult<()>;
273 fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
274 let _ = mode;
275 self.create_dir(path)
276 }
277 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()>;
278 fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
279 let _ = mode;
280 self.mkdir(path, recursive)
281 }
282 fn exists(&self, path: &str) -> bool;
283 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
284 fn remove_file(&mut self, path: &str) -> VfsResult<()>;
285 fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
286 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
287 fn realpath(&self, path: &str) -> VfsResult<String>;
288 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
289 fn read_link(&self, path: &str) -> VfsResult<String>;
290 fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
291 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
292 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
293 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
294 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
295 fn utimes_spec(
296 &mut self,
297 path: &str,
298 atime: VirtualUtimeSpec,
299 mtime: VirtualUtimeSpec,
300 follow_symlinks: bool,
301 ) -> VfsResult<()> {
302 if !follow_symlinks {
303 return Err(VfsError::unsupported(format!(
304 "lutimes is not supported for '{path}'"
305 )));
306 }
307 let existing = match (atime, mtime) {
308 (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
309 _ => None,
310 };
311 let now = now_ms();
312 let atime_ms = resolve_utime_millis(
313 atime,
314 now,
315 existing.as_ref().map(|stat| VirtualTimeSpec {
316 sec: (stat.atime_ms / 1_000) as i64,
317 nsec: stat.atime_nsec,
318 }),
319 )?;
320 let mtime_ms = resolve_utime_millis(
321 mtime,
322 now,
323 existing.as_ref().map(|stat| VirtualTimeSpec {
324 sec: (stat.mtime_ms / 1_000) as i64,
325 nsec: stat.mtime_nsec,
326 }),
327 )?;
328 self.utimes(path, atime_ms, mtime_ms)
329 }
330 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
332 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
333 fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
336 let content = content.into();
337 let mut existing = self.read_file(path)?;
338 let start = checked_file_len(offset, "pwrite offset")?;
339 if start > existing.len() {
340 resize_file_data(&mut existing, start)?;
341 }
342 let end = start.checked_add(content.len()).ok_or_else(|| {
343 VfsError::new(
344 "ENOMEM",
345 format!(
346 "pwrite result length overflows addressable memory: offset {offset}, content length {}",
347 content.len()
348 ),
349 )
350 })?;
351 if end > existing.len() {
352 resize_file_data(&mut existing, end)?;
353 }
354 existing[start..end].copy_from_slice(&content);
355 self.write_file(path, existing)
356 }
357}
358
359#[derive(Debug, Clone)]
360struct Metadata {
361 mode: u32,
362 uid: u32,
363 gid: u32,
364 nlink: u64,
365 ino: u64,
366 atime_ms: u64,
367 atime_nsec: u32,
368 mtime_ms: u64,
369 mtime_nsec: u32,
370 ctime_ms: u64,
371 ctime_nsec: u32,
372 birthtime_ms: u64,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376pub struct MemoryFileSystemSnapshotMetadata {
377 pub mode: u32,
378 pub uid: u32,
379 pub gid: u32,
380 pub nlink: u64,
381 pub ino: u64,
382 pub atime_ms: u64,
383 #[serde(default)]
384 pub atime_nsec: u32,
385 pub mtime_ms: u64,
386 #[serde(default)]
387 pub mtime_nsec: u32,
388 pub ctime_ms: u64,
389 #[serde(default)]
390 pub ctime_nsec: u32,
391 pub birthtime_ms: u64,
392}
393
394#[derive(Debug, Clone)]
395enum InodeKind {
396 File { data: Vec<u8> },
397 Directory,
398 SymbolicLink { target: String },
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub enum MemoryFileSystemSnapshotInodeKind {
403 File { data: Vec<u8> },
404 Directory,
405 SymbolicLink { target: String },
406}
407
408#[derive(Debug, Clone)]
409struct Inode {
410 metadata: Metadata,
411 kind: InodeKind,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct MemoryFileSystemSnapshotInode {
416 pub metadata: MemoryFileSystemSnapshotMetadata,
417 pub kind: MemoryFileSystemSnapshotInodeKind,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct MemoryFileSystemSnapshot {
422 pub path_index: BTreeMap<String, u64>,
423 pub inodes: BTreeMap<u64, MemoryFileSystemSnapshotInode>,
424 pub next_ino: u64,
425}
426
427#[derive(Debug)]
428pub struct MemoryFileSystem {
429 device_id: u64,
430 path_index: BTreeMap<String, u64>,
431 inodes: BTreeMap<u64, Inode>,
432 next_ino: u64,
433}
434
435impl MemoryFileSystem {
436 pub fn new() -> Self {
437 let mut filesystem = Self {
438 device_id: allocate_memory_filesystem_device_id(),
439 path_index: BTreeMap::new(),
440 inodes: BTreeMap::new(),
441 next_ino: 1,
442 };
443
444 let root_ino = filesystem.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
445 filesystem.path_index.insert(String::from("/"), root_ino);
446 filesystem
447 }
448
449 pub fn read_dir_filtered_limited<F>(
450 &mut self,
451 path: &str,
452 max_entries: usize,
453 mut include: F,
454 ) -> VfsResult<Vec<String>>
455 where
456 F: FnMut(&str) -> bool,
457 {
458 self.assert_directory_path(path, "scandir")?;
459 let resolved = self.resolve_path(path, 0)?;
460 self.inode_mut_for_existing_path(&resolved, "scandir", false)?
461 .metadata
462 .atime_ms = now_ms();
463 let prefix = if resolved == "/" {
464 String::from("/")
465 } else {
466 format!("{resolved}/")
467 };
468
469 let mut entries = BTreeMap::<String, String>::new();
470 for (candidate_path, _) in self.path_index.range(prefix.clone()..) {
471 if !candidate_path.starts_with(&prefix) {
472 break;
473 }
474
475 let rest = &candidate_path[prefix.len()..];
476 if rest.is_empty() || rest.contains('/') || !include(rest) {
477 continue;
478 }
479
480 entries.insert(String::from(rest), String::from(rest));
481 if entries.len() > max_entries {
482 return Err(VfsError::new(
483 "ENOMEM",
484 format!(
485 "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
486 ),
487 ));
488 }
489 }
490
491 Ok(entries.into_values().collect())
492 }
493
494 pub fn link_count_in_subtree(&self, ino: u64, path: &str) -> usize {
495 let normalized = normalize_path(path);
496 let prefix = if normalized == "/" {
497 String::from("/")
498 } else {
499 format!("{normalized}/")
500 };
501
502 self.path_index
503 .iter()
504 .filter(|(candidate_path, candidate_ino)| {
505 **candidate_ino == ino
506 && (candidate_path.as_str() == normalized
507 || candidate_path.starts_with(&prefix))
508 })
509 .count()
510 }
511
512 fn allocate_inode(&mut self, kind: InodeKind, mode: u32) -> u64 {
513 let ino = self.next_ino;
514 self.next_ino += 1;
515 let now = now_ms();
516 let nlink = if matches!(kind, InodeKind::Directory) {
517 2
518 } else {
519 1
520 };
521 self.inodes.insert(
522 ino,
523 Inode {
524 metadata: Metadata {
525 mode,
526 uid: DEFAULT_UID,
527 gid: DEFAULT_GID,
528 nlink,
529 ino,
530 atime_ms: now,
531 atime_nsec: 0,
532 mtime_ms: now,
533 mtime_nsec: 0,
534 ctime_ms: now,
535 ctime_nsec: 0,
536 birthtime_ms: now,
537 },
538 kind,
539 },
540 );
541 ino
542 }
543
544 pub fn symlink_with_metadata(
545 &mut self,
546 target: &str,
547 link_path: &str,
548 mode: u32,
549 uid: u32,
550 gid: u32,
551 ) -> VfsResult<()> {
552 let normalized = self.resolve_exact_path(link_path)?;
553 if self.path_index.contains_key(&normalized) {
554 return Err(VfsError::already_exists("symlink", link_path));
555 }
556
557 self.assert_directory_path(&dirname(&normalized), "symlink")?;
558 let ino = self.allocate_inode(
559 InodeKind::SymbolicLink {
560 target: String::from(target),
561 },
562 if mode & 0o170000 == 0 {
563 S_IFLNK | (mode & 0o7777)
564 } else {
565 mode
566 },
567 );
568 let inode = self
569 .inodes
570 .get_mut(&ino)
571 .expect("allocated inode should exist");
572 inode.metadata.uid = uid;
573 inode.metadata.gid = gid;
574 self.path_index.insert(normalized, ino);
575 Ok(())
576 }
577
578 fn resolve_path_with_options(
579 &self,
580 path: &str,
581 follow_final_symlink: bool,
582 depth: usize,
583 ) -> VfsResult<String> {
584 validate_path(path)?;
585 if depth > MAX_SYMLINK_DEPTH {
586 return Err(VfsError::symlink_loop(path));
587 }
588
589 let normalized = normalize_path(path);
590 if normalized == "/" {
591 return Ok(normalized);
592 }
593
594 let components: Vec<&str> = normalized
595 .split('/')
596 .filter(|part| !part.is_empty())
597 .collect();
598 let mut current = String::from("/");
599
600 for (index, component) in components.iter().enumerate() {
601 let candidate = if current == "/" {
602 format!("/{}", component)
603 } else {
604 format!("{current}/{}", component)
605 };
606 let is_final = index + 1 == components.len();
607 let should_follow = !is_final || follow_final_symlink;
608
609 if let Some(ino) = self.path_index.get(&candidate) {
610 let inode = self
611 .inodes
612 .get(ino)
613 .expect("path index should always point at a valid inode");
614
615 if should_follow {
616 if let InodeKind::SymbolicLink { target } = &inode.kind {
617 let target_path = if target.starts_with('/') {
618 target.clone()
619 } else {
620 normalize_path(&format!("{}/{}", dirname(&candidate), target))
621 };
622 let remainder = components[index + 1..].join("/");
623 let next_path = if remainder.is_empty() {
624 target_path
625 } else {
626 normalize_path(&format!("{target_path}/{remainder}"))
627 };
628 return self.resolve_path_with_options(
629 &next_path,
630 follow_final_symlink,
631 depth + 1,
632 );
633 }
634 }
635
636 if !is_final && !matches!(inode.kind, InodeKind::Directory) {
637 return Err(VfsError::not_directory("stat", &candidate));
638 }
639 }
640
641 current = candidate;
642 }
643
644 Ok(current)
645 }
646
647 fn resolve_path(&self, path: &str, depth: usize) -> VfsResult<String> {
648 self.resolve_path_with_options(path, true, depth)
649 }
650
651 fn resolve_exact_path(&self, path: &str) -> VfsResult<String> {
652 self.resolve_path_with_options(path, false, 0)
653 }
654
655 fn inode_id_for_existing_path(
656 &self,
657 path: &str,
658 op: &'static str,
659 follow_symlinks: bool,
660 ) -> VfsResult<u64> {
661 let normalized = normalize_path(path);
662 let resolved = if follow_symlinks {
663 self.resolve_path(&normalized, 0)?
664 } else {
665 self.resolve_exact_path(&normalized)?
666 };
667 self.path_index
668 .get(&resolved)
669 .copied()
670 .ok_or_else(|| VfsError::not_found(op, path))
671 }
672
673 fn inode_for_existing_path(
674 &self,
675 path: &str,
676 op: &'static str,
677 follow_symlinks: bool,
678 ) -> VfsResult<&Inode> {
679 let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
680 Ok(self
681 .inodes
682 .get(&ino)
683 .expect("existing path should resolve to a live inode"))
684 }
685
686 fn inode_mut_for_existing_path(
687 &mut self,
688 path: &str,
689 op: &'static str,
690 follow_symlinks: bool,
691 ) -> VfsResult<&mut Inode> {
692 let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
693 Ok(self
694 .inodes
695 .get_mut(&ino)
696 .expect("existing path should resolve to a live inode"))
697 }
698
699 fn assert_directory_path(&self, path: &str, op: &'static str) -> VfsResult<()> {
700 let inode = self.inode_for_existing_path(path, op, true)?;
701 if matches!(inode.kind, InodeKind::Directory) {
702 Ok(())
703 } else {
704 Err(VfsError::not_directory(op, path))
705 }
706 }
707
708 fn remove_exact_path(&mut self, path: &str) -> VfsResult<()> {
709 let normalized = self.resolve_exact_path(path)?;
710 let ino = self
711 .path_index
712 .get(&normalized)
713 .copied()
714 .ok_or_else(|| VfsError::not_found("unlink", path))?;
715 let inode = self
716 .inodes
717 .get(&ino)
718 .expect("existing path should resolve to a live inode");
719
720 if matches!(inode.kind, InodeKind::Directory) {
721 return Err(VfsError::is_directory("unlink", path));
722 }
723
724 self.inodes
725 .get_mut(&ino)
726 .expect("inode should exist when unlinking")
727 .metadata
728 .ctime_ms = now_ms();
729 self.path_index.remove(&normalized);
730 self.decrement_link_count(ino);
731 Ok(())
732 }
733
734 fn remove_existing_destination(&mut self, path: &str) -> VfsResult<()> {
735 let normalized = self.resolve_exact_path(path)?;
736 let Some(ino) = self.path_index.get(&normalized).copied() else {
737 return Ok(());
738 };
739
740 let inode = self
741 .inodes
742 .get(&ino)
743 .expect("existing path should resolve to a live inode");
744
745 if matches!(inode.kind, InodeKind::Directory) {
746 let prefix = format!("{normalized}/");
747 if self
748 .path_index
749 .keys()
750 .any(|candidate| candidate.starts_with(&prefix))
751 {
752 return Err(VfsError::not_empty(path));
753 }
754 }
755
756 self.inodes
757 .get_mut(&ino)
758 .expect("inode should exist when removing destination")
759 .metadata
760 .ctime_ms = now_ms();
761 self.path_index.remove(&normalized);
762 self.decrement_link_count(ino);
763 Ok(())
764 }
765
766 fn decrement_link_count(&mut self, ino: u64) {
767 let should_remove = {
768 let inode = self
769 .inodes
770 .get_mut(&ino)
771 .expect("inode should exist when decrementing link count");
772 inode.metadata.nlink = inode.metadata.nlink.saturating_sub(1);
773 inode.metadata.nlink == 0
774 };
775
776 if should_remove {
777 self.inodes.remove(&ino);
778 }
779 }
780
781 fn build_stat(&self, inode: &Inode) -> VirtualStat {
782 let size = match &inode.kind {
783 InodeKind::File { data } => data.len() as u64,
784 InodeKind::Directory => DIRECTORY_SIZE,
785 InodeKind::SymbolicLink { target } => target.len() as u64,
786 };
787
788 VirtualStat {
789 mode: inode.metadata.mode,
790 size,
791 blocks: block_count_for_size(size),
792 dev: self.device_id,
793 rdev: 0,
794 is_directory: matches!(inode.kind, InodeKind::Directory),
795 is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
796 atime_ms: inode.metadata.atime_ms,
797 atime_nsec: inode.metadata.atime_nsec,
798 mtime_ms: inode.metadata.mtime_ms,
799 mtime_nsec: inode.metadata.mtime_nsec,
800 ctime_ms: inode.metadata.ctime_ms,
801 ctime_nsec: inode.metadata.ctime_nsec,
802 birthtime_ms: inode.metadata.birthtime_ms,
803 ino: inode.metadata.ino,
804 nlink: inode.metadata.nlink,
805 uid: inode.metadata.uid,
806 gid: inode.metadata.gid,
807 }
808 }
809
810 pub fn snapshot(&self) -> MemoryFileSystemSnapshot {
815 MemoryFileSystemSnapshot {
816 path_index: self.path_index.clone(),
817 inodes: self
818 .inodes
819 .iter()
820 .map(|(ino, inode)| {
821 (
822 *ino,
823 MemoryFileSystemSnapshotInode {
824 metadata: MemoryFileSystemSnapshotMetadata {
825 mode: inode.metadata.mode,
826 uid: inode.metadata.uid,
827 gid: inode.metadata.gid,
828 nlink: inode.metadata.nlink,
829 ino: inode.metadata.ino,
830 atime_ms: inode.metadata.atime_ms,
831 atime_nsec: inode.metadata.atime_nsec,
832 mtime_ms: inode.metadata.mtime_ms,
833 mtime_nsec: inode.metadata.mtime_nsec,
834 ctime_ms: inode.metadata.ctime_ms,
835 ctime_nsec: inode.metadata.ctime_nsec,
836 birthtime_ms: inode.metadata.birthtime_ms,
837 },
838 kind: match &inode.kind {
839 InodeKind::File { data } => {
840 MemoryFileSystemSnapshotInodeKind::File { data: data.clone() }
841 }
842 InodeKind::Directory => {
843 MemoryFileSystemSnapshotInodeKind::Directory
844 }
845 InodeKind::SymbolicLink { target } => {
846 MemoryFileSystemSnapshotInodeKind::SymbolicLink {
847 target: target.clone(),
848 }
849 }
850 },
851 },
852 )
853 })
854 .collect(),
855 next_ino: self.next_ino,
856 }
857 }
858
859 pub fn from_snapshot(snapshot: MemoryFileSystemSnapshot) -> Self {
860 Self {
861 device_id: allocate_memory_filesystem_device_id(),
862 path_index: snapshot.path_index,
863 inodes: snapshot
864 .inodes
865 .into_iter()
866 .map(|(ino, inode)| {
867 (
868 ino,
869 Inode {
870 metadata: Metadata {
871 mode: inode.metadata.mode,
872 uid: inode.metadata.uid,
873 gid: inode.metadata.gid,
874 nlink: inode.metadata.nlink,
875 ino: inode.metadata.ino,
876 atime_ms: inode.metadata.atime_ms,
877 atime_nsec: inode.metadata.atime_nsec,
878 mtime_ms: inode.metadata.mtime_ms,
879 mtime_nsec: inode.metadata.mtime_nsec,
880 ctime_ms: inode.metadata.ctime_ms,
881 ctime_nsec: inode.metadata.ctime_nsec,
882 birthtime_ms: inode.metadata.birthtime_ms,
883 },
884 kind: match inode.kind {
885 MemoryFileSystemSnapshotInodeKind::File { data } => {
886 InodeKind::File { data }
887 }
888 MemoryFileSystemSnapshotInodeKind::Directory => {
889 InodeKind::Directory
890 }
891 MemoryFileSystemSnapshotInodeKind::SymbolicLink { target } => {
892 InodeKind::SymbolicLink { target }
893 }
894 },
895 },
896 )
897 })
898 .collect(),
899 next_ino: snapshot.next_ino,
900 }
901 }
902}
903
904impl VirtualFileSystem for MemoryFileSystem {
905 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
906 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
907 match &inode.kind {
908 InodeKind::File { data } => {
909 inode.metadata.atime_ms = now_ms();
910 Ok(data.clone())
911 }
912 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
913 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
914 }
915 }
916
917 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
918 Ok(self
919 .read_dir_with_types(path)?
920 .into_iter()
921 .map(|entry| entry.name)
922 .collect())
923 }
924
925 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
926 self.read_dir_filtered_limited(path, max_entries, |_| true)
927 }
928
929 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
930 self.assert_directory_path(path, "scandir")?;
931 let resolved = self.resolve_path(path, 0)?;
932 self.inode_mut_for_existing_path(&resolved, "scandir", false)?
933 .metadata
934 .atime_ms = now_ms();
935 let prefix = if resolved == "/" {
936 String::from("/")
937 } else {
938 format!("{resolved}/")
939 };
940
941 let mut entries = BTreeMap::<String, VirtualDirEntry>::new();
942 for (candidate_path, ino) in self.path_index.range(prefix.clone()..) {
943 if !candidate_path.starts_with(&prefix) {
944 break;
945 }
946
947 let rest = &candidate_path[prefix.len()..];
948 if rest.is_empty() || rest.contains('/') {
949 continue;
950 }
951
952 let inode = self
953 .inodes
954 .get(ino)
955 .expect("path index should always point at a valid inode");
956 entries.insert(
957 String::from(rest),
958 VirtualDirEntry {
959 name: String::from(rest),
960 is_directory: matches!(inode.kind, InodeKind::Directory),
961 is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
962 },
963 );
964 }
965
966 Ok(entries.into_values().collect())
967 }
968
969 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
970 let normalized = self.resolve_path(path, 0)?;
971 self.mkdir(&dirname(&normalized), true)?;
972 let data = content.into();
973
974 if self.path_index.contains_key(&normalized) {
975 let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
976 let now = now_ms();
977 match &mut inode.kind {
978 InodeKind::File { data: existing } => {
979 *existing = data;
980 inode.metadata.mtime_ms = now;
981 inode.metadata.ctime_ms = now;
982 return Ok(());
983 }
984 InodeKind::Directory => return Err(VfsError::is_directory("open", path)),
985 InodeKind::SymbolicLink { .. } => return Err(VfsError::not_found("open", path)),
986 }
987 }
988
989 let ino = self.allocate_inode(InodeKind::File { data }, S_IFREG | 0o644);
990 self.path_index.insert(normalized, ino);
991 Ok(())
992 }
993
994 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
995 let normalized = self.resolve_path(path, 0)?;
996 self.mkdir(&dirname(&normalized), true)?;
997 if self.path_index.contains_key(&normalized) {
998 return Err(VfsError::already_exists("open", path));
999 }
1000
1001 let ino = self.allocate_inode(
1002 InodeKind::File {
1003 data: content.into(),
1004 },
1005 S_IFREG | 0o644,
1006 );
1007 self.path_index.insert(normalized, ino);
1008 Ok(())
1009 }
1010
1011 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1012 let normalized = self.resolve_path(path, 0)?;
1013 let data = content.into();
1014 let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
1015 let now = now_ms();
1016 match &mut inode.kind {
1017 InodeKind::File { data: existing } => {
1018 reserve_file_growth(existing, data.len())?;
1019 existing.extend_from_slice(&data);
1020 inode.metadata.mtime_ms = now;
1021 inode.metadata.ctime_ms = now;
1022 Ok(existing.len() as u64)
1023 }
1024 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1025 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1026 }
1027 }
1028
1029 fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1030 let normalized = self.resolve_exact_path(path)?;
1031 if normalized == "/" {
1032 return Ok(());
1033 }
1034
1035 self.assert_directory_path(&dirname(&normalized), "mkdir")?;
1036 if let Some(existing) = self.path_index.get(&normalized) {
1037 let inode = self
1038 .inodes
1039 .get(existing)
1040 .expect("path index should always point at a valid inode");
1041 if matches!(inode.kind, InodeKind::Directory) {
1042 return Ok(());
1043 }
1044 return Err(VfsError::already_exists("mkdir", path));
1045 }
1046
1047 let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1048 self.path_index.insert(normalized, ino);
1049 Ok(())
1050 }
1051
1052 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
1053 let normalized = normalize_path(path);
1054 if normalized == "/" {
1055 return Ok(());
1056 }
1057
1058 if !recursive {
1059 return self.create_dir(path);
1060 }
1061
1062 let parts: Vec<&str> = normalized
1063 .split('/')
1064 .filter(|part| !part.is_empty())
1065 .collect();
1066 let mut current = String::from("/");
1067
1068 for (index, part) in parts.iter().enumerate() {
1069 let raw_path = if current == "/" {
1070 format!("/{}", part)
1071 } else {
1072 format!("{current}/{}", part)
1073 };
1074 let resolved =
1075 self.resolve_path_with_options(&raw_path, index + 1 != parts.len(), 0)?;
1076
1077 match self.path_index.get(&resolved).copied() {
1078 Some(ino) => {
1079 let inode = self
1080 .inodes
1081 .get(&ino)
1082 .expect("path index should always point at a valid inode");
1083 if !matches!(inode.kind, InodeKind::Directory) {
1084 return Err(VfsError::not_directory("mkdir", &raw_path));
1085 }
1086 }
1087 None => {
1088 let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1089 self.path_index.insert(resolved.clone(), ino);
1090 }
1091 }
1092
1093 current = resolved;
1094 }
1095
1096 Ok(())
1097 }
1098
1099 fn exists(&self, path: &str) -> bool {
1100 self.resolve_path(path, 0)
1101 .ok()
1102 .is_some_and(|resolved| self.path_index.contains_key(&resolved))
1103 }
1104
1105 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1106 let inode = self.inode_for_existing_path(path, "stat", true)?;
1107 Ok(self.build_stat(inode))
1108 }
1109
1110 fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1111 self.remove_exact_path(path)
1112 }
1113
1114 fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1115 let normalized = self.resolve_exact_path(path)?;
1116 if normalized == "/" {
1117 return Err(VfsError::permission_denied("rmdir", path));
1118 }
1119
1120 let ino = self
1121 .path_index
1122 .get(&normalized)
1123 .copied()
1124 .ok_or_else(|| VfsError::not_found("rmdir", path))?;
1125 let inode = self
1126 .inodes
1127 .get(&ino)
1128 .expect("path index should always point at a valid inode");
1129 if !matches!(inode.kind, InodeKind::Directory) {
1130 return Err(VfsError::not_directory("rmdir", path));
1131 }
1132
1133 let prefix = format!("{normalized}/");
1134 if self
1135 .path_index
1136 .keys()
1137 .any(|candidate| candidate.starts_with(&prefix))
1138 {
1139 return Err(VfsError::not_empty(path));
1140 }
1141
1142 self.path_index.remove(&normalized);
1143 self.decrement_link_count(ino);
1144 Ok(())
1145 }
1146
1147 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1148 let old_normalized = self.resolve_exact_path(old_path)?;
1149 let new_normalized = self.resolve_exact_path(new_path)?;
1150
1151 if old_normalized == "/" {
1152 return Err(VfsError::permission_denied("rename", old_path));
1153 }
1154
1155 if old_normalized == new_normalized {
1156 return Ok(());
1157 }
1158
1159 self.assert_directory_path(&dirname(&new_normalized), "rename")?;
1160
1161 if new_normalized.starts_with(&(old_normalized.clone() + "/")) {
1162 return Err(VfsError::invalid_input(format!(
1163 "cannot move '{}' into its own descendant '{}'",
1164 old_path, new_path
1165 )));
1166 }
1167
1168 let ino = self
1169 .path_index
1170 .get(&old_normalized)
1171 .copied()
1172 .ok_or_else(|| VfsError::not_found("rename", old_path))?;
1173 let is_directory = matches!(
1174 self.inodes
1175 .get(&ino)
1176 .expect("path index should always point at a valid inode")
1177 .kind,
1178 InodeKind::Directory
1179 );
1180
1181 self.remove_existing_destination(new_path)?;
1182
1183 if !is_directory {
1184 self.path_index.remove(&old_normalized);
1185 self.path_index.insert(new_normalized, ino);
1186 self.inodes
1187 .get_mut(&ino)
1188 .expect("renamed inode should exist")
1189 .metadata
1190 .ctime_ms = now_ms();
1191 return Ok(());
1192 }
1193
1194 let prefix = format!("{old_normalized}/");
1195 let to_move: Vec<(String, u64)> = self
1196 .path_index
1197 .iter()
1198 .filter(|(path, _)| **path == old_normalized || path.starts_with(&prefix))
1199 .map(|(path, inode_id)| (path.clone(), *inode_id))
1200 .collect();
1201
1202 for (path, _) in &to_move {
1203 self.path_index.remove(path);
1204 }
1205
1206 for (path, inode_id) in to_move {
1207 let relocated_path = if path == old_normalized {
1208 new_normalized.clone()
1209 } else {
1210 format!("{new_normalized}{}", &path[old_normalized.len()..])
1211 };
1212 self.path_index.insert(relocated_path, inode_id);
1213 }
1214
1215 self.inodes
1216 .get_mut(&ino)
1217 .expect("renamed directory inode should exist")
1218 .metadata
1219 .ctime_ms = now_ms();
1220
1221 Ok(())
1222 }
1223
1224 fn realpath(&self, path: &str) -> VfsResult<String> {
1225 let resolved = self.resolve_path(path, 0)?;
1226 if !self.path_index.contains_key(&resolved) {
1227 return Err(VfsError::not_found("realpath", path));
1228 }
1229 Ok(resolved)
1230 }
1231
1232 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1233 self.symlink_with_metadata(target, link_path, S_IFLNK | 0o777, DEFAULT_UID, DEFAULT_GID)
1234 }
1235
1236 fn read_link(&self, path: &str) -> VfsResult<String> {
1237 let inode = self.inode_for_existing_path(path, "readlink", false)?;
1238 match &inode.kind {
1239 InodeKind::SymbolicLink { target } => Ok(target.clone()),
1240 _ => Err(VfsError::invalid_input(format!(
1241 "invalid argument, readlink '{path}'"
1242 ))),
1243 }
1244 }
1245
1246 fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1247 let inode = self.inode_for_existing_path(path, "lstat", false)?;
1248 Ok(self.build_stat(inode))
1249 }
1250
1251 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1252 let ino = self.inode_id_for_existing_path(old_path, "link", true)?;
1253 let inode = self
1254 .inodes
1255 .get(&ino)
1256 .expect("path index should always point at a valid inode");
1257 if !matches!(inode.kind, InodeKind::File { .. }) {
1258 return Err(VfsError::permission_denied("link", old_path));
1259 }
1260
1261 let normalized = self.resolve_exact_path(new_path)?;
1262 if self.path_index.contains_key(&normalized) {
1263 return Err(VfsError::already_exists("link", new_path));
1264 }
1265
1266 self.assert_directory_path(&dirname(&normalized), "link")?;
1267 self.path_index.insert(normalized, ino);
1268 let inode = self
1269 .inodes
1270 .get_mut(&ino)
1271 .expect("path index should always point at a valid inode");
1272 inode.metadata.nlink += 1;
1273 inode.metadata.ctime_ms = now_ms();
1274 Ok(())
1275 }
1276
1277 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1278 let inode = self.inode_mut_for_existing_path(path, "chmod", true)?;
1279 let type_bits = if mode & 0o170000 == 0 {
1280 inode.metadata.mode & 0o170000
1281 } else {
1282 mode & 0o170000
1283 };
1284 inode.metadata.mode = type_bits | (mode & 0o7777);
1285 inode.metadata.ctime_ms = now_ms();
1286 Ok(())
1287 }
1288
1289 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1290 let inode = self.inode_mut_for_existing_path(path, "chown", true)?;
1291 inode.metadata.uid = uid;
1292 inode.metadata.gid = gid;
1293 inode.metadata.ctime_ms = now_ms();
1294 Ok(())
1295 }
1296
1297 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1298 let inode = self.inode_mut_for_existing_path(path, "utimes", true)?;
1299 inode.metadata.atime_ms = atime_ms;
1300 inode.metadata.atime_nsec = 0;
1301 inode.metadata.mtime_ms = mtime_ms;
1302 inode.metadata.mtime_nsec = 0;
1303 inode.metadata.ctime_ms = now_ms();
1304 inode.metadata.ctime_nsec = 0;
1305 Ok(())
1306 }
1307
1308 fn utimes_spec(
1309 &mut self,
1310 path: &str,
1311 atime: VirtualUtimeSpec,
1312 mtime: VirtualUtimeSpec,
1313 follow_symlinks: bool,
1314 ) -> VfsResult<()> {
1315 let stat = if follow_symlinks {
1316 self.stat(path)?
1317 } else {
1318 self.lstat(path)?
1319 };
1320 let inode = self.inode_mut_for_existing_path(path, "utimes", follow_symlinks)?;
1321 let now = now_time_spec();
1322 let atime = resolve_utime_spec(
1323 atime,
1324 now,
1325 VirtualTimeSpec {
1326 sec: (stat.atime_ms / 1_000) as i64,
1327 nsec: stat.atime_nsec,
1328 },
1329 )?;
1330 let mtime = resolve_utime_spec(
1331 mtime,
1332 now,
1333 VirtualTimeSpec {
1334 sec: (stat.mtime_ms / 1_000) as i64,
1335 nsec: stat.mtime_nsec,
1336 },
1337 )?;
1338 inode.metadata.atime_ms = atime.to_truncated_millis()?;
1339 inode.metadata.atime_nsec = atime.nsec;
1340 inode.metadata.mtime_ms = mtime.to_truncated_millis()?;
1341 inode.metadata.mtime_nsec = mtime.nsec;
1342 let ctime = now_time_spec();
1343 inode.metadata.ctime_ms = ctime.to_truncated_millis()?;
1344 inode.metadata.ctime_nsec = ctime.nsec;
1345 Ok(())
1346 }
1347
1348 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1349 let inode = self.inode_mut_for_existing_path(path, "truncate", true)?;
1350 let now = now_ms();
1351 match &mut inode.kind {
1352 InodeKind::File { data } => {
1353 resize_file_data(data, checked_file_len(length, "truncate length")?)?;
1354 inode.metadata.mtime_ms = now;
1355 inode.metadata.ctime_ms = now;
1356 Ok(())
1357 }
1358 InodeKind::Directory => Err(VfsError::is_directory("truncate", path)),
1359 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("truncate", path)),
1360 }
1361 }
1362
1363 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
1364 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
1365 match &mut inode.kind {
1366 InodeKind::File { data } => {
1367 inode.metadata.atime_ms = now_ms();
1368 let start = offset as usize;
1369 if start >= data.len() {
1370 return Ok(Vec::new());
1371 }
1372 let end = start.saturating_add(length).min(data.len());
1373 Ok(data[start..end].to_vec())
1374 }
1375 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1376 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1377 }
1378 }
1379}
1380
1381impl Default for MemoryFileSystem {
1382 fn default() -> Self {
1383 Self::new()
1384 }
1385}
1386
1387fn resolve_utime_spec(
1388 spec: VirtualUtimeSpec,
1389 now: VirtualTimeSpec,
1390 existing: VirtualTimeSpec,
1391) -> VfsResult<VirtualTimeSpec> {
1392 match spec {
1393 VirtualUtimeSpec::Set(spec) => Ok(spec),
1394 VirtualUtimeSpec::Now => Ok(now),
1395 VirtualUtimeSpec::Omit => Ok(existing),
1396 }
1397}
1398
1399fn resolve_utime_millis(
1400 spec: VirtualUtimeSpec,
1401 now_ms: u64,
1402 existing: Option<VirtualTimeSpec>,
1403) -> VfsResult<u64> {
1404 match spec {
1405 VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis(),
1406 VirtualUtimeSpec::Now => Ok(now_ms),
1407 VirtualUtimeSpec::Omit => existing
1408 .ok_or_else(|| VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata"))?
1409 .to_truncated_millis(),
1410 }
1411}
1412
1413pub fn validate_path(path: &str) -> VfsResult<()> {
1414 if path.as_bytes().contains(&0) {
1415 return Err(VfsError::invalid_input("path contains NUL byte"));
1416 }
1417 if let Some(control) = path
1418 .bytes()
1419 .find(|byte| byte.is_ascii_control() && *byte != b'\0')
1420 {
1421 return Err(VfsError::invalid_input(format!(
1422 "path contains control character byte 0x{control:02x}"
1423 )));
1424 }
1425 let normalized = normalize_path(path);
1426 if normalized.len() > MAX_PATH_LENGTH {
1427 return Err(VfsError::path_too_long(path));
1428 }
1429 Ok(())
1430}
1431
1432pub fn normalize_path(path: &str) -> String {
1433 if path.is_empty() {
1434 return String::from("/");
1435 }
1436
1437 let candidate = if path.starts_with('/') {
1438 path.to_owned()
1439 } else {
1440 format!("/{path}")
1441 };
1442
1443 let mut resolved = Vec::new();
1444 for part in candidate.split('/') {
1445 match part {
1446 "" | "." => {}
1447 ".." => {
1448 resolved.pop();
1449 }
1450 component => resolved.push(component),
1451 }
1452 }
1453
1454 if resolved.is_empty() {
1455 String::from("/")
1456 } else {
1457 format!("/{}", resolved.join("/"))
1458 }
1459}
1460
1461fn block_count_for_size(size: u64) -> u64 {
1462 if size == 0 {
1463 0
1464 } else {
1465 size.div_ceil(512)
1466 }
1467}
1468
1469fn checked_file_len(value: u64, description: &'static str) -> VfsResult<usize> {
1470 usize::try_from(value).map_err(|_| {
1471 VfsError::new(
1472 "EINVAL",
1473 format!("{description} exceeds addressable memory: {value}"),
1474 )
1475 })
1476}
1477
1478fn reserve_file_growth(data: &mut Vec<u8>, additional: usize) -> VfsResult<()> {
1479 data.try_reserve(additional).map_err(|error| {
1480 VfsError::new(
1481 "ENOMEM",
1482 format!(
1483 "file growth exceeds addressable memory: current length {}, additional {additional}: {error}",
1484 data.len()
1485 ),
1486 )
1487 })
1488}
1489
1490fn resize_file_data(data: &mut Vec<u8>, new_len: usize) -> VfsResult<()> {
1491 if new_len > data.len() {
1492 reserve_file_growth(data, new_len - data.len())?;
1493 }
1494 data.resize(new_len, 0);
1495 Ok(())
1496}
1497
1498fn dirname(path: &str) -> String {
1499 let normalized = normalize_path(path);
1500 let Some((head, _)) = normalized.rsplit_once('/') else {
1501 return String::from("/");
1502 };
1503
1504 if head.is_empty() {
1505 String::from("/")
1506 } else {
1507 String::from(head)
1508 }
1509}
1510
1511fn now_ms() -> u64 {
1512 SystemTime::now()
1513 .duration_since(UNIX_EPOCH)
1514 .unwrap_or_default()
1515 .as_millis() as u64
1516}
1517
1518fn now_time_spec() -> VirtualTimeSpec {
1519 let now = SystemTime::now()
1520 .duration_since(UNIX_EPOCH)
1521 .unwrap_or_default();
1522 VirtualTimeSpec {
1523 sec: now.as_secs() as i64,
1524 nsec: now.subsec_nanos(),
1525 }
1526}