1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::error::Error;
4use std::fmt;
5use std::sync::atomic::{AtomicU64, Ordering};
6use web_time::{SystemTime, UNIX_EPOCH};
7
8pub const S_IFREG: u32 = 0o100000;
9pub const S_IFDIR: u32 = 0o040000;
10pub const S_IFLNK: u32 = 0o120000;
11pub const S_IFCHR: u32 = 0o020000;
12pub const S_IFBLK: u32 = 0o060000;
13pub const S_IFIFO: u32 = 0o010000;
14pub const RENAME_NOREPLACE: u32 = 1;
15pub const RENAME_EXCHANGE: u32 = 2;
16pub const RENAME_WHITEOUT: u32 = 4;
17
18static NEXT_MEMORY_FILESYSTEM_DEVICE_ID: AtomicU64 = AtomicU64::new(256);
24static NEXT_RENAME_EXCHANGE_ID: AtomicU64 = AtomicU64::new(1);
25
26fn allocate_memory_filesystem_device_id() -> u64 {
27 NEXT_MEMORY_FILESYSTEM_DEVICE_ID.fetch_add(1, Ordering::Relaxed)
28}
29
30const DEFAULT_UID: u32 = 1000;
31const DEFAULT_GID: u32 = 1000;
32const DIRECTORY_SIZE: u64 = 4096;
33pub const MAX_PATH_LENGTH: usize = 4096;
34const MAX_SYMLINK_DEPTH: usize = 40;
35pub const XATTR_CREATE: u32 = 1;
36pub const XATTR_REPLACE: u32 = 2;
37pub const XATTR_NAME_MAX: usize = 255;
38pub const XATTR_SIZE_MAX: usize = 64 * 1024;
39pub const XATTR_LIST_MAX: usize = 64 * 1024;
40
41pub type VfsResult<T> = Result<T, VfsError>;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct VfsError {
45 code: &'static str,
46 message: String,
47}
48
49impl VfsError {
50 pub fn new(code: &'static str, message: impl Into<String>) -> Self {
51 Self {
52 code,
53 message: message.into(),
54 }
55 }
56
57 pub fn io(message: impl Into<String>) -> Self {
58 Self::new("EIO", message)
59 }
60
61 pub fn unsupported(message: impl Into<String>) -> Self {
62 Self::new("ENOSYS", message)
63 }
64
65 pub fn code(&self) -> &'static str {
66 self.code
67 }
68
69 pub fn message(&self) -> &str {
70 &self.message
71 }
72
73 fn not_found(op: &'static str, path: &str) -> Self {
74 Self::new(
75 "ENOENT",
76 format!("no such file or directory, {op} '{path}'"),
77 )
78 }
79
80 fn already_exists(op: &'static str, path: &str) -> Self {
81 Self::new("EEXIST", format!("file already exists, {op} '{path}'"))
82 }
83
84 fn is_directory(op: &'static str, path: &str) -> Self {
85 Self::new(
86 "EISDIR",
87 format!("illegal operation on a directory, {op} '{path}'"),
88 )
89 }
90
91 fn not_directory(op: &'static str, path: &str) -> Self {
92 Self::new("ENOTDIR", format!("not a directory, {op} '{path}'"))
93 }
94
95 fn path_too_long(path: &str) -> Self {
96 Self::new("ENAMETOOLONG", format!("file name too long: {path}"))
97 }
98
99 fn not_empty(path: &str) -> Self {
100 Self::new("ENOTEMPTY", format!("directory not empty, rmdir '{path}'"))
101 }
102
103 pub fn permission_denied(op: &'static str, path: &str) -> Self {
104 Self::new("EPERM", format!("operation not permitted, {op} '{path}'"))
105 }
106
107 pub fn access_denied(op: &'static str, path: &str, reason: Option<&str>) -> Self {
108 let message = match reason {
109 Some(reason) => format!("permission denied, {op} '{path}': {reason}"),
110 None => format!("permission denied, {op} '{path}'"),
111 };
112
113 Self::new("EACCES", message)
114 }
115
116 fn symlink_loop(path: &str) -> Self {
117 Self::new(
118 "ELOOP",
119 format!("too many levels of symbolic links, '{path}'"),
120 )
121 }
122
123 fn invalid_input(message: impl Into<String>) -> Self {
124 Self::new("EINVAL", message)
125 }
126
127 fn invalid_utf8(path: &str) -> Self {
128 Self::new("EINVAL", format!("file contains invalid UTF-8, '{path}'"))
129 }
130}
131
132impl fmt::Display for VfsError {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}: {}", self.code, self.message)
135 }
136}
137
138impl Error for VfsError {}
139
140pub fn validate_xattr_name(name: &str) -> VfsResult<()> {
141 if name.is_empty() || name.len() > XATTR_NAME_MAX || !name.contains('.') || name.contains('\0')
142 {
143 return Err(VfsError::new(
144 "ERANGE",
145 format!("invalid extended attribute name: {name:?}"),
146 ));
147 }
148 Ok(())
149}
150
151pub fn set_xattr_value(
152 xattrs: &mut BTreeMap<String, Vec<u8>>,
153 name: &str,
154 value: &[u8],
155 flags: u32,
156) -> VfsResult<()> {
157 validate_xattr_name(name)?;
158 if flags & !(XATTR_CREATE | XATTR_REPLACE) != 0 || flags == (XATTR_CREATE | XATTR_REPLACE) {
159 return Err(VfsError::new(
160 "EINVAL",
161 format!("invalid xattr flags: {flags}"),
162 ));
163 }
164 if value.len() > XATTR_SIZE_MAX {
165 return Err(VfsError::new(
166 "E2BIG",
167 format!(
168 "extended attribute value is {} bytes; Linux-compatible limit is {XATTR_SIZE_MAX} bytes",
169 value.len()
170 ),
171 ));
172 }
173 let exists = xattrs.contains_key(name);
174 if flags == XATTR_CREATE && exists {
175 return Err(VfsError::new(
176 "EEXIST",
177 format!("extended attribute already exists: {name}"),
178 ));
179 }
180 if flags == XATTR_REPLACE && !exists {
181 return Err(VfsError::new(
182 "ENODATA",
183 format!("extended attribute does not exist: {name}"),
184 ));
185 }
186 let old_len = xattrs.get(name).map_or(0, Vec::len);
187 let list_bytes = xattrs.keys().map(|key| key.len() + 1).sum::<usize>();
188 let value_bytes = xattrs.values().map(Vec::len).sum::<usize>();
189 let new_total = list_bytes
190 .saturating_add(value_bytes)
191 .saturating_sub(old_len)
192 .saturating_add(if exists { 0 } else { name.len() + 1 })
193 .saturating_add(value.len());
194 if new_total > XATTR_LIST_MAX {
195 return Err(VfsError::new(
196 "ENOSPC",
197 format!(
198 "inode extended attributes require {new_total} bytes; Linux-compatible limit is {XATTR_LIST_MAX} bytes"
199 ),
200 ));
201 }
202 xattrs.insert(name.to_string(), value.to_vec());
203 Ok(())
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum FileType {
208 File,
209 Directory,
210 SymbolicLink,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct VirtualDirEntry {
215 pub name: String,
216 pub is_directory: bool,
217 pub is_symbolic_link: bool,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct VirtualStat {
222 pub mode: u32,
223 pub size: u64,
224 pub blocks: u64,
225 pub dev: u64,
226 pub rdev: u64,
227 pub is_directory: bool,
228 pub is_symbolic_link: bool,
229 pub atime_ms: u64,
230 pub atime_nsec: u32,
231 pub mtime_ms: u64,
232 pub mtime_nsec: u32,
233 pub ctime_ms: u64,
234 pub ctime_nsec: u32,
235 pub birthtime_ms: u64,
236 pub ino: u64,
237 pub nlink: u64,
238 pub uid: u32,
239 pub gid: u32,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub struct VirtualTimeSpec {
244 pub sec: i64,
245 pub nsec: u32,
246}
247
248impl VirtualTimeSpec {
249 pub fn new(sec: i64, nsec: u32) -> VfsResult<Self> {
250 if nsec >= 1_000_000_000 {
251 return Err(VfsError::new(
252 "EINVAL",
253 format!("timespec nanoseconds out of range: {nsec}"),
254 ));
255 }
256 Ok(Self { sec, nsec })
257 }
258
259 pub fn from_millis(ms: u64) -> Self {
260 Self {
261 sec: (ms / 1_000) as i64,
262 nsec: ((ms % 1_000) * 1_000_000) as u32,
263 }
264 }
265
266 pub fn to_truncated_millis(self) -> VfsResult<u64> {
267 if self.sec < 0 {
268 return Err(VfsError::new(
269 "EINVAL",
270 format!(
271 "negative timestamps are not supported by this filesystem: {}",
272 self.sec
273 ),
274 ));
275 }
276 let seconds = u64::try_from(self.sec).map_err(|_| {
277 VfsError::new("EINVAL", format!("timestamp is out of range: {}", self.sec))
278 })?;
279 Ok(seconds.saturating_mul(1_000) + (self.nsec as u64 / 1_000_000))
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum VirtualUtimeSpec {
285 Set(VirtualTimeSpec),
286 Now,
287 Omit,
288}
289
290pub trait VirtualFileSystem {
291 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>>;
292 fn read_text_file(&mut self, path: &str) -> VfsResult<String> {
293 String::from_utf8(self.read_file(path)?).map_err(|_| VfsError::invalid_utf8(path))
294 }
295 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>>;
296 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
297 let entries = self.read_dir(path)?;
298 if entries.len() > max_entries {
299 return Err(VfsError::new(
300 "ENOMEM",
301 format!(
302 "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
303 ),
304 ));
305 }
306 Ok(entries)
307 }
308 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>>;
309 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()>;
314 fn write_file_with_mode(
315 &mut self,
316 path: &str,
317 content: impl Into<Vec<u8>>,
318 mode: Option<u32>,
319 ) -> VfsResult<()> {
320 let _ = mode;
321 self.write_file(path, content)
322 }
323 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
324 let content = content.into();
325 if self.exists(path) {
326 return Err(VfsError::already_exists("open", path));
327 }
328 self.write_file(path, content)
329 }
330 fn create_file_exclusive_with_mode(
331 &mut self,
332 path: &str,
333 content: impl Into<Vec<u8>>,
334 mode: Option<u32>,
335 ) -> VfsResult<()> {
336 let _ = mode;
337 self.create_file_exclusive(path, content)
338 }
339 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
342 let content = content.into();
343 let mut existing = self.read_file(path)?;
344 reserve_file_growth(&mut existing, content.len())?;
345 existing.extend_from_slice(&content);
346 let new_len = existing.len() as u64;
347 self.write_file(path, existing)?;
348 Ok(new_len)
349 }
350 fn create_dir(&mut self, path: &str) -> VfsResult<()>;
351 fn create_dir_with_mode(&mut self, path: &str, mode: Option<u32>) -> VfsResult<()> {
352 let _ = mode;
353 self.create_dir(path)
354 }
355 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()>;
356 fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
357 let _ = (mode, rdev);
358 Err(VfsError::new(
359 "EOPNOTSUPP",
360 format!("special inode creation is not supported for {path}"),
361 ))
362 }
363 fn mkdir_with_mode(&mut self, path: &str, recursive: bool, mode: Option<u32>) -> VfsResult<()> {
364 let _ = mode;
365 self.mkdir(path, recursive)
366 }
367 fn exists(&self, path: &str) -> bool;
368 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat>;
369 fn remove_file(&mut self, path: &str) -> VfsResult<()>;
370 fn remove_dir(&mut self, path: &str) -> VfsResult<()>;
371 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
372 fn rename_at2(&mut self, old_path: &str, new_path: &str, flags: u32) -> VfsResult<()> {
373 match flags {
374 0 => self.rename(old_path, new_path),
375 RENAME_NOREPLACE => {
376 self.lstat(old_path)?;
377 match self.lstat(new_path) {
378 Ok(_) => Err(VfsError::new(
379 "EEXIST",
380 format!("file exists, rename '{old_path}' -> '{new_path}'"),
381 )),
382 Err(error) if error.code() == "ENOENT" => self.rename(old_path, new_path),
383 Err(error) => Err(error),
384 }
385 }
386 RENAME_EXCHANGE => {
387 self.lstat(old_path)?;
388 self.lstat(new_path)?;
389 if normalize_path(old_path) == normalize_path(new_path) {
390 return Ok(());
391 }
392
393 let parent = dirname(old_path);
394 let temporary = (0..128)
395 .find_map(|_| {
396 let id = NEXT_RENAME_EXCHANGE_ID.fetch_add(1, Ordering::Relaxed);
397 let candidate = if parent == "/" {
398 format!("/.agentos-rename-exchange-{id}")
399 } else {
400 format!("{parent}/.agentos-rename-exchange-{id}")
401 };
402 if self
403 .lstat(&candidate)
404 .is_err_and(|error| error.code() == "ENOENT")
405 {
406 Some(candidate)
407 } else {
408 None
409 }
410 })
411 .ok_or_else(|| {
412 VfsError::new(
413 "EEXIST",
414 "could not allocate a bounded temporary rename-exchange path",
415 )
416 })?;
417
418 self.rename(old_path, &temporary)?;
419 if let Err(error) = self.rename(new_path, old_path) {
420 return match self.rename(&temporary, old_path) {
421 Ok(()) => Err(error),
422 Err(rollback) => Err(VfsError::new(
423 "EIO",
424 format!("rename exchange failed: {error}; rollback failed: {rollback}"),
425 )),
426 };
427 }
428 if let Err(error) = self.rename(&temporary, new_path) {
429 let rollback_destination = self.rename(old_path, new_path);
430 let rollback_source = self.rename(&temporary, old_path);
431 return match (rollback_destination, rollback_source) {
432 (Ok(()), Ok(())) => Err(error),
433 (destination, source) => Err(VfsError::new(
434 "EIO",
435 format!(
436 "rename exchange failed: {error}; rollback destination: {destination:?}; rollback source: {source:?}"
437 ),
438 )),
439 };
440 }
441 Ok(())
442 }
443 _ => Err(VfsError::new(
444 "EINVAL",
445 format!("invalid renameat2 flags: {flags:#x}"),
446 )),
447 }
448 }
449 fn realpath(&self, path: &str) -> VfsResult<String>;
450 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()>;
451 fn read_link(&self, path: &str) -> VfsResult<String>;
452 fn lstat(&self, path: &str) -> VfsResult<VirtualStat>;
453 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>;
454 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>;
455 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>;
456 fn chown_spec(
457 &mut self,
458 path: &str,
459 uid: u32,
460 gid: u32,
461 follow_symlinks: bool,
462 ) -> VfsResult<()> {
463 if !follow_symlinks {
464 return Err(VfsError::unsupported(format!(
465 "lchown is not supported for '{path}'"
466 )));
467 }
468 self.chown(path, uid, gid)
469 }
470
471 fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
472 self.chown(path, uid, gid)
473 }
474 fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
475 let _ = (name, follow_symlinks);
476 Err(VfsError::new(
477 "EOPNOTSUPP",
478 format!("extended attributes are not supported for {path}"),
479 ))
480 }
481 fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
482 let _ = follow_symlinks;
483 Err(VfsError::new(
484 "EOPNOTSUPP",
485 format!("extended attributes are not supported for {path}"),
486 ))
487 }
488 fn set_xattr(
489 &mut self,
490 path: &str,
491 name: &str,
492 value: Vec<u8>,
493 flags: u32,
494 follow_symlinks: bool,
495 ) -> VfsResult<()> {
496 let _ = (name, value, flags, follow_symlinks);
497 Err(VfsError::new(
498 "EOPNOTSUPP",
499 format!("extended attributes are not supported for {path}"),
500 ))
501 }
502 fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
503 let _ = (name, follow_symlinks);
504 Err(VfsError::new(
505 "EOPNOTSUPP",
506 format!("extended attributes are not supported for {path}"),
507 ))
508 }
509 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>;
510 fn utimes_spec(
511 &mut self,
512 path: &str,
513 atime: VirtualUtimeSpec,
514 mtime: VirtualUtimeSpec,
515 follow_symlinks: bool,
516 ) -> VfsResult<()> {
517 if !follow_symlinks {
518 return Err(VfsError::unsupported(format!(
519 "lutimes is not supported for '{path}'"
520 )));
521 }
522 let existing = match (atime, mtime) {
523 (VirtualUtimeSpec::Omit, _) | (_, VirtualUtimeSpec::Omit) => Some(self.stat(path)?),
524 _ => None,
525 };
526 let now = now_ms();
527 let atime_ms = resolve_utime_millis(
528 atime,
529 now,
530 existing.as_ref().map(|stat| VirtualTimeSpec {
531 sec: (stat.atime_ms / 1_000) as i64,
532 nsec: stat.atime_nsec,
533 }),
534 )?;
535 let mtime_ms = resolve_utime_millis(
536 mtime,
537 now,
538 existing.as_ref().map(|stat| VirtualTimeSpec {
539 sec: (stat.mtime_ms / 1_000) as i64,
540 nsec: stat.mtime_nsec,
541 }),
542 )?;
543 self.utimes(path, atime_ms, mtime_ms)
544 }
545 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()>;
547 fn sync(&mut self, _path: &str) -> VfsResult<()> {
548 Ok(())
549 }
550 fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
553 const ALLOCATION_CHUNK_BYTES: u64 = 64 * 1024;
554
555 let end = offset
556 .checked_add(length)
557 .ok_or_else(|| VfsError::new("EINVAL", "allocation range overflows"))?;
558 if length == 0 {
559 return Ok(());
560 }
561 let stat = self.stat(path)?;
562 if end > stat.size {
563 self.truncate(path, end)?;
564 }
565 let mut cursor = offset;
566 while cursor < end {
567 let chunk_len = (end - cursor).min(ALLOCATION_CHUNK_BYTES) as usize;
568 let mut bytes = self.pread(path, cursor, chunk_len)?;
569 bytes.resize(chunk_len, 0);
570 self.pwrite(path, bytes, cursor)?;
571 cursor += chunk_len as u64;
572 }
573 Ok(())
574 }
575 fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
576 validate_shift_range(offset, length)?;
577 let size = self.stat(path)?.size;
578 if offset >= size {
579 return Err(VfsError::new(
580 "EINVAL",
581 "insert range offset must be before EOF",
582 ));
583 }
584 let tail_len = usize::try_from(size - offset)
585 .map_err(|_| VfsError::new("EINVAL", "insert range tail is too large"))?;
586 let tail = self.pread(path, offset, tail_len)?;
587 self.truncate(
588 path,
589 size.checked_add(length)
590 .ok_or_else(|| VfsError::new("EINVAL", "insert range size overflows"))?,
591 )?;
592 self.pwrite(path, tail, offset + length)?;
593 self.punch_hole(path, offset, length)
594 }
595 fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
596 validate_shift_range(offset, length)?;
597 let size = self.stat(path)?.size;
598 let end = offset
599 .checked_add(length)
600 .ok_or_else(|| VfsError::new("EINVAL", "collapse range overflows"))?;
601 if end >= size {
602 return Err(VfsError::new(
603 "EINVAL",
604 "collapse range must end before EOF",
605 ));
606 }
607 let tail_len = usize::try_from(size - end)
608 .map_err(|_| VfsError::new("EINVAL", "collapse range tail is too large"))?;
609 let tail = self.pread(path, end, tail_len)?;
610 self.pwrite(path, tail, offset)?;
611 self.truncate(path, size - length)
612 }
613 fn zero_range(
615 &mut self,
616 path: &str,
617 offset: u64,
618 length: u64,
619 keep_size: bool,
620 ) -> VfsResult<()> {
621 let end = offset
622 .checked_add(length)
623 .ok_or_else(|| VfsError::new("EINVAL", "zero range overflows"))?;
624 if length == 0 {
625 return Err(VfsError::new("EINVAL", "zero range length must be nonzero"));
626 }
627 let original_size = self.stat(path)?.size;
628 self.allocate(path, offset, length)?;
629 let zero_end = if keep_size {
630 end.min(original_size)
631 } else {
632 end
633 };
634 let mut cursor = offset.min(zero_end);
635 while cursor < zero_end {
636 let chunk_len = (zero_end - cursor).min(64 * 1024) as usize;
637 self.pwrite(path, vec![0; chunk_len], cursor)?;
638 cursor += chunk_len as u64;
639 }
640 if keep_size && self.stat(path)?.size != original_size {
641 self.truncate(path, original_size)?;
642 }
643 Ok(())
644 }
645 fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
648 let requested_end = offset
649 .checked_add(length)
650 .ok_or_else(|| VfsError::new("EINVAL", "hole-punch range overflows"))?;
651 let size = self.stat(path)?.size;
652 let end = requested_end.min(size);
653 let mut cursor = offset.min(size);
654 while cursor < end {
655 let chunk_len = (end - cursor).min(64 * 1024) as usize;
656 self.pwrite(path, vec![0; chunk_len], cursor)?;
657 cursor += chunk_len as u64;
658 }
659 Ok(())
660 }
661 fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
663 Err(VfsError::new(
664 "EOPNOTSUPP",
665 format!("extent mapping is not supported for {path}"),
666 ))
667 }
668 fn unwritten_ranges(&mut self, _path: &str) -> VfsResult<Vec<(u64, u64)>> {
671 Ok(Vec::new())
672 }
673 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>>;
674 fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
677 let content = content.into();
678 let mut existing = self.read_file(path)?;
679 let start = checked_file_len(offset, "pwrite offset")?;
680 if start > existing.len() {
681 resize_file_data(&mut existing, start)?;
682 }
683 let end = start.checked_add(content.len()).ok_or_else(|| {
684 VfsError::new(
685 "ENOMEM",
686 format!(
687 "pwrite result length overflows addressable memory: offset {offset}, content length {}",
688 content.len()
689 ),
690 )
691 })?;
692 if end > existing.len() {
693 resize_file_data(&mut existing, end)?;
694 }
695 existing[start..end].copy_from_slice(&content);
696 self.write_file(path, existing)
697 }
698}
699
700#[derive(Debug, Clone)]
701struct Metadata {
702 mode: u32,
703 uid: u32,
704 gid: u32,
705 nlink: u64,
706 ino: u64,
707 atime_ms: u64,
708 atime_nsec: u32,
709 mtime_ms: u64,
710 mtime_nsec: u32,
711 ctime_ms: u64,
712 ctime_nsec: u32,
713 birthtime_ms: u64,
714 allocated_extents: Vec<(u64, u64)>,
715 unwritten_extents: Vec<(u64, u64)>,
716 xattrs: BTreeMap<String, Vec<u8>>,
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720pub struct MemoryFileSystemSnapshotMetadata {
721 pub mode: u32,
722 pub uid: u32,
723 pub gid: u32,
724 pub nlink: u64,
725 pub ino: u64,
726 pub atime_ms: u64,
727 #[serde(default)]
728 pub atime_nsec: u32,
729 pub mtime_ms: u64,
730 #[serde(default)]
731 pub mtime_nsec: u32,
732 pub ctime_ms: u64,
733 #[serde(default)]
734 pub ctime_nsec: u32,
735 pub birthtime_ms: u64,
736 #[serde(default)]
737 pub allocated_extents: Vec<(u64, u64)>,
738 #[serde(default)]
739 pub unwritten_extents: Vec<(u64, u64)>,
740 #[serde(default)]
741 pub xattrs: BTreeMap<String, Vec<u8>>,
742}
743
744#[derive(Debug, Clone)]
745enum InodeKind {
746 File { data: Vec<u8> },
747 Directory,
748 SymbolicLink { target: String },
749 CharacterDevice { rdev: u64 },
750 BlockDevice { rdev: u64 },
751 Fifo,
752}
753
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755pub enum MemoryFileSystemSnapshotInodeKind {
756 File { data: Vec<u8> },
757 Directory,
758 SymbolicLink { target: String },
759 CharacterDevice { rdev: u64 },
760 BlockDevice { rdev: u64 },
761 Fifo,
762}
763
764#[derive(Debug, Clone)]
765struct Inode {
766 metadata: Metadata,
767 kind: InodeKind,
768}
769
770#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
771pub struct MemoryFileSystemSnapshotInode {
772 pub metadata: MemoryFileSystemSnapshotMetadata,
773 pub kind: MemoryFileSystemSnapshotInodeKind,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777pub struct MemoryFileSystemSnapshot {
778 pub path_index: BTreeMap<String, u64>,
779 pub inodes: BTreeMap<u64, MemoryFileSystemSnapshotInode>,
780 pub next_ino: u64,
781}
782
783#[derive(Debug)]
784pub struct MemoryFileSystem {
785 device_id: u64,
786 path_index: BTreeMap<String, u64>,
787 inodes: BTreeMap<u64, Inode>,
788 next_ino: u64,
789}
790
791impl MemoryFileSystem {
792 pub fn new() -> Self {
793 let mut filesystem = Self {
794 device_id: allocate_memory_filesystem_device_id(),
795 path_index: BTreeMap::new(),
796 inodes: BTreeMap::new(),
797 next_ino: 1,
798 };
799
800 let root_ino = filesystem.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
801 filesystem.path_index.insert(String::from("/"), root_ino);
802 filesystem
803 }
804
805 pub fn read_dir_filtered_limited<F>(
806 &mut self,
807 path: &str,
808 max_entries: usize,
809 mut include: F,
810 ) -> VfsResult<Vec<String>>
811 where
812 F: FnMut(&str) -> bool,
813 {
814 self.assert_directory_path(path, "scandir")?;
815 let resolved = self.resolve_path(path, 0)?;
816 self.inode_mut_for_existing_path(&resolved, "scandir", false)?
817 .metadata
818 .atime_ms = now_ms();
819 let prefix = if resolved == "/" {
820 String::from("/")
821 } else {
822 format!("{resolved}/")
823 };
824
825 let mut entries = BTreeMap::<String, String>::new();
826 for (candidate_path, _) in self.path_index.range(prefix.clone()..) {
827 if !candidate_path.starts_with(&prefix) {
828 break;
829 }
830
831 let rest = &candidate_path[prefix.len()..];
832 if rest.is_empty() || rest.contains('/') || !include(rest) {
833 continue;
834 }
835
836 entries.insert(String::from(rest), String::from(rest));
837 if entries.len() > max_entries {
838 return Err(VfsError::new(
839 "ENOMEM",
840 format!(
841 "directory listing for '{path}' exceeds configured limit of {max_entries} entries"
842 ),
843 ));
844 }
845 }
846
847 Ok(entries.into_values().collect())
848 }
849
850 pub fn link_count_in_subtree(&self, ino: u64, path: &str) -> usize {
851 let normalized = normalize_path(path);
852 let prefix = if normalized == "/" {
853 String::from("/")
854 } else {
855 format!("{normalized}/")
856 };
857
858 self.path_index
859 .iter()
860 .filter(|(candidate_path, candidate_ino)| {
861 **candidate_ino == ino
862 && (candidate_path.as_str() == normalized
863 || candidate_path.starts_with(&prefix))
864 })
865 .count()
866 }
867
868 fn allocate_inode(&mut self, kind: InodeKind, mode: u32) -> u64 {
869 let ino = self.next_ino;
870 self.next_ino += 1;
871 let now = now_ms();
872 let nlink = if matches!(kind, InodeKind::Directory) {
873 2
874 } else {
875 1
876 };
877 let allocated_extents = match &kind {
878 InodeKind::File { data } => dense_allocation(data.len() as u64),
879 InodeKind::Directory
880 | InodeKind::SymbolicLink { .. }
881 | InodeKind::CharacterDevice { .. }
882 | InodeKind::BlockDevice { .. }
883 | InodeKind::Fifo => Vec::new(),
884 };
885 self.inodes.insert(
886 ino,
887 Inode {
888 metadata: Metadata {
889 mode,
890 uid: DEFAULT_UID,
891 gid: DEFAULT_GID,
892 nlink,
893 ino,
894 atime_ms: now,
895 atime_nsec: 0,
896 mtime_ms: now,
897 mtime_nsec: 0,
898 ctime_ms: now,
899 ctime_nsec: 0,
900 birthtime_ms: now,
901 allocated_extents,
902 unwritten_extents: Vec::new(),
903 xattrs: BTreeMap::new(),
904 },
905 kind,
906 },
907 );
908 ino
909 }
910
911 pub fn symlink_with_metadata(
912 &mut self,
913 target: &str,
914 link_path: &str,
915 mode: u32,
916 uid: u32,
917 gid: u32,
918 ) -> VfsResult<()> {
919 let normalized = self.resolve_exact_path(link_path)?;
920 if self.path_index.contains_key(&normalized) {
921 return Err(VfsError::already_exists("symlink", link_path));
922 }
923
924 self.assert_directory_path(&dirname(&normalized), "symlink")?;
925 let ino = self.allocate_inode(
926 InodeKind::SymbolicLink {
927 target: String::from(target),
928 },
929 if mode & 0o170000 == 0 {
930 S_IFLNK | (mode & 0o7777)
931 } else {
932 mode
933 },
934 );
935 let inode = self
936 .inodes
937 .get_mut(&ino)
938 .expect("allocated inode should exist");
939 inode.metadata.uid = uid;
940 inode.metadata.gid = gid;
941 self.path_index.insert(normalized, ino);
942 Ok(())
943 }
944
945 fn resolve_path_with_options(
946 &self,
947 path: &str,
948 follow_final_symlink: bool,
949 depth: usize,
950 ) -> VfsResult<String> {
951 validate_path(path)?;
952 if depth > MAX_SYMLINK_DEPTH {
953 return Err(VfsError::symlink_loop(path));
954 }
955
956 let normalized = normalize_path(path);
957 if normalized == "/" {
958 return Ok(normalized);
959 }
960
961 let components: Vec<&str> = normalized
962 .split('/')
963 .filter(|part| !part.is_empty())
964 .collect();
965 let mut current = String::from("/");
966
967 for (index, component) in components.iter().enumerate() {
968 let candidate = if current == "/" {
969 format!("/{}", component)
970 } else {
971 format!("{current}/{}", component)
972 };
973 let is_final = index + 1 == components.len();
974 let should_follow = !is_final || follow_final_symlink;
975
976 if let Some(ino) = self.path_index.get(&candidate) {
977 let inode = self
978 .inodes
979 .get(ino)
980 .expect("path index should always point at a valid inode");
981
982 if should_follow {
983 if let InodeKind::SymbolicLink { target } = &inode.kind {
984 let target_path = if target.starts_with('/') {
985 target.clone()
986 } else {
987 normalize_path(&format!("{}/{}", dirname(&candidate), target))
988 };
989 let remainder = components[index + 1..].join("/");
990 let next_path = if remainder.is_empty() {
991 target_path
992 } else {
993 normalize_path(&format!("{target_path}/{remainder}"))
994 };
995 return self.resolve_path_with_options(
996 &next_path,
997 follow_final_symlink,
998 depth + 1,
999 );
1000 }
1001 }
1002
1003 if !is_final && !matches!(inode.kind, InodeKind::Directory) {
1004 return Err(VfsError::not_directory("stat", &candidate));
1005 }
1006 }
1007
1008 current = candidate;
1009 }
1010
1011 Ok(current)
1012 }
1013
1014 fn resolve_path(&self, path: &str, depth: usize) -> VfsResult<String> {
1015 self.resolve_path_with_options(path, true, depth)
1016 }
1017
1018 fn resolve_exact_path(&self, path: &str) -> VfsResult<String> {
1019 self.resolve_path_with_options(path, false, 0)
1020 }
1021
1022 fn inode_id_for_existing_path(
1023 &self,
1024 path: &str,
1025 op: &'static str,
1026 follow_symlinks: bool,
1027 ) -> VfsResult<u64> {
1028 let normalized = normalize_path(path);
1029 let resolved = if follow_symlinks {
1030 self.resolve_path(&normalized, 0)?
1031 } else {
1032 self.resolve_exact_path(&normalized)?
1033 };
1034 self.path_index
1035 .get(&resolved)
1036 .copied()
1037 .ok_or_else(|| VfsError::not_found(op, path))
1038 }
1039
1040 fn inode_for_existing_path(
1041 &self,
1042 path: &str,
1043 op: &'static str,
1044 follow_symlinks: bool,
1045 ) -> VfsResult<&Inode> {
1046 let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
1047 Ok(self
1048 .inodes
1049 .get(&ino)
1050 .expect("existing path should resolve to a live inode"))
1051 }
1052
1053 fn inode_mut_for_existing_path(
1054 &mut self,
1055 path: &str,
1056 op: &'static str,
1057 follow_symlinks: bool,
1058 ) -> VfsResult<&mut Inode> {
1059 let ino = self.inode_id_for_existing_path(path, op, follow_symlinks)?;
1060 Ok(self
1061 .inodes
1062 .get_mut(&ino)
1063 .expect("existing path should resolve to a live inode"))
1064 }
1065
1066 fn assert_directory_path(&self, path: &str, op: &'static str) -> VfsResult<()> {
1067 let inode = self.inode_for_existing_path(path, op, true)?;
1068 if matches!(inode.kind, InodeKind::Directory) {
1069 Ok(())
1070 } else {
1071 Err(VfsError::not_directory(op, path))
1072 }
1073 }
1074
1075 fn remove_exact_path(&mut self, path: &str) -> VfsResult<()> {
1076 let normalized = self.resolve_exact_path(path)?;
1077 let ino = self
1078 .path_index
1079 .get(&normalized)
1080 .copied()
1081 .ok_or_else(|| VfsError::not_found("unlink", path))?;
1082 let inode = self
1083 .inodes
1084 .get(&ino)
1085 .expect("existing path should resolve to a live inode");
1086
1087 if matches!(inode.kind, InodeKind::Directory) {
1088 return Err(VfsError::is_directory("unlink", path));
1089 }
1090
1091 self.inodes
1092 .get_mut(&ino)
1093 .expect("inode should exist when unlinking")
1094 .metadata
1095 .ctime_ms = now_ms();
1096 self.path_index.remove(&normalized);
1097 self.decrement_link_count(ino);
1098 Ok(())
1099 }
1100
1101 fn remove_existing_destination(&mut self, path: &str) -> VfsResult<()> {
1102 let normalized = self.resolve_exact_path(path)?;
1103 let Some(ino) = self.path_index.get(&normalized).copied() else {
1104 return Ok(());
1105 };
1106
1107 let inode = self
1108 .inodes
1109 .get(&ino)
1110 .expect("existing path should resolve to a live inode");
1111
1112 if matches!(inode.kind, InodeKind::Directory) {
1113 let prefix = format!("{normalized}/");
1114 if self
1115 .path_index
1116 .keys()
1117 .any(|candidate| candidate.starts_with(&prefix))
1118 {
1119 return Err(VfsError::not_empty(path));
1120 }
1121 }
1122
1123 self.inodes
1124 .get_mut(&ino)
1125 .expect("inode should exist when removing destination")
1126 .metadata
1127 .ctime_ms = now_ms();
1128 self.path_index.remove(&normalized);
1129 self.decrement_link_count(ino);
1130 Ok(())
1131 }
1132
1133 fn decrement_link_count(&mut self, ino: u64) {
1134 let should_remove = {
1135 let inode = self
1136 .inodes
1137 .get_mut(&ino)
1138 .expect("inode should exist when decrementing link count");
1139 inode.metadata.nlink = inode.metadata.nlink.saturating_sub(1);
1140 inode.metadata.nlink == 0
1141 };
1142
1143 if should_remove {
1144 self.inodes.remove(&ino);
1145 }
1146 }
1147
1148 fn build_stat(&self, inode: &Inode) -> VirtualStat {
1149 let size = match &inode.kind {
1150 InodeKind::File { data } => data.len() as u64,
1151 InodeKind::Directory => DIRECTORY_SIZE,
1152 InodeKind::SymbolicLink { target } => target.len() as u64,
1153 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1154 0
1155 }
1156 };
1157
1158 VirtualStat {
1159 mode: inode.metadata.mode,
1160 size,
1161 blocks: allocated_block_count(&inode.metadata.allocated_extents),
1162 dev: self.device_id,
1163 rdev: match inode.kind {
1164 InodeKind::CharacterDevice { rdev } => rdev,
1165 InodeKind::BlockDevice { rdev } => rdev,
1166 _ => 0,
1167 },
1168 is_directory: matches!(inode.kind, InodeKind::Directory),
1169 is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
1170 atime_ms: inode.metadata.atime_ms,
1171 atime_nsec: inode.metadata.atime_nsec,
1172 mtime_ms: inode.metadata.mtime_ms,
1173 mtime_nsec: inode.metadata.mtime_nsec,
1174 ctime_ms: inode.metadata.ctime_ms,
1175 ctime_nsec: inode.metadata.ctime_nsec,
1176 birthtime_ms: inode.metadata.birthtime_ms,
1177 ino: inode.metadata.ino,
1178 nlink: inode.metadata.nlink,
1179 uid: inode.metadata.uid,
1180 gid: inode.metadata.gid,
1181 }
1182 }
1183
1184 pub fn snapshot(&self) -> MemoryFileSystemSnapshot {
1189 MemoryFileSystemSnapshot {
1190 path_index: self.path_index.clone(),
1191 inodes: self
1192 .inodes
1193 .iter()
1194 .map(|(ino, inode)| {
1195 (
1196 *ino,
1197 MemoryFileSystemSnapshotInode {
1198 metadata: MemoryFileSystemSnapshotMetadata {
1199 mode: inode.metadata.mode,
1200 uid: inode.metadata.uid,
1201 gid: inode.metadata.gid,
1202 nlink: inode.metadata.nlink,
1203 ino: inode.metadata.ino,
1204 atime_ms: inode.metadata.atime_ms,
1205 atime_nsec: inode.metadata.atime_nsec,
1206 mtime_ms: inode.metadata.mtime_ms,
1207 mtime_nsec: inode.metadata.mtime_nsec,
1208 ctime_ms: inode.metadata.ctime_ms,
1209 ctime_nsec: inode.metadata.ctime_nsec,
1210 birthtime_ms: inode.metadata.birthtime_ms,
1211 allocated_extents: inode.metadata.allocated_extents.clone(),
1212 unwritten_extents: inode.metadata.unwritten_extents.clone(),
1213 xattrs: inode.metadata.xattrs.clone(),
1214 },
1215 kind: match &inode.kind {
1216 InodeKind::File { data } => {
1217 MemoryFileSystemSnapshotInodeKind::File { data: data.clone() }
1218 }
1219 InodeKind::Directory => {
1220 MemoryFileSystemSnapshotInodeKind::Directory
1221 }
1222 InodeKind::SymbolicLink { target } => {
1223 MemoryFileSystemSnapshotInodeKind::SymbolicLink {
1224 target: target.clone(),
1225 }
1226 }
1227 InodeKind::CharacterDevice { rdev } => {
1228 MemoryFileSystemSnapshotInodeKind::CharacterDevice {
1229 rdev: *rdev,
1230 }
1231 }
1232 InodeKind::BlockDevice { rdev } => {
1233 MemoryFileSystemSnapshotInodeKind::BlockDevice { rdev: *rdev }
1234 }
1235 InodeKind::Fifo => MemoryFileSystemSnapshotInodeKind::Fifo,
1236 },
1237 },
1238 )
1239 })
1240 .collect(),
1241 next_ino: self.next_ino,
1242 }
1243 }
1244
1245 pub fn from_snapshot(snapshot: MemoryFileSystemSnapshot) -> Self {
1246 Self {
1247 device_id: allocate_memory_filesystem_device_id(),
1248 path_index: snapshot.path_index,
1249 inodes: snapshot
1250 .inodes
1251 .into_iter()
1252 .map(|(ino, inode)| {
1253 (
1254 ino,
1255 Inode {
1256 metadata: Metadata {
1257 mode: inode.metadata.mode,
1258 uid: inode.metadata.uid,
1259 gid: inode.metadata.gid,
1260 nlink: inode.metadata.nlink,
1261 ino: inode.metadata.ino,
1262 atime_ms: inode.metadata.atime_ms,
1263 atime_nsec: inode.metadata.atime_nsec,
1264 mtime_ms: inode.metadata.mtime_ms,
1265 mtime_nsec: inode.metadata.mtime_nsec,
1266 ctime_ms: inode.metadata.ctime_ms,
1267 ctime_nsec: inode.metadata.ctime_nsec,
1268 birthtime_ms: inode.metadata.birthtime_ms,
1269 allocated_extents: inode.metadata.allocated_extents,
1270 unwritten_extents: inode.metadata.unwritten_extents,
1271 xattrs: inode.metadata.xattrs,
1272 },
1273 kind: match inode.kind {
1274 MemoryFileSystemSnapshotInodeKind::File { data } => {
1275 InodeKind::File { data }
1276 }
1277 MemoryFileSystemSnapshotInodeKind::Directory => {
1278 InodeKind::Directory
1279 }
1280 MemoryFileSystemSnapshotInodeKind::SymbolicLink { target } => {
1281 InodeKind::SymbolicLink { target }
1282 }
1283 MemoryFileSystemSnapshotInodeKind::CharacterDevice { rdev } => {
1284 InodeKind::CharacterDevice { rdev }
1285 }
1286 MemoryFileSystemSnapshotInodeKind::BlockDevice { rdev } => {
1287 InodeKind::BlockDevice { rdev }
1288 }
1289 MemoryFileSystemSnapshotInodeKind::Fifo => InodeKind::Fifo,
1290 },
1291 },
1292 )
1293 })
1294 .collect(),
1295 next_ino: snapshot.next_ino,
1296 }
1297 }
1298}
1299
1300impl VirtualFileSystem for MemoryFileSystem {
1301 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
1302 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
1303 match &inode.kind {
1304 InodeKind::File { data } => {
1305 inode.metadata.atime_ms = now_ms();
1306 Ok(data.clone())
1307 }
1308 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1309 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1310 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1311 Err(VfsError::new(
1312 "ENXIO",
1313 format!("device I/O requires kernel dispatch: {path}"),
1314 ))
1315 }
1316 }
1317 }
1318
1319 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
1320 Ok(self
1321 .read_dir_with_types(path)?
1322 .into_iter()
1323 .map(|entry| entry.name)
1324 .collect())
1325 }
1326
1327 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
1328 self.read_dir_filtered_limited(path, max_entries, |_| true)
1329 }
1330
1331 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
1332 self.assert_directory_path(path, "scandir")?;
1333 let resolved = self.resolve_path(path, 0)?;
1334 self.inode_mut_for_existing_path(&resolved, "scandir", false)?
1335 .metadata
1336 .atime_ms = now_ms();
1337 let prefix = if resolved == "/" {
1338 String::from("/")
1339 } else {
1340 format!("{resolved}/")
1341 };
1342
1343 let mut entries = BTreeMap::<String, VirtualDirEntry>::new();
1344 for (candidate_path, ino) in self.path_index.range(prefix.clone()..) {
1345 if !candidate_path.starts_with(&prefix) {
1346 break;
1347 }
1348
1349 let rest = &candidate_path[prefix.len()..];
1350 if rest.is_empty() || rest.contains('/') {
1351 continue;
1352 }
1353
1354 let inode = self
1355 .inodes
1356 .get(ino)
1357 .expect("path index should always point at a valid inode");
1358 entries.insert(
1359 String::from(rest),
1360 VirtualDirEntry {
1361 name: String::from(rest),
1362 is_directory: matches!(inode.kind, InodeKind::Directory),
1363 is_symbolic_link: matches!(inode.kind, InodeKind::SymbolicLink { .. }),
1364 },
1365 );
1366 }
1367
1368 Ok(entries.into_values().collect())
1369 }
1370
1371 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1372 let normalized = self.resolve_path(path, 0)?;
1373 self.mkdir(&dirname(&normalized), true)?;
1374 let data = content.into();
1375
1376 if self.path_index.contains_key(&normalized) {
1377 let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
1378 let now = now_ms();
1379 match &mut inode.kind {
1380 InodeKind::File { data: existing } => {
1381 *existing = data;
1382 inode.metadata.allocated_extents = dense_allocation(existing.len() as u64);
1383 inode.metadata.unwritten_extents.clear();
1384 inode.metadata.mtime_ms = now;
1385 inode.metadata.ctime_ms = now;
1386 return Ok(());
1387 }
1388 InodeKind::Directory => return Err(VfsError::is_directory("open", path)),
1389 InodeKind::SymbolicLink { .. } => return Err(VfsError::not_found("open", path)),
1390 InodeKind::CharacterDevice { .. }
1391 | InodeKind::BlockDevice { .. }
1392 | InodeKind::Fifo => {
1393 return Err(VfsError::new(
1394 "ENXIO",
1395 format!("device write requires kernel dispatch: {path}"),
1396 ))
1397 }
1398 }
1399 }
1400
1401 let ino = self.allocate_inode(InodeKind::File { data }, S_IFREG | 0o644);
1402 self.path_index.insert(normalized, ino);
1403 Ok(())
1404 }
1405
1406 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
1407 let normalized = self.resolve_path(path, 0)?;
1408 self.mkdir(&dirname(&normalized), true)?;
1409 if self.path_index.contains_key(&normalized) {
1410 return Err(VfsError::already_exists("open", path));
1411 }
1412
1413 let ino = self.allocate_inode(
1414 InodeKind::File {
1415 data: content.into(),
1416 },
1417 S_IFREG | 0o644,
1418 );
1419 self.path_index.insert(normalized, ino);
1420 Ok(())
1421 }
1422
1423 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
1424 let normalized = self.resolve_path(path, 0)?;
1425 let data = content.into();
1426 let inode = self.inode_mut_for_existing_path(&normalized, "open", false)?;
1427 let now = now_ms();
1428 match &mut inode.kind {
1429 InodeKind::File { data: existing } => {
1430 let offset = existing.len() as u64;
1431 reserve_file_growth(existing, data.len())?;
1432 existing.extend_from_slice(&data);
1433 allocate_range(
1434 &mut inode.metadata.allocated_extents,
1435 offset,
1436 data.len() as u64,
1437 );
1438 remove_extent_range(
1439 &mut inode.metadata.unwritten_extents,
1440 offset,
1441 data.len() as u64,
1442 );
1443 inode.metadata.mtime_ms = now;
1444 inode.metadata.ctime_ms = now;
1445 Ok(existing.len() as u64)
1446 }
1447 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
1448 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
1449 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1450 Err(VfsError::new(
1451 "ENXIO",
1452 format!("device I/O requires kernel dispatch: {path}"),
1453 ))
1454 }
1455 }
1456 }
1457
1458 fn create_dir(&mut self, path: &str) -> VfsResult<()> {
1459 let normalized = self.resolve_exact_path(path)?;
1460 if normalized == "/" {
1461 return Ok(());
1462 }
1463
1464 self.assert_directory_path(&dirname(&normalized), "mkdir")?;
1465 if let Some(existing) = self.path_index.get(&normalized) {
1466 let inode = self
1467 .inodes
1468 .get(existing)
1469 .expect("path index should always point at a valid inode");
1470 if matches!(inode.kind, InodeKind::Directory) {
1471 return Ok(());
1472 }
1473 return Err(VfsError::already_exists("mkdir", path));
1474 }
1475
1476 let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1477 self.path_index.insert(normalized, ino);
1478 Ok(())
1479 }
1480
1481 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
1482 let normalized = normalize_path(path);
1483 if normalized == "/" {
1484 return Ok(());
1485 }
1486
1487 if !recursive {
1488 return self.create_dir(path);
1489 }
1490
1491 let parts: Vec<&str> = normalized
1492 .split('/')
1493 .filter(|part| !part.is_empty())
1494 .collect();
1495 let mut current = String::from("/");
1496
1497 for (index, part) in parts.iter().enumerate() {
1498 let raw_path = if current == "/" {
1499 format!("/{}", part)
1500 } else {
1501 format!("{current}/{}", part)
1502 };
1503 let resolved =
1504 self.resolve_path_with_options(&raw_path, index + 1 != parts.len(), 0)?;
1505
1506 match self.path_index.get(&resolved).copied() {
1507 Some(ino) => {
1508 let inode = self
1509 .inodes
1510 .get(&ino)
1511 .expect("path index should always point at a valid inode");
1512 if !matches!(inode.kind, InodeKind::Directory) {
1513 return Err(VfsError::not_directory("mkdir", &raw_path));
1514 }
1515 }
1516 None => {
1517 let ino = self.allocate_inode(InodeKind::Directory, S_IFDIR | 0o755);
1518 self.path_index.insert(resolved.clone(), ino);
1519 }
1520 }
1521
1522 current = resolved;
1523 }
1524
1525 Ok(())
1526 }
1527
1528 fn mknod(&mut self, path: &str, mode: u32, rdev: u64) -> VfsResult<()> {
1529 let normalized = self.resolve_path(path, 0)?;
1530 self.mkdir(&dirname(&normalized), true)?;
1531 if self.path_index.contains_key(&normalized) {
1532 return Err(VfsError::already_exists("mknod", path));
1533 }
1534 let (kind, type_mode) = match mode & 0o170000 {
1535 S_IFCHR => (InodeKind::CharacterDevice { rdev }, S_IFCHR),
1536 S_IFBLK => (InodeKind::BlockDevice { rdev }, S_IFBLK),
1537 S_IFIFO => (InodeKind::Fifo, S_IFIFO),
1538 _ => return Err(VfsError::invalid_input("unsupported special inode type")),
1539 };
1540 let ino = self.allocate_inode(kind, type_mode | (mode & 0o7777));
1541 self.path_index.insert(normalized, ino);
1542 Ok(())
1543 }
1544
1545 fn exists(&self, path: &str) -> bool {
1546 self.resolve_path(path, 0)
1547 .ok()
1548 .is_some_and(|resolved| self.path_index.contains_key(&resolved))
1549 }
1550
1551 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
1552 let inode = self.inode_for_existing_path(path, "stat", true)?;
1553 Ok(self.build_stat(inode))
1554 }
1555
1556 fn remove_file(&mut self, path: &str) -> VfsResult<()> {
1557 self.remove_exact_path(path)
1558 }
1559
1560 fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
1561 let normalized = self.resolve_exact_path(path)?;
1562 if normalized == "/" {
1563 return Err(VfsError::permission_denied("rmdir", path));
1564 }
1565
1566 let ino = self
1567 .path_index
1568 .get(&normalized)
1569 .copied()
1570 .ok_or_else(|| VfsError::not_found("rmdir", path))?;
1571 let inode = self
1572 .inodes
1573 .get(&ino)
1574 .expect("path index should always point at a valid inode");
1575 if !matches!(inode.kind, InodeKind::Directory) {
1576 return Err(VfsError::not_directory("rmdir", path));
1577 }
1578
1579 let prefix = format!("{normalized}/");
1580 if self
1581 .path_index
1582 .keys()
1583 .any(|candidate| candidate.starts_with(&prefix))
1584 {
1585 return Err(VfsError::not_empty(path));
1586 }
1587
1588 self.path_index.remove(&normalized);
1589 self.decrement_link_count(ino);
1590 Ok(())
1591 }
1592
1593 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1594 let old_normalized = self.resolve_exact_path(old_path)?;
1595 let new_normalized = self.resolve_exact_path(new_path)?;
1596
1597 if old_normalized == "/" {
1598 return Err(VfsError::permission_denied("rename", old_path));
1599 }
1600
1601 if old_normalized == new_normalized {
1602 return Ok(());
1603 }
1604
1605 self.assert_directory_path(&dirname(&new_normalized), "rename")?;
1606
1607 if new_normalized.starts_with(&(old_normalized.clone() + "/")) {
1608 return Err(VfsError::invalid_input(format!(
1609 "cannot move '{}' into its own descendant '{}'",
1610 old_path, new_path
1611 )));
1612 }
1613
1614 let ino = self
1615 .path_index
1616 .get(&old_normalized)
1617 .copied()
1618 .ok_or_else(|| VfsError::not_found("rename", old_path))?;
1619 let is_directory = matches!(
1620 self.inodes
1621 .get(&ino)
1622 .expect("path index should always point at a valid inode")
1623 .kind,
1624 InodeKind::Directory
1625 );
1626
1627 if let Some(destination_ino) = self.path_index.get(&new_normalized).copied() {
1628 let destination_is_directory = matches!(
1629 self.inodes
1630 .get(&destination_ino)
1631 .expect("destination path should point at a valid inode")
1632 .kind,
1633 InodeKind::Directory
1634 );
1635 match (is_directory, destination_is_directory) {
1636 (false, true) => return Err(VfsError::is_directory("rename", new_path)),
1637 (true, false) => return Err(VfsError::not_directory("rename", new_path)),
1638 _ => {}
1639 }
1640 }
1641
1642 self.remove_existing_destination(new_path)?;
1643
1644 if !is_directory {
1645 self.path_index.remove(&old_normalized);
1646 self.path_index.insert(new_normalized, ino);
1647 self.inodes
1648 .get_mut(&ino)
1649 .expect("renamed inode should exist")
1650 .metadata
1651 .ctime_ms = now_ms();
1652 return Ok(());
1653 }
1654
1655 let prefix = format!("{old_normalized}/");
1656 let to_move: Vec<(String, u64)> = self
1657 .path_index
1658 .iter()
1659 .filter(|(path, _)| **path == old_normalized || path.starts_with(&prefix))
1660 .map(|(path, inode_id)| (path.clone(), *inode_id))
1661 .collect();
1662
1663 for (path, _) in &to_move {
1664 self.path_index.remove(path);
1665 }
1666
1667 for (path, inode_id) in to_move {
1668 let relocated_path = if path == old_normalized {
1669 new_normalized.clone()
1670 } else {
1671 format!("{new_normalized}{}", &path[old_normalized.len()..])
1672 };
1673 self.path_index.insert(relocated_path, inode_id);
1674 }
1675
1676 self.inodes
1677 .get_mut(&ino)
1678 .expect("renamed directory inode should exist")
1679 .metadata
1680 .ctime_ms = now_ms();
1681
1682 Ok(())
1683 }
1684
1685 fn realpath(&self, path: &str) -> VfsResult<String> {
1686 let resolved = self.resolve_path(path, 0)?;
1687 if !self.path_index.contains_key(&resolved) {
1688 return Err(VfsError::not_found("realpath", path));
1689 }
1690 Ok(resolved)
1691 }
1692
1693 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
1694 self.symlink_with_metadata(target, link_path, S_IFLNK | 0o777, DEFAULT_UID, DEFAULT_GID)
1695 }
1696
1697 fn read_link(&self, path: &str) -> VfsResult<String> {
1698 let inode = self.inode_for_existing_path(path, "readlink", false)?;
1699 match &inode.kind {
1700 InodeKind::SymbolicLink { target } => Ok(target.clone()),
1701 _ => Err(VfsError::invalid_input(format!(
1702 "invalid argument, readlink '{path}'"
1703 ))),
1704 }
1705 }
1706
1707 fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
1708 let inode = self.inode_for_existing_path(path, "lstat", false)?;
1709 Ok(self.build_stat(inode))
1710 }
1711
1712 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
1713 let ino = self.inode_id_for_existing_path(old_path, "link", true)?;
1714 let inode = self
1715 .inodes
1716 .get(&ino)
1717 .expect("path index should always point at a valid inode");
1718 if !matches!(inode.kind, InodeKind::File { .. }) {
1719 return Err(VfsError::permission_denied("link", old_path));
1720 }
1721
1722 let normalized = self.resolve_exact_path(new_path)?;
1723 if self.path_index.contains_key(&normalized) {
1724 return Err(VfsError::already_exists("link", new_path));
1725 }
1726
1727 self.assert_directory_path(&dirname(&normalized), "link")?;
1728 self.path_index.insert(normalized, ino);
1729 let inode = self
1730 .inodes
1731 .get_mut(&ino)
1732 .expect("path index should always point at a valid inode");
1733 inode.metadata.nlink += 1;
1734 inode.metadata.ctime_ms = now_ms();
1735 Ok(())
1736 }
1737
1738 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
1739 let inode = self.inode_mut_for_existing_path(path, "chmod", true)?;
1740 let type_bits = if mode & 0o170000 == 0 {
1741 inode.metadata.mode & 0o170000
1742 } else {
1743 mode & 0o170000
1744 };
1745 inode.metadata.mode = type_bits | (mode & 0o7777);
1746 inode.metadata.ctime_ms = now_ms();
1747 Ok(())
1748 }
1749
1750 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1751 let inode = self.inode_mut_for_existing_path(path, "chown", true)?;
1752 inode.metadata.uid = uid;
1753 inode.metadata.gid = gid;
1754 inode.metadata.ctime_ms = now_ms();
1755 Ok(())
1756 }
1757
1758 fn chown_spec(
1759 &mut self,
1760 path: &str,
1761 uid: u32,
1762 gid: u32,
1763 follow_symlinks: bool,
1764 ) -> VfsResult<()> {
1765 let inode = self.inode_mut_for_existing_path(path, "chown", follow_symlinks)?;
1766 inode.metadata.uid = uid;
1767 inode.metadata.gid = gid;
1768 inode.metadata.ctime_ms = now_ms();
1769 Ok(())
1770 }
1771
1772 fn lchown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
1773 let inode = self.inode_mut_for_existing_path(path, "lchown", false)?;
1774 inode.metadata.uid = uid;
1775 inode.metadata.gid = gid;
1776 inode.metadata.ctime_ms = now_ms();
1777 Ok(())
1778 }
1779
1780 fn get_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<Vec<u8>> {
1781 validate_xattr_name(name)?;
1782 self.inode_for_existing_path(path, "getxattr", follow_symlinks)?
1783 .metadata
1784 .xattrs
1785 .get(name)
1786 .cloned()
1787 .ok_or_else(|| {
1788 VfsError::new(
1789 "ENODATA",
1790 format!("extended attribute does not exist: {name}"),
1791 )
1792 })
1793 }
1794
1795 fn list_xattrs(&mut self, path: &str, follow_symlinks: bool) -> VfsResult<Vec<String>> {
1796 Ok(self
1797 .inode_for_existing_path(path, "listxattr", follow_symlinks)?
1798 .metadata
1799 .xattrs
1800 .keys()
1801 .cloned()
1802 .collect())
1803 }
1804
1805 fn set_xattr(
1806 &mut self,
1807 path: &str,
1808 name: &str,
1809 value: Vec<u8>,
1810 flags: u32,
1811 follow_symlinks: bool,
1812 ) -> VfsResult<()> {
1813 let inode = self.inode_mut_for_existing_path(path, "setxattr", follow_symlinks)?;
1814 set_xattr_value(&mut inode.metadata.xattrs, name, &value, flags)?;
1815 inode.metadata.ctime_ms = now_ms();
1816 Ok(())
1817 }
1818
1819 fn remove_xattr(&mut self, path: &str, name: &str, follow_symlinks: bool) -> VfsResult<()> {
1820 validate_xattr_name(name)?;
1821 let inode = self.inode_mut_for_existing_path(path, "removexattr", follow_symlinks)?;
1822 if inode.metadata.xattrs.remove(name).is_none() {
1823 return Err(VfsError::new(
1824 "ENODATA",
1825 format!("extended attribute does not exist: {name}"),
1826 ));
1827 }
1828 inode.metadata.ctime_ms = now_ms();
1829 Ok(())
1830 }
1831
1832 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
1833 let inode = self.inode_mut_for_existing_path(path, "utimes", true)?;
1834 inode.metadata.atime_ms = atime_ms;
1835 inode.metadata.atime_nsec = 0;
1836 inode.metadata.mtime_ms = mtime_ms;
1837 inode.metadata.mtime_nsec = 0;
1838 inode.metadata.ctime_ms = now_ms();
1839 inode.metadata.ctime_nsec = 0;
1840 Ok(())
1841 }
1842
1843 fn utimes_spec(
1844 &mut self,
1845 path: &str,
1846 atime: VirtualUtimeSpec,
1847 mtime: VirtualUtimeSpec,
1848 follow_symlinks: bool,
1849 ) -> VfsResult<()> {
1850 let stat = if follow_symlinks {
1851 self.stat(path)?
1852 } else {
1853 self.lstat(path)?
1854 };
1855 let inode = self.inode_mut_for_existing_path(path, "utimes", follow_symlinks)?;
1856 let now = now_time_spec();
1857 let atime = resolve_utime_spec(
1858 atime,
1859 now,
1860 VirtualTimeSpec {
1861 sec: (stat.atime_ms / 1_000) as i64,
1862 nsec: stat.atime_nsec,
1863 },
1864 )?;
1865 let mtime = resolve_utime_spec(
1866 mtime,
1867 now,
1868 VirtualTimeSpec {
1869 sec: (stat.mtime_ms / 1_000) as i64,
1870 nsec: stat.mtime_nsec,
1871 },
1872 )?;
1873 inode.metadata.atime_ms = atime.to_truncated_millis()?;
1874 inode.metadata.atime_nsec = atime.nsec;
1875 inode.metadata.mtime_ms = mtime.to_truncated_millis()?;
1876 inode.metadata.mtime_nsec = mtime.nsec;
1877 let ctime = now_time_spec();
1878 inode.metadata.ctime_ms = ctime.to_truncated_millis()?;
1879 inode.metadata.ctime_nsec = ctime.nsec;
1880 Ok(())
1881 }
1882
1883 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
1884 let inode = self.inode_mut_for_existing_path(path, "truncate", true)?;
1885 let now = now_ms();
1886 match &mut inode.kind {
1887 InodeKind::File { data } => {
1888 resize_file_data(data, checked_file_len(length, "truncate length")?)?;
1889 truncate_allocation(&mut inode.metadata.allocated_extents, length);
1890 truncate_allocation(&mut inode.metadata.unwritten_extents, length);
1891 inode.metadata.mtime_ms = now;
1892 inode.metadata.ctime_ms = now;
1893 Ok(())
1894 }
1895 InodeKind::Directory => Err(VfsError::is_directory("truncate", path)),
1896 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("truncate", path)),
1897 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1898 Err(VfsError::new(
1899 "ENXIO",
1900 format!("cannot truncate character device: {path}"),
1901 ))
1902 }
1903 }
1904 }
1905
1906 fn allocate(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1907 let end = offset
1908 .checked_add(length)
1909 .ok_or_else(|| VfsError::new("EINVAL", "allocation range overflows"))?;
1910 if length == 0 {
1911 return Ok(());
1912 }
1913 let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
1914 match &mut inode.kind {
1915 InodeKind::File { data } => {
1916 let end_len = checked_file_len(end, "allocation range end")?;
1917 if end_len > data.len() {
1918 resize_file_data(data, end_len)?;
1919 }
1920 allocate_unwritten_holes(
1921 &mut inode.metadata.unwritten_extents,
1922 &inode.metadata.allocated_extents,
1923 offset,
1924 length,
1925 );
1926 allocate_range(&mut inode.metadata.allocated_extents, offset, length);
1927 let now = now_ms();
1928 inode.metadata.mtime_ms = now;
1929 inode.metadata.ctime_ms = now;
1930 Ok(())
1931 }
1932 InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
1933 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
1934 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
1935 Err(VfsError::new(
1936 "ENXIO",
1937 format!("cannot allocate character device: {path}"),
1938 ))
1939 }
1940 }
1941 }
1942
1943 fn insert_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1944 validate_shift_range(offset, length)?;
1945 let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
1946 match &mut inode.kind {
1947 InodeKind::File { data } => {
1948 let start = checked_file_len(offset, "insert range offset")?;
1949 let insert_len = checked_file_len(length, "insert range length")?;
1950 if start >= data.len() {
1951 return Err(VfsError::new(
1952 "EINVAL",
1953 "insert range offset must be before EOF",
1954 ));
1955 }
1956 data.splice(start..start, std::iter::repeat_n(0, insert_len));
1957 inode.metadata.allocated_extents =
1958 allocation_after_insert(&inode.metadata.allocated_extents, offset, length);
1959 inode.metadata.unwritten_extents =
1960 allocation_after_insert(&inode.metadata.unwritten_extents, offset, length);
1961 let now = now_ms();
1962 inode.metadata.mtime_ms = now;
1963 inode.metadata.ctime_ms = now;
1964 Ok(())
1965 }
1966 InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
1967 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
1968 _ => Err(VfsError::new(
1969 "ENXIO",
1970 "cannot insert range on special inode",
1971 )),
1972 }
1973 }
1974
1975 fn collapse_range(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
1976 validate_shift_range(offset, length)?;
1977 let end = offset
1978 .checked_add(length)
1979 .ok_or_else(|| VfsError::new("EINVAL", "collapse range overflows"))?;
1980 let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
1981 match &mut inode.kind {
1982 InodeKind::File { data } => {
1983 if end >= data.len() as u64 {
1984 return Err(VfsError::new(
1985 "EINVAL",
1986 "collapse range must end before EOF",
1987 ));
1988 }
1989 let start = checked_file_len(offset, "collapse range offset")?;
1990 let end = checked_file_len(end, "collapse range end")?;
1991 data.drain(start..end);
1992 inode.metadata.allocated_extents =
1993 allocation_after_collapse(&inode.metadata.allocated_extents, offset, length);
1994 inode.metadata.unwritten_extents =
1995 allocation_after_collapse(&inode.metadata.unwritten_extents, offset, length);
1996 let now = now_ms();
1997 inode.metadata.mtime_ms = now;
1998 inode.metadata.ctime_ms = now;
1999 Ok(())
2000 }
2001 InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
2002 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
2003 _ => Err(VfsError::new(
2004 "ENXIO",
2005 "cannot collapse range on special inode",
2006 )),
2007 }
2008 }
2009
2010 fn zero_range(
2011 &mut self,
2012 path: &str,
2013 offset: u64,
2014 length: u64,
2015 keep_size: bool,
2016 ) -> VfsResult<()> {
2017 let end = offset
2018 .checked_add(length)
2019 .ok_or_else(|| VfsError::new("EINVAL", "zero range overflows"))?;
2020 if length == 0 {
2021 return Err(VfsError::new("EINVAL", "zero range length must be nonzero"));
2022 }
2023 let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
2024 match &mut inode.kind {
2025 InodeKind::File { data } => {
2026 let original_size = data.len() as u64;
2027 let zero_end = if keep_size {
2028 end.min(original_size)
2029 } else {
2030 end
2031 };
2032 if !keep_size {
2033 let end_len = checked_file_len(end, "zero range end")?;
2034 if end_len > data.len() {
2035 resize_file_data(data, end_len)?;
2036 }
2037 }
2038 let start = checked_file_len(offset.min(zero_end), "zero range offset")?;
2039 let zero_end = checked_file_len(zero_end, "zero range end")?;
2040 data[start..zero_end].fill(0);
2041 mark_zero_unwritten(
2042 &mut inode.metadata.unwritten_extents,
2043 &inode.metadata.allocated_extents,
2044 offset,
2045 length,
2046 );
2047 allocate_range(&mut inode.metadata.allocated_extents, offset, length);
2048 let now = now_ms();
2049 inode.metadata.mtime_ms = now;
2050 inode.metadata.ctime_ms = now;
2051 Ok(())
2052 }
2053 InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
2054 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
2055 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2056 Err(VfsError::new("ENXIO", format!("cannot zero range: {path}")))
2057 }
2058 }
2059 }
2060
2061 fn punch_hole(&mut self, path: &str, offset: u64, length: u64) -> VfsResult<()> {
2062 let requested_end = offset
2063 .checked_add(length)
2064 .ok_or_else(|| VfsError::new("EINVAL", "hole-punch range overflows"))?;
2065 let inode = self.inode_mut_for_existing_path(path, "fallocate", true)?;
2066 match &mut inode.kind {
2067 InodeKind::File { data } => {
2068 let start = checked_file_len(offset.min(data.len() as u64), "hole-punch offset")?;
2069 let end =
2070 checked_file_len(requested_end.min(data.len() as u64), "hole-punch range end")?;
2071 data[start..end].fill(0);
2072 punch_allocation(&mut inode.metadata.allocated_extents, offset, length);
2073 remove_extent_range(&mut inode.metadata.unwritten_extents, offset, length);
2074 let now = now_ms();
2075 inode.metadata.mtime_ms = now;
2076 inode.metadata.ctime_ms = now;
2077 Ok(())
2078 }
2079 InodeKind::Directory => Err(VfsError::is_directory("fallocate", path)),
2080 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fallocate", path)),
2081 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2082 Err(VfsError::new(
2083 "ENXIO",
2084 format!("cannot punch character device: {path}"),
2085 ))
2086 }
2087 }
2088 }
2089
2090 fn allocated_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2091 let inode = self.inode_mut_for_existing_path(path, "fiemap", true)?;
2092 match &inode.kind {
2093 InodeKind::File { data } => Ok(allocation_byte_ranges(
2094 &inode.metadata.allocated_extents,
2095 data.len() as u64,
2096 )),
2097 InodeKind::Directory => Err(VfsError::is_directory("fiemap", path)),
2098 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fiemap", path)),
2099 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2100 Ok(Vec::new())
2101 }
2102 }
2103 }
2104
2105 fn unwritten_ranges(&mut self, path: &str) -> VfsResult<Vec<(u64, u64)>> {
2106 let inode = self.inode_mut_for_existing_path(path, "fiemap", true)?;
2107 match &inode.kind {
2108 InodeKind::File { data } => Ok(allocation_byte_ranges(
2109 &inode.metadata.unwritten_extents,
2110 data.len() as u64,
2111 )),
2112 InodeKind::Directory => Err(VfsError::is_directory("fiemap", path)),
2113 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("fiemap", path)),
2114 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2115 Ok(Vec::new())
2116 }
2117 }
2118 }
2119
2120 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
2121 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
2122 match &mut inode.kind {
2123 InodeKind::File { data } => {
2124 inode.metadata.atime_ms = now_ms();
2125 let start = offset as usize;
2126 if start >= data.len() {
2127 return Ok(Vec::new());
2128 }
2129 let end = start.saturating_add(length).min(data.len());
2130 Ok(data[start..end].to_vec())
2131 }
2132 InodeKind::Directory => Err(VfsError::is_directory("open", path)),
2133 InodeKind::SymbolicLink { .. } => Err(VfsError::not_found("open", path)),
2134 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2135 Err(VfsError::new(
2136 "ENXIO",
2137 format!("device I/O requires kernel dispatch: {path}"),
2138 ))
2139 }
2140 }
2141 }
2142
2143 fn pwrite(&mut self, path: &str, content: impl Into<Vec<u8>>, offset: u64) -> VfsResult<()> {
2144 let content = content.into();
2145 let inode = self.inode_mut_for_existing_path(path, "open", true)?;
2146 let data = match &mut inode.kind {
2147 InodeKind::File { data } => data,
2148 InodeKind::Directory => return Err(VfsError::is_directory("open", path)),
2149 InodeKind::SymbolicLink { .. } => {
2150 return Err(VfsError::not_found("open", path));
2151 }
2152 InodeKind::CharacterDevice { .. } | InodeKind::BlockDevice { .. } | InodeKind::Fifo => {
2153 return Err(VfsError::new(
2154 "ENXIO",
2155 format!("device I/O requires kernel dispatch: {path}"),
2156 ));
2157 }
2158 };
2159 let start = checked_file_len(offset, "pwrite offset")?;
2160 let end = start.checked_add(content.len()).ok_or_else(|| {
2161 VfsError::new(
2162 "ENOMEM",
2163 format!(
2164 "pwrite result length overflows addressable memory: offset {offset}, content length {}",
2165 content.len()
2166 ),
2167 )
2168 })?;
2169 if end > data.len() {
2170 resize_file_data(data, end)?;
2171 }
2172 data[start..end].copy_from_slice(&content);
2173 allocate_range(
2174 &mut inode.metadata.allocated_extents,
2175 offset,
2176 content.len() as u64,
2177 );
2178 remove_extent_range(
2179 &mut inode.metadata.unwritten_extents,
2180 offset,
2181 content.len() as u64,
2182 );
2183 let now = now_ms();
2184 inode.metadata.mtime_ms = now;
2185 inode.metadata.ctime_ms = now;
2186 Ok(())
2187 }
2188}
2189
2190impl Default for MemoryFileSystem {
2191 fn default() -> Self {
2192 Self::new()
2193 }
2194}
2195
2196fn resolve_utime_spec(
2197 spec: VirtualUtimeSpec,
2198 now: VirtualTimeSpec,
2199 existing: VirtualTimeSpec,
2200) -> VfsResult<VirtualTimeSpec> {
2201 match spec {
2202 VirtualUtimeSpec::Set(spec) => Ok(spec),
2203 VirtualUtimeSpec::Now => Ok(now),
2204 VirtualUtimeSpec::Omit => Ok(existing),
2205 }
2206}
2207
2208fn resolve_utime_millis(
2209 spec: VirtualUtimeSpec,
2210 now_ms: u64,
2211 existing: Option<VirtualTimeSpec>,
2212) -> VfsResult<u64> {
2213 match spec {
2214 VirtualUtimeSpec::Set(spec) => spec.to_truncated_millis(),
2215 VirtualUtimeSpec::Now => Ok(now_ms),
2216 VirtualUtimeSpec::Omit => existing
2217 .ok_or_else(|| VfsError::new("EINVAL", "UTIME_OMIT requires existing metadata"))?
2218 .to_truncated_millis(),
2219 }
2220}
2221
2222pub fn validate_path(path: &str) -> VfsResult<()> {
2223 if path.as_bytes().contains(&0) {
2224 return Err(VfsError::invalid_input("path contains NUL byte"));
2225 }
2226 let normalized = normalize_path(path);
2227 if normalized.len() > MAX_PATH_LENGTH {
2228 return Err(VfsError::path_too_long(path));
2229 }
2230 Ok(())
2231}
2232
2233pub fn normalize_path(path: &str) -> String {
2234 if path.is_empty() {
2235 return String::from("/");
2236 }
2237
2238 let candidate = if path.starts_with('/') {
2239 path.to_owned()
2240 } else {
2241 format!("/{path}")
2242 };
2243
2244 let mut resolved = Vec::new();
2245 for part in candidate.split('/') {
2246 match part {
2247 "" | "." => {}
2248 ".." => {
2249 resolved.pop();
2250 }
2251 component => resolved.push(component),
2252 }
2253 }
2254
2255 if resolved.is_empty() {
2256 String::from("/")
2257 } else {
2258 format!("/{}", resolved.join("/"))
2259 }
2260}
2261
2262fn dense_allocation(size: u64) -> Vec<(u64, u64)> {
2263 if size == 0 {
2264 Vec::new()
2265 } else {
2266 vec![(0, size.div_ceil(512))]
2267 }
2268}
2269
2270fn allocate_range(extents: &mut Vec<(u64, u64)>, offset: u64, length: u64) {
2271 if length == 0 {
2272 return;
2273 }
2274 let start = offset / 512;
2275 let end = offset.saturating_add(length).div_ceil(512);
2276 let mut merged = Vec::with_capacity(extents.len() + 1);
2277 let mut pending = (start, end);
2278 for &(extent_start, extent_end) in extents.iter() {
2279 if extent_end < pending.0 {
2280 merged.push((extent_start, extent_end));
2281 } else if pending.1 < extent_start {
2282 merged.push(pending);
2283 pending = (extent_start, extent_end);
2284 } else {
2285 pending.0 = pending.0.min(extent_start);
2286 pending.1 = pending.1.max(extent_end);
2287 }
2288 }
2289 merged.push(pending);
2290 *extents = merged;
2291}
2292
2293fn remove_extent_range(extents: &mut Vec<(u64, u64)>, offset: u64, length: u64) {
2294 if length == 0 {
2295 return;
2296 }
2297 let start = offset / 512;
2298 let end = offset.saturating_add(length).div_ceil(512);
2299 *extents = extents
2300 .iter()
2301 .flat_map(|&(extent_start, extent_end)| {
2302 [
2303 (extent_start, extent_end.min(start)),
2304 (extent_start.max(end), extent_end),
2305 ]
2306 .into_iter()
2307 .filter(|(part_start, part_end)| part_start < part_end)
2308 })
2309 .collect();
2310}
2311
2312fn allocate_unwritten_holes(
2313 unwritten: &mut Vec<(u64, u64)>,
2314 allocated: &[(u64, u64)],
2315 offset: u64,
2316 length: u64,
2317) {
2318 if length == 0 {
2319 return;
2320 }
2321 let start = offset / 512;
2322 let end = offset.saturating_add(length).div_ceil(512);
2323 let mut cursor = start;
2324 for &(allocated_start, allocated_end) in allocated {
2325 if allocated_end <= cursor || allocated_start >= end {
2326 continue;
2327 }
2328 if cursor < allocated_start {
2329 allocate_range(
2330 unwritten,
2331 cursor.saturating_mul(512),
2332 (allocated_start.min(end) - cursor).saturating_mul(512),
2333 );
2334 }
2335 cursor = cursor.max(allocated_end).min(end);
2336 if cursor == end {
2337 return;
2338 }
2339 }
2340 if cursor < end {
2341 allocate_range(
2342 unwritten,
2343 cursor.saturating_mul(512),
2344 (end - cursor).saturating_mul(512),
2345 );
2346 }
2347}
2348
2349fn mark_zero_unwritten(
2350 unwritten: &mut Vec<(u64, u64)>,
2351 allocated: &[(u64, u64)],
2352 offset: u64,
2353 length: u64,
2354) {
2355 allocate_unwritten_holes(unwritten, allocated, offset, length);
2356 let end = offset.saturating_add(length);
2357 let first_full_block = offset.div_ceil(4096);
2358 let past_full_block = end / 4096;
2359 if first_full_block < past_full_block {
2360 allocate_range(
2361 unwritten,
2362 first_full_block * 4096,
2363 (past_full_block - first_full_block) * 4096,
2364 );
2365 }
2366}
2367
2368fn truncate_allocation(extents: &mut Vec<(u64, u64)>, size: u64) {
2369 let end = size.div_ceil(512);
2370 extents.retain_mut(|(start, extent_end)| {
2371 *extent_end = (*extent_end).min(end);
2372 *start < *extent_end
2373 });
2374}
2375
2376fn punch_allocation(extents: &mut Vec<(u64, u64)>, offset: u64, length: u64) {
2377 let start = offset.div_ceil(512);
2378 let end = offset.saturating_add(length) / 512;
2379 if start >= end {
2380 return;
2381 }
2382 *extents = extents
2383 .iter()
2384 .flat_map(|&(extent_start, extent_end)| {
2385 let left = (extent_start, extent_end.min(start));
2386 let right = (extent_start.max(end), extent_end);
2387 [left, right]
2388 .into_iter()
2389 .filter(|(part_start, part_end)| part_start < part_end)
2390 })
2391 .collect();
2392}
2393
2394fn validate_shift_range(offset: u64, length: u64) -> VfsResult<()> {
2395 if length == 0 || !offset.is_multiple_of(512) || !length.is_multiple_of(512) {
2396 return Err(VfsError::new(
2397 "EINVAL",
2398 "insert/collapse range requires a nonzero 512-byte-aligned range",
2399 ));
2400 }
2401 Ok(())
2402}
2403
2404fn allocation_after_insert(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
2405 let start = offset / 512;
2406 let shift = length / 512;
2407 normalize_extents(existing.iter().flat_map(|&(extent_start, extent_end)| {
2408 if extent_end <= start {
2409 vec![(extent_start, extent_end)]
2410 } else if extent_start >= start {
2411 vec![(extent_start + shift, extent_end + shift)]
2412 } else {
2413 vec![(extent_start, start), (start + shift, extent_end + shift)]
2414 }
2415 }))
2416}
2417
2418fn allocation_after_collapse(existing: &[(u64, u64)], offset: u64, length: u64) -> Vec<(u64, u64)> {
2419 let start = offset / 512;
2420 let end = start + length / 512;
2421 normalize_extents(existing.iter().flat_map(|&(extent_start, extent_end)| {
2422 let mut parts = Vec::with_capacity(2);
2423 if extent_start < start {
2424 parts.push((extent_start, extent_end.min(start)));
2425 }
2426 if extent_end > end {
2427 parts.push((
2428 extent_start.max(end) - (end - start),
2429 extent_end - (end - start),
2430 ));
2431 }
2432 parts
2433 }))
2434}
2435
2436fn normalize_extents(extents: impl IntoIterator<Item = (u64, u64)>) -> Vec<(u64, u64)> {
2437 let mut merged = Vec::<(u64, u64)>::new();
2438 for (start, end) in extents.into_iter().filter(|(start, end)| start < end) {
2439 if let Some(last) = merged.last_mut().filter(|last| start <= last.1) {
2440 last.1 = last.1.max(end);
2441 } else {
2442 merged.push((start, end));
2443 }
2444 }
2445 merged
2446}
2447
2448fn allocated_block_count(extents: &[(u64, u64)]) -> u64 {
2449 extents
2450 .iter()
2451 .map(|(start, end)| end.saturating_sub(*start))
2452 .sum()
2453}
2454
2455fn allocation_byte_ranges(extents: &[(u64, u64)], size: u64) -> Vec<(u64, u64)> {
2456 extents
2457 .iter()
2458 .filter_map(|&(start, end)| {
2459 let start = start.saturating_mul(512).min(size);
2460 let end = end.saturating_mul(512).min(size);
2461 (start < end).then_some((start, end))
2462 })
2463 .collect()
2464}
2465
2466fn checked_file_len(value: u64, description: &'static str) -> VfsResult<usize> {
2467 usize::try_from(value).map_err(|_| {
2468 VfsError::new(
2469 "EINVAL",
2470 format!("{description} exceeds addressable memory: {value}"),
2471 )
2472 })
2473}
2474
2475fn reserve_file_growth(data: &mut Vec<u8>, additional: usize) -> VfsResult<()> {
2476 data.try_reserve(additional).map_err(|error| {
2477 VfsError::new(
2478 "ENOMEM",
2479 format!(
2480 "file growth exceeds addressable memory: current length {}, additional {additional}: {error}",
2481 data.len()
2482 ),
2483 )
2484 })
2485}
2486
2487fn resize_file_data(data: &mut Vec<u8>, new_len: usize) -> VfsResult<()> {
2488 if new_len > data.len() {
2489 reserve_file_growth(data, new_len - data.len())?;
2490 }
2491 data.resize(new_len, 0);
2492 Ok(())
2493}
2494
2495fn dirname(path: &str) -> String {
2496 let normalized = normalize_path(path);
2497 let Some((head, _)) = normalized.rsplit_once('/') else {
2498 return String::from("/");
2499 };
2500
2501 if head.is_empty() {
2502 String::from("/")
2503 } else {
2504 String::from(head)
2505 }
2506}
2507
2508fn now_ms() -> u64 {
2509 SystemTime::now()
2510 .duration_since(UNIX_EPOCH)
2511 .unwrap_or_default()
2512 .as_millis() as u64
2513}
2514
2515fn now_time_spec() -> VirtualTimeSpec {
2516 let now = SystemTime::now()
2517 .duration_since(UNIX_EPOCH)
2518 .unwrap_or_default();
2519 VirtualTimeSpec {
2520 sec: now.as_secs() as i64,
2521 nsec: now.subsec_nanos(),
2522 }
2523}