1use std::collections::{BTreeMap, BTreeSet};
7use std::error::Error;
8use std::fmt;
9use std::sync::{Arc, Mutex, OnceLock};
10
11use vsh_store::{BlobStore, BlobStoreError};
12use vsh_types::{
13 BlobId, DiffDigest, DiffEntry, DiffKind, DirectoryDigest, FileStamp, NodeKind, NodeState,
14 SnapshotId, VPath, VPathError,
15};
16
17#[cfg(not(windows))]
18const DEFAULT_FILE_MODE: u32 = 0o644;
19#[cfg(windows)]
20const DEFAULT_FILE_MODE: u32 = 0o666;
21
22#[cfg(not(windows))]
23const fn platform_directory_mode(mode: u32) -> u32 {
24 mode
25}
26
27#[cfg(windows)]
28const fn platform_directory_mode(mode: u32) -> u32 {
29 if mode & 0o200 == 0 { 0o555 } else { 0o777 }
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct CapturedContent {
35 pub bytes: Vec<u8>,
37 pub before: FileStamp,
39 pub after: FileStamp,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct ContentLoadError {
46 message: String,
47}
48
49impl ContentLoadError {
50 #[must_use]
52 pub fn new(message: impl Into<String>) -> Self {
53 Self {
54 message: message.into(),
55 }
56 }
57}
58
59impl fmt::Display for ContentLoadError {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 formatter.write_str(&self.message)
62 }
63}
64
65impl Error for ContentLoadError {}
66
67pub trait ContentLoader: Send + Sync {
73 fn load(&self, expected: FileStamp) -> Result<CapturedContent, ContentLoadError>;
79}
80
81impl<F> ContentLoader for F
82where
83 F: Fn(FileStamp) -> Result<CapturedContent, ContentLoadError> + Send + Sync,
84{
85 fn load(&self, expected: FileStamp) -> Result<CapturedContent, ContentLoadError> {
86 self(expected)
87 }
88}
89
90struct LazyContent {
91 expected: FileStamp,
92 loader: Arc<dyn ContentLoader>,
93 captured: OnceLock<BlobId>,
94 load_lock: Mutex<()>,
95}
96
97#[derive(Clone)]
98enum ContentHandle {
99 None,
100 Materialized(BlobId),
101 Lazy(Arc<LazyContent>),
102}
103
104#[derive(Clone)]
105struct SnapshotNode {
106 state: NodeState,
107 content: ContentHandle,
108}
109
110impl SnapshotNode {
111 fn directory(mode: u32) -> Self {
112 Self {
113 state: NodeState::directory(mode),
114 content: ContentHandle::None,
115 }
116 }
117
118 fn stamped_directory(stamp: FileStamp) -> Self {
119 debug_assert_eq!(stamp.kind, NodeKind::Directory);
120 Self {
121 state: NodeState::from_stamp(stamp),
122 content: ContentHandle::None,
123 }
124 }
125
126 fn materialized(kind: NodeKind, id: BlobId, size: u64, mode: u32) -> Self {
127 let state = match kind {
128 NodeKind::File => NodeState::file(id, size, mode),
129 NodeKind::Symlink => NodeState::symlink(id, size, mode),
130 NodeKind::Directory => NodeState::directory(mode),
131 };
132 let content = match kind {
133 NodeKind::Directory => ContentHandle::None,
134 NodeKind::File | NodeKind::Symlink => ContentHandle::Materialized(id),
135 };
136 Self { state, content }
137 }
138
139 fn lazy(stamp: FileStamp, loader: Arc<dyn ContentLoader>) -> Self {
140 Self {
141 state: NodeState::from_stamp(stamp),
142 content: ContentHandle::Lazy(Arc::new(LazyContent {
143 expected: stamp,
144 loader,
145 captured: OnceLock::new(),
146 load_lock: Mutex::new(()),
147 })),
148 }
149 }
150
151 fn state(&self) -> NodeState {
152 match &self.content {
153 ContentHandle::Lazy(lazy) => lazy
154 .captured
155 .get()
156 .copied()
157 .and_then(|blob| self.state.with_blob(blob, self.state.size()))
158 .unwrap_or(self.state),
159 ContentHandle::None | ContentHandle::Materialized(_) => self.state,
160 }
161 }
162
163 fn expected_state(&self) -> NodeState {
164 self.state
165 }
166
167 fn read(&self, path: &VPath, store: &BlobStore) -> Result<(BlobId, Vec<u8>), SnapshotError> {
168 match &self.content {
169 ContentHandle::None => Err(SnapshotError::ContentUnavailable {
170 path: path.clone(),
171 kind: self.state.kind(),
172 }),
173 ContentHandle::Materialized(id) => {
174 let bytes = store.get(*id).map_err(SnapshotError::Store)?;
175 Ok((*id, bytes))
176 }
177 ContentHandle::Lazy(lazy) => {
178 if let Some(id) = lazy.captured.get().copied() {
179 let bytes = store.get(id).map_err(SnapshotError::Store)?;
180 return Ok((id, bytes));
181 }
182 let _load_guard = lazy
183 .load_lock
184 .lock()
185 .map_err(|_| SnapshotError::LazyStatePoisoned { path: path.clone() })?;
186 if let Some(id) = lazy.captured.get().copied() {
187 let bytes = store.get(id).map_err(SnapshotError::Store)?;
188 return Ok((id, bytes));
189 }
190
191 let captured = lazy.loader.load(lazy.expected).map_err(|source| {
192 SnapshotError::ContentLoad {
193 path: path.clone(),
194 source,
195 }
196 })?;
197 if captured.before != lazy.expected || captured.after != lazy.expected {
198 return Err(SnapshotError::StaleContent {
199 path: path.clone(),
200 expected: Box::new(lazy.expected),
201 before: Box::new(captured.before),
202 after: Box::new(captured.after),
203 });
204 }
205 if u64::try_from(captured.bytes.len()).ok() != Some(lazy.expected.size) {
206 return Err(SnapshotError::ContentSizeMismatch {
207 path: path.clone(),
208 expected: lazy.expected.size,
209 actual: captured.bytes.len(),
210 });
211 }
212 let id = store.put(&captured.bytes).map_err(SnapshotError::Store)?;
213 lazy.captured
214 .set(id)
215 .map_err(|_| SnapshotError::LazyStatePoisoned { path: path.clone() })?;
216 Ok((id, captured.bytes))
217 }
218 }
219 }
220
221 fn materialized_state(
222 &self,
223 path: &VPath,
224 store: &BlobStore,
225 ) -> Result<NodeState, SnapshotError> {
226 if self.state.kind() == NodeKind::Directory {
227 return Ok(self.state);
228 }
229 let (blob, bytes) = self.read(path, store)?;
230 Ok(self
231 .state
232 .with_blob(blob, bytes.len() as u64)
233 .expect("non-directory nodes accept blob content"))
234 }
235
236 fn is_lazy(&self) -> bool {
237 matches!(self.content, ContentHandle::Lazy(_))
238 }
239
240 fn is_materialized(&self) -> bool {
241 match &self.content {
242 ContentHandle::Materialized(_) => true,
243 ContentHandle::Lazy(lazy) => lazy.captured.get().is_some(),
244 ContentHandle::None => false,
245 }
246 }
247}
248
249pub struct SnapshotBuilder {
251 store: BlobStore,
252 nodes: BTreeMap<VPath, Arc<SnapshotNode>>,
253}
254
255impl SnapshotBuilder {
256 #[must_use]
258 pub fn new(store: BlobStore) -> Self {
259 let mut nodes = BTreeMap::new();
260 nodes.insert(VPath::root(), Arc::new(SnapshotNode::directory(0o755)));
261 Self { store, nodes }
262 }
263
264 #[must_use]
271 pub fn with_root_stamp(store: BlobStore, root_stamp: FileStamp) -> Self {
272 assert_eq!(root_stamp.kind, NodeKind::Directory);
273 let mut nodes = BTreeMap::new();
274 nodes.insert(
275 VPath::root(),
276 Arc::new(SnapshotNode::stamped_directory(root_stamp)),
277 );
278 Self { store, nodes }
279 }
280
281 pub fn add_directory(&mut self, path: VPath, mode: u32) -> Result<&mut Self, SnapshotError> {
287 self.insert(path, SnapshotNode::directory(mode))?;
288 Ok(self)
289 }
290
291 pub fn add_stamped_directory(
297 &mut self,
298 path: VPath,
299 stamp: FileStamp,
300 ) -> Result<&mut Self, SnapshotError> {
301 if stamp.kind != NodeKind::Directory {
302 return Err(SnapshotError::ExpectedDirectoryStamp { path, stamp });
303 }
304 self.insert(path, SnapshotNode::stamped_directory(stamp))?;
305 Ok(self)
306 }
307
308 pub fn add_file(
314 &mut self,
315 path: VPath,
316 bytes: &[u8],
317 mode: u32,
318 ) -> Result<&mut Self, SnapshotError> {
319 let blob = self.store.put(bytes).map_err(SnapshotError::Store)?;
320 self.insert(
321 path,
322 SnapshotNode::materialized(NodeKind::File, blob, bytes.len() as u64, mode),
323 )?;
324 Ok(self)
325 }
326
327 pub fn add_symlink(
333 &mut self,
334 path: VPath,
335 target: &[u8],
336 mode: u32,
337 ) -> Result<&mut Self, SnapshotError> {
338 let blob = self.store.put(target).map_err(SnapshotError::Store)?;
339 self.insert(
340 path,
341 SnapshotNode::materialized(NodeKind::Symlink, blob, target.len() as u64, mode),
342 )?;
343 Ok(self)
344 }
345
346 pub fn add_lazy<L>(
352 &mut self,
353 path: VPath,
354 stamp: FileStamp,
355 loader: L,
356 ) -> Result<&mut Self, SnapshotError>
357 where
358 L: ContentLoader + 'static,
359 {
360 if stamp.kind == NodeKind::Directory {
361 return Err(SnapshotError::LazyDirectory { path });
362 }
363 self.insert(path, SnapshotNode::lazy(stamp, Arc::new(loader)))?;
364 Ok(self)
365 }
366
367 pub fn build(self) -> Result<BaseSnapshot, SnapshotError> {
373 for path in self.nodes.keys().filter(|path| !path.is_root()) {
374 let parent = path.parent().unwrap_or_else(VPath::root);
375 let Some(parent_node) = self.nodes.get(&parent) else {
376 return Err(SnapshotError::MissingParent {
377 path: path.clone(),
378 parent,
379 });
380 };
381 if parent_node.state.kind() != NodeKind::Directory {
382 return Err(SnapshotError::ParentNotDirectory {
383 path: path.clone(),
384 parent,
385 });
386 }
387 }
388
389 let mut children: BTreeMap<VPath, BTreeSet<VPath>> = BTreeMap::new();
390 for path in self.nodes.keys().filter(|path| !path.is_root()) {
391 let parent = path.parent().unwrap_or_else(VPath::root);
392 children.entry(parent).or_default().insert(path.clone());
393 }
394 let id = snapshot_id(&self.nodes);
395 Ok(BaseSnapshot {
396 inner: Arc::new(SnapshotInner {
397 id,
398 nodes: self.nodes,
399 children,
400 store: self.store,
401 }),
402 })
403 }
404
405 fn insert(&mut self, path: VPath, node: SnapshotNode) -> Result<(), SnapshotError> {
406 if self.nodes.contains_key(&path) {
407 return Err(SnapshotError::DuplicatePath { path });
408 }
409 self.nodes.insert(path, Arc::new(node));
410 Ok(())
411 }
412}
413
414fn snapshot_id(nodes: &BTreeMap<VPath, Arc<SnapshotNode>>) -> SnapshotId {
415 let mut canonical = Vec::new();
416 for (path, node) in nodes {
417 encode_path(path, &mut canonical);
418 node.state.encode_canonical(&mut canonical);
419 }
420 SnapshotId::digest_manifest(&canonical)
421}
422
423struct SnapshotInner {
424 id: SnapshotId,
425 nodes: BTreeMap<VPath, Arc<SnapshotNode>>,
426 children: BTreeMap<VPath, BTreeSet<VPath>>,
427 store: BlobStore,
428}
429
430#[derive(Clone)]
432pub struct BaseSnapshot {
433 inner: Arc<SnapshotInner>,
434}
435
436impl BaseSnapshot {
437 #[must_use]
439 pub fn id(&self) -> SnapshotId {
440 self.inner.id
441 }
442
443 #[must_use]
445 pub fn len(&self) -> usize {
446 self.inner.nodes.len()
447 }
448
449 #[must_use]
451 pub fn is_empty(&self) -> bool {
452 self.len() == 1
453 }
454
455 #[must_use]
457 pub fn metrics(&self) -> SnapshotMetrics {
458 let lazy_nodes = self
459 .inner
460 .nodes
461 .values()
462 .filter(|node| node.is_lazy())
463 .count();
464 let materialized_content_nodes = self
465 .inner
466 .nodes
467 .values()
468 .filter(|node| node.is_materialized())
469 .count();
470 SnapshotMetrics {
471 node_count: self.len(),
472 lazy_content_nodes: lazy_nodes,
473 materialized_content_nodes,
474 }
475 }
476
477 fn node(&self, path: &VPath) -> Option<Arc<SnapshotNode>> {
478 self.inner.nodes.get(path).cloned()
479 }
480
481 fn direct_children(&self, path: &VPath) -> impl Iterator<Item = VPath> + '_ {
482 self.inner
483 .children
484 .get(path)
485 .into_iter()
486 .flat_map(|children| children.iter().cloned())
487 }
488
489 fn subtree_paths(&self, root: &VPath) -> Vec<VPath> {
490 if !self.inner.nodes.contains_key(root) {
491 return Vec::new();
492 }
493 let mut paths = Vec::new();
494 let mut pending = vec![root.clone()];
495 while let Some(path) = pending.pop() {
496 if let Some(children) = self.inner.children.get(&path) {
497 pending.extend(children.iter().rev().cloned());
498 }
499 paths.push(path);
500 }
501 paths.sort_unstable();
502 paths
503 }
504
505 fn directory_digest(&self, path: &VPath) -> DirectoryDigest {
506 let children = self.direct_children(path).collect::<Vec<_>>();
507 DirectoryDigest::digest_entries(
508 children
509 .iter()
510 .map(|child| (child, self.inner.nodes[child].expected_state())),
511 )
512 }
513
514 fn store(&self) -> &BlobStore {
515 &self.inner.store
516 }
517}
518
519#[derive(Debug)]
521#[non_exhaustive]
522pub enum SnapshotError {
523 Store(BlobStoreError),
525 DuplicatePath {
527 path: VPath,
529 },
530 MissingParent {
532 path: VPath,
534 parent: VPath,
536 },
537 ParentNotDirectory {
539 path: VPath,
541 parent: VPath,
543 },
544 LazyDirectory {
546 path: VPath,
548 },
549 ExpectedDirectoryStamp {
551 path: VPath,
553 stamp: FileStamp,
555 },
556 ContentUnavailable {
558 path: VPath,
560 kind: NodeKind,
562 },
563 ContentLoad {
565 path: VPath,
567 source: ContentLoadError,
569 },
570 StaleContent {
572 path: VPath,
574 expected: Box<FileStamp>,
576 before: Box<FileStamp>,
578 after: Box<FileStamp>,
580 },
581 ContentSizeMismatch {
583 path: VPath,
585 expected: u64,
587 actual: usize,
589 },
590 LazyStatePoisoned {
592 path: VPath,
594 },
595}
596
597impl fmt::Display for SnapshotError {
598 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
599 match self {
600 Self::Store(source) => write!(formatter, "blob store failure: {source}"),
601 Self::DuplicatePath { path } => write!(formatter, "duplicate snapshot path: {path}"),
602 Self::MissingParent { path, parent } => {
603 write!(
604 formatter,
605 "snapshot path {path} has missing parent {parent}"
606 )
607 }
608 Self::ParentNotDirectory { path, parent } => {
609 write!(
610 formatter,
611 "snapshot path {path} has non-directory parent {parent}"
612 )
613 }
614 Self::LazyDirectory { path } => {
615 write!(formatter, "directory {path} cannot have lazy byte content")
616 }
617 Self::ExpectedDirectoryStamp { path, stamp } => write!(
618 formatter,
619 "expected a directory stamp for {path}, got {:?}",
620 stamp.kind
621 ),
622 Self::ContentUnavailable { path, kind } => {
623 write!(formatter, "{path} has no readable byte content ({kind:?})")
624 }
625 Self::ContentLoad { path, source } => {
626 write!(
627 formatter,
628 "failed to capture snapshot content for {path}: {source}"
629 )
630 }
631 Self::StaleContent { path, .. } => {
632 write!(formatter, "snapshot content changed while reading {path}")
633 }
634 Self::ContentSizeMismatch {
635 path,
636 expected,
637 actual,
638 } => write!(
639 formatter,
640 "snapshot content size changed for {path}: expected {expected}, got {actual}"
641 ),
642 Self::LazyStatePoisoned { path } => {
643 write!(formatter, "lazy snapshot state was poisoned for {path}")
644 }
645 }
646 }
647}
648
649impl Error for SnapshotError {
650 fn source(&self) -> Option<&(dyn Error + 'static)> {
651 match self {
652 Self::Store(source) => Some(source),
653 Self::ContentLoad { source, .. } => Some(source),
654 Self::DuplicatePath { .. }
655 | Self::MissingParent { .. }
656 | Self::ParentNotDirectory { .. }
657 | Self::LazyDirectory { .. }
658 | Self::ExpectedDirectoryStamp { .. }
659 | Self::ContentUnavailable { .. }
660 | Self::StaleContent { .. }
661 | Self::ContentSizeMismatch { .. }
662 | Self::LazyStatePoisoned { .. } => None,
663 }
664 }
665}
666
667#[derive(Clone, Copy, Debug, Eq, PartialEq)]
669pub struct SnapshotMetrics {
670 pub node_count: usize,
672 pub lazy_content_nodes: usize,
674 pub materialized_content_nodes: usize,
676}
677
678#[derive(Clone)]
679struct ResolvedNode {
680 node: Arc<SnapshotNode>,
681 base_origin: Option<VPath>,
682}
683
684#[derive(Clone)]
685enum OverlayEntry {
686 Present(ResolvedNode),
687 Tombstone,
688}
689
690#[derive(Clone, Debug, Default, Eq, PartialEq)]
692pub struct ReadObservation {
693 pub metadata: Option<Option<NodeState>>,
695 pub content: Option<BlobId>,
697 pub directory: Option<DirectoryDigest>,
699}
700
701#[derive(Clone, Copy, Debug, Eq, PartialEq)]
703pub struct WritePrecondition {
704 pub expected: Option<NodeState>,
706}
707
708#[derive(Clone, Copy, Debug, Eq, PartialEq)]
710#[non_exhaustive]
711pub enum EffectOrigin {
712 VirtualFs,
714 MontyOsCall,
716}
717
718#[derive(Clone, Debug, Eq, PartialEq)]
720#[non_exhaustive]
721pub enum Effect {
722 MetadataRead {
724 path: VPath,
726 state: Option<NodeState>,
728 },
729 ContentRead {
731 path: VPath,
733 blob: BlobId,
735 },
736 DirectoryRead {
738 path: VPath,
740 digest: DirectoryDigest,
742 },
743 Create {
745 path: VPath,
747 after: NodeState,
749 },
750 ModifyContent {
752 path: VPath,
754 before: NodeState,
756 after: NodeState,
758 },
759 Delete {
761 path: VPath,
763 before: NodeState,
765 },
766 Rename {
768 from: VPath,
770 to: VPath,
772 before: NodeState,
774 after: NodeState,
776 },
777}
778
779#[derive(Clone, Debug, Eq, PartialEq)]
781pub struct EffectEvent {
782 pub sequence: u64,
784 pub origin: EffectOrigin,
786 pub effect: Effect,
788}
789
790#[derive(Clone, Debug, Eq, PartialEq)]
792pub struct CanonicalDiff {
793 entries: Vec<DiffEntry>,
794 digest: DiffDigest,
795 metrics: CanonicalDiffMetrics,
796}
797
798impl CanonicalDiff {
799 pub fn from_entries(entries: Vec<DiffEntry>) -> Result<Self, VfsError> {
809 let mut previous: Option<&VPath> = None;
810 let mut materialized_after_bytes = 0_u64;
811 for entry in &entries {
812 if entry.path.is_root() {
813 return Err(VfsError::InvalidCanonicalDiff {
814 path: Some(entry.path.clone()),
815 reason: "the workspace root cannot appear in a diff",
816 });
817 }
818 if previous.is_some_and(|path| path >= &entry.path) {
819 return Err(VfsError::InvalidCanonicalDiff {
820 path: Some(entry.path.clone()),
821 reason: "diff paths are not strictly ordered",
822 });
823 }
824 if entry.before == entry.after {
825 return Err(VfsError::InvalidCanonicalDiff {
826 path: Some(entry.path.clone()),
827 reason: "diff entry does not change state",
828 });
829 }
830 if entry.kind != classify_diff(entry.before, entry.after) {
831 return Err(VfsError::InvalidCanonicalDiff {
832 path: Some(entry.path.clone()),
833 reason: "diff kind does not match before and after states",
834 });
835 }
836 materialized_after_bytes =
837 materialized_after_bytes.saturating_add(entry.after.map_or(0, NodeState::size));
838 previous = Some(&entry.path);
839 }
840 let mut canonical = Vec::new();
841 for entry in &entries {
842 encode_diff_entry(entry, &mut canonical);
843 }
844 Ok(Self {
845 metrics: CanonicalDiffMetrics {
846 candidate_paths: entries.len(),
847 expanded_delete_paths: 0,
848 changed_paths: entries.len(),
849 materialized_after_bytes,
850 },
851 digest: DiffDigest::digest_canonical(&canonical),
852 entries,
853 })
854 }
855
856 #[must_use]
858 pub fn entries(&self) -> &[DiffEntry] {
859 &self.entries
860 }
861
862 #[must_use]
864 pub const fn digest(&self) -> DiffDigest {
865 self.digest
866 }
867
868 #[must_use]
870 pub fn is_empty(&self) -> bool {
871 self.entries.is_empty()
872 }
873
874 #[must_use]
876 pub const fn metrics(&self) -> CanonicalDiffMetrics {
877 self.metrics
878 }
879}
880
881#[derive(Clone, Copy, Debug, Eq, PartialEq)]
883pub struct CanonicalDiffMetrics {
884 pub candidate_paths: usize,
886 pub expanded_delete_paths: usize,
888 pub changed_paths: usize,
890 pub materialized_after_bytes: u64,
892}
893
894pub struct VirtualFs {
896 base: BaseSnapshot,
897 overlay: BTreeMap<VPath, OverlayEntry>,
898 effects: Vec<EffectEvent>,
899 read_set: BTreeMap<VPath, ReadObservation>,
900 write_set: BTreeMap<VPath, WritePrecondition>,
901 next_sequence: u64,
902 effect_origin: EffectOrigin,
903}
904
905impl VirtualFs {
906 #[must_use]
908 pub fn new(base: BaseSnapshot) -> Self {
909 Self {
910 base,
911 overlay: BTreeMap::new(),
912 effects: Vec::new(),
913 read_set: BTreeMap::new(),
914 write_set: BTreeMap::new(),
915 next_sequence: 0,
916 effect_origin: EffectOrigin::VirtualFs,
917 }
918 }
919
920 #[must_use]
922 pub fn base_node_count(&self) -> usize {
923 self.base.len()
924 }
925
926 pub fn with_effect_origin<T>(
932 &mut self,
933 origin: EffectOrigin,
934 operation: impl FnOnce(&mut Self) -> T,
935 ) -> T {
936 let previous = self.effect_origin;
937 self.effect_origin = origin;
938 let result = operation(self);
939 self.effect_origin = previous;
940 result
941 }
942
943 #[must_use]
945 pub fn base_snapshot_id(&self) -> SnapshotId {
946 self.base.id()
947 }
948
949 pub fn exists(&mut self, path: &VPath) -> bool {
951 let state = self.resolve(path).map(|resolved| resolved.node.state());
952 self.record_metadata_dependency(path);
953 self.push_effect(Effect::MetadataRead {
954 path: path.clone(),
955 state,
956 });
957 state.is_some()
958 }
959
960 pub fn metadata(&mut self, path: &VPath) -> Result<NodeState, VfsError> {
966 let state = self.resolve(path).map(|resolved| resolved.node.state());
967 self.record_metadata_dependency(path);
968 self.push_effect(Effect::MetadataRead {
969 path: path.clone(),
970 state,
971 });
972 state.ok_or_else(|| VfsError::NotFound { path: path.clone() })
973 }
974
975 pub fn read(&mut self, path: &VPath) -> Result<Vec<u8>, VfsError> {
982 let resolved = self
983 .resolve(path)
984 .ok_or_else(|| VfsError::NotFound { path: path.clone() })?;
985 if resolved.node.state.kind() != NodeKind::File {
986 return Err(VfsError::NotFile {
987 path: path.clone(),
988 actual: resolved.node.state.kind(),
989 });
990 }
991 let (blob, bytes) = resolved.node.read(path, self.base.store())?;
992 if let Some(origin) = resolved.base_origin {
993 let observation = self.read_set.entry(origin).or_default();
994 observation.content.get_or_insert(blob);
995 }
996 self.push_effect(Effect::ContentRead {
997 path: path.clone(),
998 blob,
999 });
1000 Ok(bytes)
1001 }
1002
1003 pub fn read_link(&mut self, path: &VPath) -> Result<Vec<u8>, VfsError> {
1009 let resolved = self
1010 .resolve(path)
1011 .ok_or_else(|| VfsError::NotFound { path: path.clone() })?;
1012 if resolved.node.state.kind() != NodeKind::Symlink {
1013 return Err(VfsError::NotSymlink {
1014 path: path.clone(),
1015 actual: resolved.node.state.kind(),
1016 });
1017 }
1018 let (blob, bytes) = resolved.node.read(path, self.base.store())?;
1019 if let Some(origin) = resolved.base_origin {
1020 let observation = self.read_set.entry(origin).or_default();
1021 observation.content.get_or_insert(blob);
1022 }
1023 self.push_effect(Effect::ContentRead {
1024 path: path.clone(),
1025 blob,
1026 });
1027 Ok(bytes)
1028 }
1029
1030 pub fn read_dir(&mut self, path: &VPath) -> Result<Vec<VPath>, VfsError> {
1036 self.require_directory(path)?;
1037 self.record_metadata_dependency(path);
1038 let children = self.visible_direct_children(path);
1039 let digest = self.listing_digest(&children);
1040 if self.base.node(path).is_some() {
1041 self.read_set.entry(path.clone()).or_default().directory =
1042 Some(self.base.directory_digest(path));
1043 }
1044 self.push_effect(Effect::DirectoryRead {
1045 path: path.clone(),
1046 digest,
1047 });
1048 Ok(children)
1049 }
1050
1051 pub fn write(&mut self, path: &VPath, bytes: &[u8]) -> Result<(), VfsError> {
1058 Self::ensure_mutable_path(path)?;
1059 self.require_parent_directory(path)?;
1060 let before = self.resolve(path).map(|resolved| resolved.node.state());
1061 if let Some(state) = before
1062 && state.kind() != NodeKind::File
1063 {
1064 return Err(VfsError::NotFile {
1065 path: path.clone(),
1066 actual: state.kind(),
1067 });
1068 }
1069
1070 let blob = self.base.store().put(bytes)?;
1071 let mode = before.map_or(DEFAULT_FILE_MODE, NodeState::mode);
1072 let node = Arc::new(SnapshotNode::materialized(
1073 NodeKind::File,
1074 blob,
1075 bytes.len() as u64,
1076 mode,
1077 ));
1078 let after = node.state();
1079 self.record_write_precondition(path);
1080 self.overlay.insert(
1081 path.clone(),
1082 OverlayEntry::Present(ResolvedNode {
1083 node,
1084 base_origin: None,
1085 }),
1086 );
1087 self.push_effect(match before {
1088 Some(before) => Effect::ModifyContent {
1089 path: path.clone(),
1090 before,
1091 after,
1092 },
1093 None => Effect::Create {
1094 path: path.clone(),
1095 after,
1096 },
1097 });
1098 Ok(())
1099 }
1100
1101 pub fn append(&mut self, path: &VPath, suffix: &[u8]) -> Result<(), VfsError> {
1107 let mut bytes = self.read(path)?;
1108 bytes.extend_from_slice(suffix);
1109 self.write(path, &bytes)
1110 }
1111
1112 pub fn mkdir(&mut self, path: &VPath, mode: u32) -> Result<(), VfsError> {
1118 Self::ensure_mutable_path(path)?;
1119 self.require_parent_directory(path)?;
1120 if self.resolve(path).is_some() {
1121 return Err(VfsError::AlreadyExists { path: path.clone() });
1122 }
1123 let node = Arc::new(SnapshotNode::directory(platform_directory_mode(mode)));
1124 let after = node.state();
1125 self.record_write_precondition(path);
1126 self.overlay.insert(
1127 path.clone(),
1128 OverlayEntry::Present(ResolvedNode {
1129 node,
1130 base_origin: None,
1131 }),
1132 );
1133 self.push_effect(Effect::Create {
1134 path: path.clone(),
1135 after,
1136 });
1137 Ok(())
1138 }
1139
1140 pub fn unlink(&mut self, path: &VPath) -> Result<(), VfsError> {
1146 Self::ensure_mutable_path(path)?;
1147 let before = self
1148 .resolve(path)
1149 .map(|resolved| resolved.node.state())
1150 .ok_or_else(|| VfsError::NotFound { path: path.clone() })?;
1151 if before.kind() == NodeKind::Directory {
1152 return Err(VfsError::IsDirectory { path: path.clone() });
1153 }
1154 self.record_write_precondition(path);
1155 self.overlay.insert(path.clone(), OverlayEntry::Tombstone);
1156 self.push_effect(Effect::Delete {
1157 path: path.clone(),
1158 before,
1159 });
1160 Ok(())
1161 }
1162
1163 pub fn rmdir(&mut self, path: &VPath) -> Result<(), VfsError> {
1169 Self::ensure_mutable_path(path)?;
1170 let before = self.require_directory(path)?;
1171 let children = self.read_dir(path)?;
1172 if !children.is_empty() {
1173 return Err(VfsError::DirectoryNotEmpty { path: path.clone() });
1174 }
1175 self.record_write_precondition(path);
1176 self.overlay.insert(path.clone(), OverlayEntry::Tombstone);
1177 self.push_effect(Effect::Delete {
1178 path: path.clone(),
1179 before,
1180 });
1181 Ok(())
1182 }
1183
1184 pub fn remove_tree(&mut self, path: &VPath) -> Result<(), VfsError> {
1190 Self::ensure_mutable_path(path)?;
1191 let nodes = self.visible_subtree(path);
1192 if nodes.is_empty() {
1193 return Err(VfsError::NotFound { path: path.clone() });
1194 }
1195 for (node_path, resolved) in nodes.iter().rev() {
1196 self.record_write_precondition(node_path);
1197 self.push_effect(Effect::Delete {
1198 path: node_path.clone(),
1199 before: resolved.node.state(),
1200 });
1201 }
1202 for (node_path, _) in nodes {
1203 self.overlay.insert(node_path, OverlayEntry::Tombstone);
1204 }
1205 Ok(())
1206 }
1207
1208 pub fn rename(&mut self, from: &VPath, to: &VPath) -> Result<(), VfsError> {
1218 Self::ensure_mutable_path(from)?;
1219 Self::ensure_mutable_path(to)?;
1220 if from == to {
1221 return Ok(());
1222 }
1223 if to.is_within(from) || from.is_within(to) {
1224 return Err(VfsError::InvalidRename {
1225 from: from.clone(),
1226 to: to.clone(),
1227 });
1228 }
1229 self.require_parent_directory(to)?;
1230 let source = self
1231 .resolve(from)
1232 .ok_or_else(|| VfsError::NotFound { path: from.clone() })?;
1233 let source_state = source.node.state();
1234
1235 if let Some(destination) = self.resolve(to) {
1236 let destination_state = destination.node.state();
1237 match (source_state.kind(), destination_state.kind()) {
1238 (NodeKind::Directory, NodeKind::Directory)
1239 if !self.visible_direct_children(to).is_empty() =>
1240 {
1241 return Err(VfsError::DirectoryNotEmpty { path: to.clone() });
1242 }
1243 (NodeKind::Directory, NodeKind::Directory) => {}
1244 (NodeKind::Directory, _) | (_, NodeKind::Directory) => {
1245 return Err(VfsError::RenameTypeMismatch {
1246 from: from.clone(),
1247 to: to.clone(),
1248 });
1249 }
1250 _ => {}
1251 }
1252 self.record_write_precondition(to);
1253 self.overlay.insert(to.clone(), OverlayEntry::Tombstone);
1254 }
1255
1256 let moving = self.visible_subtree(from);
1257 for (source_path, _) in &moving {
1258 self.record_write_precondition(source_path);
1259 }
1260 for (source_path, _) in &moving {
1261 let destination_path =
1262 source_path
1263 .rebase(from, to)?
1264 .ok_or_else(|| VfsError::InvalidRename {
1265 from: from.clone(),
1266 to: to.clone(),
1267 })?;
1268 self.record_write_precondition(&destination_path);
1269 }
1270
1271 for (source_path, _) in &moving {
1272 self.overlay
1273 .insert(source_path.clone(), OverlayEntry::Tombstone);
1274 }
1275 for (source_path, resolved) in moving {
1276 let destination_path =
1277 source_path
1278 .rebase(from, to)?
1279 .ok_or_else(|| VfsError::InvalidRename {
1280 from: from.clone(),
1281 to: to.clone(),
1282 })?;
1283 self.overlay
1284 .insert(destination_path, OverlayEntry::Present(resolved));
1285 }
1286 self.push_effect(Effect::Rename {
1287 from: from.clone(),
1288 to: to.clone(),
1289 before: source_state,
1290 after: source_state,
1291 });
1292 Ok(())
1293 }
1294
1295 pub fn canonical_diff(&self) -> Result<CanonicalDiff, VfsError> {
1304 let mut candidates = BTreeSet::new();
1305 let mut expanded_delete_paths = BTreeSet::new();
1306 for (path, entry) in &self.overlay {
1307 candidates.insert(path.clone());
1308 if matches!(entry, OverlayEntry::Tombstone) {
1309 expanded_delete_paths.extend(self.base.subtree_paths(path));
1310 }
1311 }
1312 candidates.extend(expanded_delete_paths.iter().cloned());
1313
1314 let candidates: Vec<VPath> = candidates
1315 .into_iter()
1316 .filter(|path| !path.is_root())
1317 .collect();
1318 let candidate_paths = candidates.len();
1319 let mut after_states = BTreeMap::new();
1320 let mut materialized_after_bytes = 0_u64;
1321 for path in &candidates {
1322 let after = match self.resolve(path) {
1323 Some(resolved) => Some(resolved.node.materialized_state(path, self.base.store())?),
1324 None => None,
1325 };
1326 materialized_after_bytes =
1327 materialized_after_bytes.saturating_add(after.map_or(0, NodeState::size));
1328 after_states.insert(path.clone(), after);
1329 }
1330
1331 let mut entries = Vec::new();
1332 for path in candidates {
1333 let before = self.base.node(&path).map(|node| node.state());
1334 let after = after_states[&path];
1335 if before == after {
1336 continue;
1337 }
1338 let kind = classify_diff(before, after);
1339 entries.push(DiffEntry {
1340 path,
1341 before,
1342 after,
1343 kind,
1344 });
1345 }
1346
1347 let mut canonical = Vec::new();
1348 for entry in &entries {
1349 encode_diff_entry(entry, &mut canonical);
1350 }
1351 Ok(CanonicalDiff {
1352 metrics: CanonicalDiffMetrics {
1353 candidate_paths,
1354 expanded_delete_paths: expanded_delete_paths.len(),
1355 changed_paths: entries.len(),
1356 materialized_after_bytes,
1357 },
1358 entries,
1359 digest: DiffDigest::digest_canonical(&canonical),
1360 })
1361 }
1362
1363 #[must_use]
1365 pub fn effects(&self) -> &[EffectEvent] {
1366 &self.effects
1367 }
1368
1369 #[must_use]
1371 pub fn read_set(&self) -> &BTreeMap<VPath, ReadObservation> {
1372 &self.read_set
1373 }
1374
1375 #[must_use]
1377 pub fn write_set(&self) -> &BTreeMap<VPath, WritePrecondition> {
1378 &self.write_set
1379 }
1380
1381 #[must_use]
1383 pub fn metrics(&self) -> VfsMetrics {
1384 let overlay_bytes = self
1385 .overlay
1386 .values()
1387 .filter_map(|entry| match entry {
1388 OverlayEntry::Present(resolved) => Some(resolved.node.state().size()),
1389 OverlayEntry::Tombstone => None,
1390 })
1391 .sum();
1392 VfsMetrics {
1393 overlay_entries: self.overlay.len(),
1394 overlay_bytes,
1395 effect_events: self.effects.len(),
1396 read_dependencies: self.read_set.len(),
1397 write_preconditions: self.write_set.len(),
1398 }
1399 }
1400
1401 pub fn materialized_final_state(&self) -> Result<BTreeMap<VPath, NodeState>, VfsError> {
1410 let mut candidates: BTreeSet<VPath> = self.base.inner.nodes.keys().cloned().collect();
1411 candidates.extend(self.overlay.keys().cloned());
1412 let mut final_state = BTreeMap::new();
1413 for path in candidates {
1414 if let Some(resolved) = self.resolve(&path) {
1415 let state = resolved.node.materialized_state(&path, self.base.store())?;
1416 final_state.insert(path, state);
1417 }
1418 }
1419 Ok(final_state)
1420 }
1421
1422 fn resolve(&self, path: &VPath) -> Option<ResolvedNode> {
1423 let mut ancestor = path.parent();
1424 while let Some(current) = ancestor {
1425 match self.overlay.get(¤t) {
1426 Some(OverlayEntry::Tombstone) => return None,
1427 Some(OverlayEntry::Present(resolved))
1428 if resolved.node.state.kind() != NodeKind::Directory =>
1429 {
1430 return None;
1431 }
1432 Some(OverlayEntry::Present(_)) | None => {}
1433 }
1434 ancestor = current.parent();
1435 }
1436 if let Some(entry) = self.overlay.get(path) {
1437 return match entry {
1438 OverlayEntry::Present(resolved) => Some(resolved.clone()),
1439 OverlayEntry::Tombstone => None,
1440 };
1441 }
1442 self.base.node(path).map(|node| ResolvedNode {
1443 node,
1444 base_origin: Some(path.clone()),
1445 })
1446 }
1447
1448 fn visible_direct_children(&self, path: &VPath) -> Vec<VPath> {
1449 let mut candidates: BTreeSet<VPath> = self.base.direct_children(path).collect();
1450 candidates.extend(
1451 self.overlay
1452 .keys()
1453 .filter(|candidate| candidate.parent().as_ref() == Some(path))
1454 .cloned(),
1455 );
1456 candidates
1457 .into_iter()
1458 .filter(|candidate| self.resolve(candidate).is_some())
1459 .collect()
1460 }
1461
1462 fn visible_subtree(&self, path: &VPath) -> Vec<(VPath, ResolvedNode)> {
1463 let mut candidates: BTreeSet<VPath> = self.base.subtree_paths(path).into_iter().collect();
1464 candidates.extend(
1465 self.overlay
1466 .keys()
1467 .filter(|candidate| candidate.is_within(path))
1468 .cloned(),
1469 );
1470 candidates
1471 .into_iter()
1472 .filter_map(|candidate| self.resolve(&candidate).map(|node| (candidate, node)))
1473 .collect()
1474 }
1475
1476 fn listing_digest(&self, children: &[VPath]) -> DirectoryDigest {
1477 DirectoryDigest::digest_entries(children.iter().map(|child| {
1478 (
1479 child,
1480 self.resolve(child)
1481 .expect("visible child resolves")
1482 .node
1483 .state(),
1484 )
1485 }))
1486 }
1487
1488 fn require_directory(&self, path: &VPath) -> Result<NodeState, VfsError> {
1489 let state = self
1490 .resolve(path)
1491 .map(|resolved| resolved.node.state())
1492 .ok_or_else(|| VfsError::NotFound { path: path.clone() })?;
1493 if state.kind() != NodeKind::Directory {
1494 return Err(VfsError::NotDirectory {
1495 path: path.clone(),
1496 actual: state.kind(),
1497 });
1498 }
1499 Ok(state)
1500 }
1501
1502 fn require_parent_directory(&self, path: &VPath) -> Result<(), VfsError> {
1503 let parent = path.parent().ok_or(VfsError::RootMutation)?;
1504 self.require_directory(&parent).map(|_| ())
1505 }
1506
1507 fn ensure_mutable_path(path: &VPath) -> Result<(), VfsError> {
1508 if path.is_root() {
1509 Err(VfsError::RootMutation)
1510 } else {
1511 Ok(())
1512 }
1513 }
1514
1515 fn record_metadata_dependency(&mut self, path: &VPath) {
1516 let expected = self.base.node(path).map(|node| node.expected_state());
1517 self.read_set
1518 .entry(path.clone())
1519 .or_default()
1520 .metadata
1521 .get_or_insert(expected);
1522 }
1523
1524 fn record_write_precondition(&mut self, path: &VPath) {
1525 let expected = self.base.node(path).map(|node| node.expected_state());
1526 self.write_set
1527 .entry(path.clone())
1528 .or_insert(WritePrecondition { expected });
1529 if let Some(parent) = path.parent() {
1530 self.record_metadata_dependency(&parent);
1531 }
1532 }
1533
1534 fn push_effect(&mut self, effect: Effect) {
1535 let sequence = self.next_sequence;
1536 self.next_sequence = self
1537 .next_sequence
1538 .checked_add(1)
1539 .expect("effect sequence cannot wrap within a bounded transaction");
1540 self.effects.push(EffectEvent {
1541 sequence,
1542 origin: self.effect_origin,
1543 effect,
1544 });
1545 }
1546}
1547
1548fn classify_diff(before: Option<NodeState>, after: Option<NodeState>) -> DiffKind {
1549 match (before, after) {
1550 (None, Some(_)) => DiffKind::Create,
1551 (Some(_), None) => DiffKind::Delete,
1552 (Some(before), Some(after)) if before.content_equivalent(after) => DiffKind::MetadataChange,
1553 (Some(_), Some(_)) => DiffKind::Modify,
1554 (None, None) => unreachable!("equal missing states are removed before classification"),
1555 }
1556}
1557
1558fn encode_path(path: &VPath, output: &mut Vec<u8>) {
1559 let bytes = path.as_str().as_bytes();
1560 output.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
1561 output.extend_from_slice(bytes);
1562}
1563
1564fn encode_optional_state(state: Option<NodeState>, output: &mut Vec<u8>) {
1565 match state {
1566 Some(state) => {
1567 output.push(1);
1568 state.encode_canonical(output);
1569 }
1570 None => output.push(0),
1571 }
1572}
1573
1574fn encode_diff_entry(entry: &DiffEntry, output: &mut Vec<u8>) {
1575 encode_path(&entry.path, output);
1576 output.push(match entry.kind {
1577 DiffKind::Create => 1,
1578 DiffKind::Delete => 2,
1579 DiffKind::Modify => 3,
1580 DiffKind::MetadataChange => 4,
1581 });
1582 encode_optional_state(entry.before, output);
1583 encode_optional_state(entry.after, output);
1584}
1585
1586#[derive(Debug)]
1588#[non_exhaustive]
1589pub enum VfsError {
1590 Snapshot(SnapshotError),
1592 Store(BlobStoreError),
1594 Path(VPathError),
1596 NotFound {
1598 path: VPath,
1600 },
1601 AlreadyExists {
1603 path: VPath,
1605 },
1606 NotDirectory {
1608 path: VPath,
1610 actual: NodeKind,
1612 },
1613 NotFile {
1615 path: VPath,
1617 actual: NodeKind,
1619 },
1620 NotSymlink {
1622 path: VPath,
1624 actual: NodeKind,
1626 },
1627 IsDirectory {
1629 path: VPath,
1631 },
1632 DirectoryNotEmpty {
1634 path: VPath,
1636 },
1637 RootMutation,
1639 InvalidRename {
1641 from: VPath,
1643 to: VPath,
1645 },
1646 RenameTypeMismatch {
1648 from: VPath,
1650 to: VPath,
1652 },
1653 InvalidCanonicalDiff {
1655 path: Option<VPath>,
1657 reason: &'static str,
1659 },
1660}
1661
1662impl From<SnapshotError> for VfsError {
1663 fn from(value: SnapshotError) -> Self {
1664 Self::Snapshot(value)
1665 }
1666}
1667
1668impl From<BlobStoreError> for VfsError {
1669 fn from(value: BlobStoreError) -> Self {
1670 Self::Store(value)
1671 }
1672}
1673
1674impl From<VPathError> for VfsError {
1675 fn from(value: VPathError) -> Self {
1676 Self::Path(value)
1677 }
1678}
1679
1680impl fmt::Display for VfsError {
1681 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1682 match self {
1683 Self::Snapshot(source) => write!(formatter, "snapshot failure: {source}"),
1684 Self::Store(source) => write!(formatter, "blob store failure: {source}"),
1685 Self::Path(source) => write!(formatter, "virtual path failure: {source}"),
1686 Self::NotFound { path } => write!(formatter, "virtual path not found: {path}"),
1687 Self::AlreadyExists { path } => write!(formatter, "virtual path exists: {path}"),
1688 Self::NotDirectory { path, actual } => {
1689 write!(
1690 formatter,
1691 "virtual path {path} is not a directory ({actual:?})"
1692 )
1693 }
1694 Self::NotFile { path, actual } => {
1695 write!(formatter, "virtual path {path} is not a file ({actual:?})")
1696 }
1697 Self::NotSymlink { path, actual } => {
1698 write!(
1699 formatter,
1700 "virtual path {path} is not a symlink ({actual:?})"
1701 )
1702 }
1703 Self::IsDirectory { path } => write!(formatter, "virtual path is a directory: {path}"),
1704 Self::DirectoryNotEmpty { path } => {
1705 write!(formatter, "virtual directory is not empty: {path}")
1706 }
1707 Self::RootMutation => formatter.write_str("the virtual root cannot be mutated"),
1708 Self::InvalidRename { from, to } => {
1709 write!(formatter, "rename subtrees overlap: {from} -> {to}")
1710 }
1711 Self::RenameTypeMismatch { from, to } => {
1712 write!(formatter, "rename type mismatch: {from} -> {to}")
1713 }
1714 Self::InvalidCanonicalDiff { path, reason } => match path {
1715 Some(path) => write!(formatter, "invalid canonical diff at {path}: {reason}"),
1716 None => write!(formatter, "invalid canonical diff: {reason}"),
1717 },
1718 }
1719 }
1720}
1721
1722impl Error for VfsError {
1723 fn source(&self) -> Option<&(dyn Error + 'static)> {
1724 match self {
1725 Self::Snapshot(source) => Some(source),
1726 Self::Store(source) => Some(source),
1727 Self::Path(source) => Some(source),
1728 Self::NotFound { .. }
1729 | Self::AlreadyExists { .. }
1730 | Self::NotDirectory { .. }
1731 | Self::NotFile { .. }
1732 | Self::NotSymlink { .. }
1733 | Self::IsDirectory { .. }
1734 | Self::DirectoryNotEmpty { .. }
1735 | Self::RootMutation
1736 | Self::InvalidRename { .. }
1737 | Self::RenameTypeMismatch { .. }
1738 | Self::InvalidCanonicalDiff { .. } => None,
1739 }
1740 }
1741}
1742
1743#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1745pub struct VfsMetrics {
1746 pub overlay_entries: usize,
1748 pub overlay_bytes: u64,
1750 pub effect_events: usize,
1752 pub read_dependencies: usize,
1754 pub write_preconditions: usize,
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760 use std::fs;
1761 use std::io;
1762 use std::path::PathBuf;
1763 use std::sync::Barrier;
1764 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
1765
1766 use vsh_types::PlatformFileId;
1767
1768 use super::*;
1769
1770 static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
1771
1772 struct TestDirectory(PathBuf);
1773
1774 impl Drop for TestDirectory {
1775 fn drop(&mut self) {
1776 let _ = fs::remove_dir_all(&self.0);
1777 }
1778 }
1779
1780 fn test_store(name: &str) -> (TestDirectory, BlobStore) {
1781 let sequence = TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
1782 let root = std::env::temp_dir().join(format!(
1783 "vsh-vfs-test-{}-{sequence}-{name}",
1784 std::process::id()
1785 ));
1786 let guard = TestDirectory(root.clone());
1787 let store = BlobStore::open(root).unwrap();
1788 (guard, store)
1789 }
1790
1791 fn path(value: &str) -> VPath {
1792 VPath::parse(value).unwrap()
1793 }
1794
1795 fn stamp(kind: NodeKind, size: usize, identity: u64) -> FileStamp {
1796 FileStamp {
1797 kind,
1798 size: size as u64,
1799 mode: 0o644,
1800 mtime_ns: 1_700_000_000_000_000_000,
1801 ctime_ns: Some(1_700_000_000_000_000_001),
1802 file_id: PlatformFileId {
1803 high: 7,
1804 low: identity,
1805 },
1806 }
1807 }
1808
1809 fn fixture_snapshot(store: BlobStore) -> BaseSnapshot {
1810 let mut builder = SnapshotBuilder::new(store);
1811 builder.add_directory(path("src"), 0o755).unwrap();
1812 builder.add_directory(path("src/nested"), 0o755).unwrap();
1813 builder.add_directory(path("empty"), 0o755).unwrap();
1814 builder
1815 .add_file(path("src/a.txt"), b"alpha", 0o644)
1816 .unwrap();
1817 builder
1818 .add_file(path("src/nested/b.txt"), b"beta", 0o600)
1819 .unwrap();
1820 builder
1821 .add_symlink(path("opaque-link"), b"../../outside", 0o777)
1822 .unwrap();
1823 builder.build().unwrap()
1824 }
1825
1826 #[test]
1827 fn public_snapshot_errors_have_stable_messages_and_sources() {
1828 let expected = stamp(NodeKind::File, 1, 1);
1829 let changed = FileStamp {
1830 size: 2,
1831 ..expected
1832 };
1833 let blob_error = || BlobStoreError::Io {
1834 operation: "read",
1835 path: PathBuf::from("blob"),
1836 source: io::Error::other("test"),
1837 };
1838 let snapshot_errors = [
1839 SnapshotError::Store(blob_error()),
1840 SnapshotError::DuplicatePath { path: path("file") },
1841 SnapshotError::MissingParent {
1842 path: path("dir/file"),
1843 parent: path("dir"),
1844 },
1845 SnapshotError::ParentNotDirectory {
1846 path: path("dir/file"),
1847 parent: path("dir"),
1848 },
1849 SnapshotError::LazyDirectory { path: path("dir") },
1850 SnapshotError::ExpectedDirectoryStamp {
1851 path: path("file"),
1852 stamp: expected,
1853 },
1854 SnapshotError::ContentUnavailable {
1855 path: path("dir"),
1856 kind: NodeKind::Directory,
1857 },
1858 SnapshotError::ContentLoad {
1859 path: path("file"),
1860 source: ContentLoadError::new("test"),
1861 },
1862 SnapshotError::StaleContent {
1863 path: path("file"),
1864 expected: Box::new(expected),
1865 before: Box::new(expected),
1866 after: Box::new(changed),
1867 },
1868 SnapshotError::ContentSizeMismatch {
1869 path: path("file"),
1870 expected: 1,
1871 actual: 2,
1872 },
1873 SnapshotError::LazyStatePoisoned { path: path("file") },
1874 ];
1875 for error in snapshot_errors {
1876 assert!(!error.to_string().is_empty());
1877 assert_eq!(
1878 Error::source(&error).is_some(),
1879 matches!(
1880 error,
1881 SnapshotError::Store(_) | SnapshotError::ContentLoad { .. }
1882 )
1883 );
1884 }
1885 }
1886
1887 #[test]
1888 fn public_vfs_errors_have_stable_messages_and_sources() {
1889 let blob_error = || BlobStoreError::Io {
1890 operation: "read",
1891 path: PathBuf::from("blob"),
1892 source: io::Error::other("test"),
1893 };
1894 let vfs_errors = [
1895 VfsError::Snapshot(SnapshotError::DuplicatePath { path: path("file") }),
1896 VfsError::Store(blob_error()),
1897 VfsError::Path(VPath::parse("").unwrap_err()),
1898 VfsError::NotFound { path: path("file") },
1899 VfsError::AlreadyExists { path: path("file") },
1900 VfsError::NotDirectory {
1901 path: path("file"),
1902 actual: NodeKind::File,
1903 },
1904 VfsError::NotFile {
1905 path: path("dir"),
1906 actual: NodeKind::Directory,
1907 },
1908 VfsError::NotSymlink {
1909 path: path("file"),
1910 actual: NodeKind::File,
1911 },
1912 VfsError::IsDirectory { path: path("dir") },
1913 VfsError::DirectoryNotEmpty { path: path("dir") },
1914 VfsError::RootMutation,
1915 VfsError::InvalidRename {
1916 from: path("dir"),
1917 to: path("dir/child"),
1918 },
1919 VfsError::RenameTypeMismatch {
1920 from: path("file"),
1921 to: path("dir"),
1922 },
1923 VfsError::InvalidCanonicalDiff {
1924 path: Some(path("file")),
1925 reason: "test",
1926 },
1927 VfsError::InvalidCanonicalDiff {
1928 path: None,
1929 reason: "test",
1930 },
1931 ];
1932 for error in vfs_errors {
1933 assert!(!error.to_string().is_empty());
1934 assert_eq!(
1935 Error::source(&error).is_some(),
1936 matches!(
1937 error,
1938 VfsError::Snapshot(_) | VfsError::Store(_) | VfsError::Path(_)
1939 )
1940 );
1941 }
1942 }
1943
1944 #[test]
1945 fn snapshot_identity_is_insertion_order_and_store_independent() {
1946 let (_first_guard, first_store) = test_store("snapshot-a");
1947 let (_second_guard, second_store) = test_store("snapshot-b");
1948
1949 let mut first = SnapshotBuilder::new(first_store);
1950 first.add_directory(path("dir"), 0o755).unwrap();
1951 first.add_file(path("dir/file"), b"same", 0o640).unwrap();
1952
1953 let mut second = SnapshotBuilder::new(second_store);
1954 second.add_file(path("dir/file"), b"same", 0o640).unwrap();
1955 second.add_directory(path("dir"), 0o755).unwrap();
1956
1957 let first = first.build().unwrap();
1958 let second = second.build().unwrap();
1959 assert_eq!(first.id(), second.id());
1960 assert_eq!(first.len(), 3);
1961 assert!(!first.is_empty());
1962 }
1963
1964 #[test]
1965 fn snapshot_rejects_missing_or_non_directory_parents() {
1966 let (_guard, store) = test_store("invalid-parent");
1967 let mut missing = SnapshotBuilder::new(store.clone());
1968 missing.add_file(path("missing/file"), b"x", 0o644).unwrap();
1969 assert!(matches!(
1970 missing.build(),
1971 Err(SnapshotError::MissingParent { .. })
1972 ));
1973
1974 let mut non_directory = SnapshotBuilder::new(store);
1975 non_directory.add_file(path("file"), b"x", 0o644).unwrap();
1976 non_directory
1977 .add_file(path("file/child"), b"y", 0o644)
1978 .unwrap();
1979 assert!(matches!(
1980 non_directory.build(),
1981 Err(SnapshotError::ParentNotDirectory { .. })
1982 ));
1983 }
1984
1985 #[test]
1986 fn lazy_content_is_captured_once_and_then_blob_backed() {
1987 let (_guard, store) = test_store("lazy-once");
1988 let expected = stamp(NodeKind::File, 5, 41);
1989 let calls = Arc::new(AtomicUsize::new(0));
1990 let loader_calls = Arc::clone(&calls);
1991 let mut builder = SnapshotBuilder::new(store);
1992 builder
1993 .add_lazy(path("lazy.txt"), expected, move |stamp| {
1994 loader_calls.fetch_add(1, Ordering::Relaxed);
1995 Ok(CapturedContent {
1996 bytes: b"hello".to_vec(),
1997 before: stamp,
1998 after: stamp,
1999 })
2000 })
2001 .unwrap();
2002 let snapshot = builder.build().unwrap();
2003 assert_eq!(
2004 snapshot.metrics(),
2005 SnapshotMetrics {
2006 node_count: 2,
2007 lazy_content_nodes: 1,
2008 materialized_content_nodes: 0,
2009 }
2010 );
2011
2012 let mut vfs = VirtualFs::new(snapshot.clone());
2013 assert_eq!(vfs.read(&path("lazy.txt")).unwrap(), b"hello");
2014 assert_eq!(vfs.read(&path("lazy.txt")).unwrap(), b"hello");
2015 assert_eq!(calls.load(Ordering::Relaxed), 1);
2016 assert_eq!(snapshot.metrics().materialized_content_nodes, 1);
2017 assert!(vfs.read_set()[&path("lazy.txt")].content.is_some());
2018 }
2019
2020 #[test]
2021 fn concurrent_snapshot_readers_share_one_lazy_capture() {
2022 let (_guard, store) = test_store("lazy-concurrent");
2023 let expected = stamp(NodeKind::File, 5, 43);
2024 let calls = Arc::new(AtomicUsize::new(0));
2025 let loader_calls = Arc::clone(&calls);
2026 let mut builder = SnapshotBuilder::new(store);
2027 builder
2028 .add_lazy(path("lazy.txt"), expected, move |stamp| {
2029 loader_calls.fetch_add(1, Ordering::Relaxed);
2030 Ok(CapturedContent {
2031 bytes: b"hello".to_vec(),
2032 before: stamp,
2033 after: stamp,
2034 })
2035 })
2036 .unwrap();
2037 let snapshot = builder.build().unwrap();
2038 let barrier = Arc::new(Barrier::new(8));
2039 let mut threads = Vec::new();
2040 for _ in 0..8 {
2041 let worker_snapshot = snapshot.clone();
2042 let worker_barrier = Arc::clone(&barrier);
2043 threads.push(std::thread::spawn(move || {
2044 let mut vfs = VirtualFs::new(worker_snapshot);
2045 worker_barrier.wait();
2046 vfs.read(&path("lazy.txt")).unwrap()
2047 }));
2048 }
2049
2050 for thread in threads {
2051 assert_eq!(thread.join().unwrap(), b"hello");
2052 }
2053 assert_eq!(calls.load(Ordering::Relaxed), 1);
2054 }
2055
2056 #[test]
2057 fn lazy_capture_fails_closed_on_stamp_or_size_drift() {
2058 let (_guard, store) = test_store("lazy-stale");
2059 let expected = stamp(NodeKind::File, 5, 42);
2060 let mut changed = expected;
2061 changed.mtime_ns += 1;
2062 let mut builder = SnapshotBuilder::new(store);
2063 builder
2064 .add_lazy(path("stale.txt"), expected, move |_| {
2065 Ok(CapturedContent {
2066 bytes: b"hello".to_vec(),
2067 before: expected,
2068 after: changed,
2069 })
2070 })
2071 .unwrap();
2072 let mut vfs = VirtualFs::new(builder.build().unwrap());
2073 assert!(matches!(
2074 vfs.read(&path("stale.txt")),
2075 Err(VfsError::Snapshot(SnapshotError::StaleContent { .. }))
2076 ));
2077
2078 let (_guard, store) = test_store("lazy-size");
2079 let mut builder = SnapshotBuilder::new(store);
2080 builder
2081 .add_lazy(path("size.txt"), expected, move |stamp| {
2082 Ok(CapturedContent {
2083 bytes: b"shorter?".to_vec(),
2084 before: stamp,
2085 after: stamp,
2086 })
2087 })
2088 .unwrap();
2089 let mut vfs = VirtualFs::new(builder.build().unwrap());
2090 assert!(matches!(
2091 vfs.read(&path("size.txt")),
2092 Err(VfsError::Snapshot(
2093 SnapshotError::ContentSizeMismatch { .. }
2094 ))
2095 ));
2096 }
2097
2098 #[test]
2099 fn virtual_operations_never_mutate_the_base_and_emit_exact_diff() {
2100 let (_guard, store) = test_store("operations");
2101 let snapshot = fixture_snapshot(store);
2102 let pristine = VirtualFs::new(snapshot.clone())
2103 .materialized_final_state()
2104 .unwrap();
2105 let mut vfs = VirtualFs::new(snapshot.clone());
2106
2107 assert_eq!(vfs.read(&path("src/a.txt")).unwrap(), b"alpha");
2108 vfs.append(&path("src/a.txt"), b"!").unwrap();
2109 vfs.mkdir(&path("generated"), 0o755).unwrap();
2110 vfs.write(&path("generated/out.txt"), b"output").unwrap();
2111 vfs.rename(&path("src/nested"), &path("moved")).unwrap();
2112 vfs.unlink(&path("opaque-link")).unwrap();
2113
2114 let diff = vfs.canonical_diff().unwrap();
2115 assert_eq!(diff, vfs.canonical_diff().unwrap());
2116 assert!(
2117 diff.entries()
2118 .windows(2)
2119 .all(|pair| pair[0].path < pair[1].path)
2120 );
2121
2122 let mut applied = pristine;
2123 apply_diff(&mut applied, &diff);
2124 assert_eq!(applied, vfs.materialized_final_state().unwrap());
2125
2126 let untouched = VirtualFs::new(snapshot).materialized_final_state().unwrap();
2127 assert_eq!(untouched[&path("src/a.txt")].size(), 5);
2128 assert!(untouched.contains_key(&path("opaque-link")));
2129 assert!(!untouched.contains_key(&path("generated")));
2130 }
2131
2132 #[test]
2133 fn recursive_delete_expands_full_descendant_closure() {
2134 let (_guard, store) = test_store("delete-closure");
2135 let snapshot = fixture_snapshot(store);
2136 let mut vfs = VirtualFs::new(snapshot);
2137 vfs.remove_tree(&path("src")).unwrap();
2138
2139 let diff = vfs.canonical_diff().unwrap();
2140 let deleted: Vec<&str> = diff
2141 .entries()
2142 .iter()
2143 .map(|entry| {
2144 assert_eq!(entry.kind, DiffKind::Delete);
2145 entry.path.as_str()
2146 })
2147 .collect();
2148 assert_eq!(
2149 deleted,
2150 ["src", "src/a.txt", "src/nested", "src/nested/b.txt"]
2151 );
2152 assert_eq!(
2153 diff.metrics(),
2154 CanonicalDiffMetrics {
2155 candidate_paths: 4,
2156 expanded_delete_paths: 4,
2157 changed_paths: 4,
2158 materialized_after_bytes: 0,
2159 }
2160 );
2161 assert_eq!(vfs.write_set().len(), 4);
2162 }
2163
2164 #[test]
2165 fn subtree_expansion_is_component_aware_not_lexical_prefix_based() {
2166 let (_guard, store) = test_store("subtree-order");
2167 let mut builder = SnapshotBuilder::new(store);
2168 builder.add_directory(path("a"), 0o755).unwrap();
2169 builder.add_file(path("a/in"), b"in", 0o644).unwrap();
2170 builder
2171 .add_file(path("a-foreign"), b"foreign", 0o644)
2172 .unwrap();
2173 builder.add_file(path("a.other"), b"other", 0o644).unwrap();
2174 let mut vfs = VirtualFs::new(builder.build().unwrap());
2175
2176 vfs.remove_tree(&path("a")).unwrap();
2177 let diff = vfs.canonical_diff().unwrap();
2178 let paths: Vec<&str> = diff
2179 .entries()
2180 .iter()
2181 .map(|entry| entry.path.as_str())
2182 .collect();
2183 assert_eq!(paths, ["a", "a/in"]);
2184 assert!(vfs.exists(&path("a-foreign")));
2185 assert!(vfs.exists(&path("a.other")));
2186 }
2187
2188 #[test]
2189 fn subtree_whiteouts_hide_old_and_overlay_descendants_after_recreation() {
2190 let (_guard, store) = test_store("whiteout");
2191 let snapshot = fixture_snapshot(store);
2192 let mut base_model = VirtualFs::new(snapshot.clone())
2193 .materialized_final_state()
2194 .unwrap();
2195 let mut vfs = VirtualFs::new(snapshot);
2196
2197 vfs.write(&path("src/overlay.txt"), b"overlay").unwrap();
2198 vfs.remove_tree(&path("src")).unwrap();
2199 assert!(!vfs.exists(&path("src/a.txt")));
2200 assert!(!vfs.exists(&path("src/overlay.txt")));
2201
2202 vfs.mkdir(&path("src"), 0o755).unwrap();
2203 assert!(vfs.read_dir(&path("src")).unwrap().is_empty());
2204 vfs.write(&path("src/fresh.txt"), b"fresh").unwrap();
2205 assert_eq!(vfs.read_dir(&path("src")).unwrap(), [path("src/fresh.txt")]);
2206
2207 let diff = vfs.canonical_diff().unwrap();
2208 apply_diff(&mut base_model, &diff);
2209 assert_eq!(base_model, vfs.materialized_final_state().unwrap());
2210 assert!(!base_model.contains_key(&path("src/a.txt")));
2211 assert!(!base_model.contains_key(&path("src/overlay.txt")));
2212 }
2213
2214 #[test]
2215 fn rename_whiteouts_hide_source_descendants_if_source_is_recreated() {
2216 let (_guard, store) = test_store("rename-whiteout");
2217 let mut vfs = VirtualFs::new(fixture_snapshot(store));
2218 vfs.write(&path("src/overlay.txt"), b"overlay").unwrap();
2219 vfs.rename(&path("src"), &path("destination")).unwrap();
2220
2221 assert!(!vfs.exists(&path("src/overlay.txt")));
2222 assert_eq!(
2223 vfs.read(&path("destination/overlay.txt")).unwrap(),
2224 b"overlay"
2225 );
2226 vfs.mkdir(&path("src"), 0o755).unwrap();
2227 assert!(vfs.read_dir(&path("src")).unwrap().is_empty());
2228 assert!(vfs.exists(&path("destination/nested/b.txt")));
2229 }
2230
2231 #[test]
2232 fn one_file_diff_in_large_snapshot_only_compares_touched_path() {
2233 let (_guard, store) = test_store("touched-scaling");
2234 let blob = store.put(b"x").unwrap();
2235 let mut builder = SnapshotBuilder::new(store);
2236 builder.add_directory(path("bulk"), 0o755).unwrap();
2237 for index in 0..10_000 {
2238 builder
2239 .insert(
2240 path(&format!("bulk/file-{index:05}")),
2241 SnapshotNode::materialized(NodeKind::File, blob, 1, 0o644),
2242 )
2243 .unwrap();
2244 }
2245 let snapshot = builder.build().unwrap();
2246 assert_eq!(snapshot.len(), 10_002);
2247
2248 let mut vfs = VirtualFs::new(snapshot);
2249 vfs.write(&path("bulk/file-05000"), b"changed").unwrap();
2250 let diff = vfs.canonical_diff().unwrap();
2251
2252 assert_eq!(
2253 diff.metrics(),
2254 CanonicalDiffMetrics {
2255 candidate_paths: 1,
2256 expanded_delete_paths: 0,
2257 changed_paths: 1,
2258 materialized_after_bytes: 7,
2259 }
2260 );
2261 }
2262
2263 #[test]
2264 fn reads_writes_and_effects_are_transaction_local_and_ordered() {
2265 let (_guard, store) = test_store("ledger");
2266 let mut vfs = VirtualFs::new(fixture_snapshot(store));
2267
2268 vfs.read(&path("src/a.txt")).unwrap();
2269 vfs.read_dir(&path("src")).unwrap();
2270 assert!(!vfs.exists(&path("missing")));
2271 vfs.write(&path("created.txt"), b"new").unwrap();
2272
2273 assert!(vfs.read_set()[&path("src/a.txt")].content.is_some());
2274 assert!(vfs.read_set()[&path("src")].directory.is_some());
2275 assert_eq!(vfs.read_set()[&path("missing")].metadata, Some(None));
2276 assert_eq!(
2277 vfs.write_set()[&path("created.txt")],
2278 WritePrecondition { expected: None }
2279 );
2280 assert!(
2281 vfs.effects()
2282 .iter()
2283 .enumerate()
2284 .all(|(index, event)| event.sequence == index as u64)
2285 );
2286 assert_eq!(vfs.metrics().effect_events, vfs.effects().len());
2287 }
2288
2289 #[test]
2290 fn symlinks_are_opaque_and_never_followed() {
2291 let (_guard, store) = test_store("symlink");
2292 let mut vfs = VirtualFs::new(fixture_snapshot(store));
2293 assert_eq!(
2294 vfs.read_link(&path("opaque-link")).unwrap(),
2295 b"../../outside"
2296 );
2297 assert!(matches!(
2298 vfs.read(&path("opaque-link")),
2299 Err(VfsError::NotFile {
2300 actual: NodeKind::Symlink,
2301 ..
2302 })
2303 ));
2304 assert!(matches!(
2305 vfs.write(&path("opaque-link/child"), b"blocked"),
2306 Err(VfsError::NotDirectory {
2307 actual: NodeKind::Symlink,
2308 ..
2309 })
2310 ));
2311 }
2312
2313 #[test]
2314 fn no_op_sequences_have_empty_canonical_diffs() {
2315 let (_guard, store) = test_store("no-op");
2316 let snapshot = fixture_snapshot(store);
2317 let mut vfs = VirtualFs::new(snapshot);
2318 vfs.write(&path("temporary"), b"x").unwrap();
2319 vfs.unlink(&path("temporary")).unwrap();
2320 vfs.write(&path("src/a.txt"), b"alpha").unwrap();
2321
2322 assert!(vfs.canonical_diff().unwrap().is_empty());
2323 assert!(!vfs.effects().is_empty());
2324 }
2325
2326 #[test]
2327 fn lazy_rename_diff_is_stable_across_repeated_generation() {
2328 let (_guard, store) = test_store("lazy-rename");
2329 let expected = stamp(NodeKind::File, 5, 91);
2330 let mut builder = SnapshotBuilder::new(store);
2331 builder
2332 .add_lazy(path("z.txt"), expected, move |stamp| {
2333 Ok(CapturedContent {
2334 bytes: b"hello".to_vec(),
2335 before: stamp,
2336 after: stamp,
2337 })
2338 })
2339 .unwrap();
2340 let mut vfs = VirtualFs::new(builder.build().unwrap());
2341 vfs.rename(&path("z.txt"), &path("a.txt")).unwrap();
2342
2343 let first = vfs.canonical_diff().unwrap();
2344 let second = vfs.canonical_diff().unwrap();
2345 assert_eq!(first, second);
2346 assert_eq!(first.entries().len(), 2);
2347 }
2348
2349 #[test]
2350 fn generated_operation_sequences_replay_through_diff_to_same_state() {
2351 for seed in 0..128 {
2352 let (_guard, store) = test_store("model-property");
2353 let base = property_snapshot(store);
2354 let mut expected = VirtualFs::new(base.clone())
2355 .materialized_final_state()
2356 .unwrap();
2357 let mut vfs = VirtualFs::new(base);
2358 let mut random = Lcg::new(seed);
2359
2360 for _ in 0..48 {
2361 apply_generated_operation(&mut vfs, &mut random);
2362 }
2363
2364 let diff = vfs.canonical_diff().unwrap();
2365 apply_diff(&mut expected, &diff);
2366 assert_eq!(
2367 expected,
2368 vfs.materialized_final_state().unwrap(),
2369 "seed {seed}"
2370 );
2371 assert_eq!(diff, vfs.canonical_diff().unwrap(), "seed {seed}");
2372 }
2373 }
2374
2375 fn property_snapshot(store: BlobStore) -> BaseSnapshot {
2376 let mut builder = SnapshotBuilder::new(store);
2377 builder.add_directory(path("a"), 0o755).unwrap();
2378 builder.add_directory(path("b"), 0o755).unwrap();
2379 builder.add_directory(path("a/sub"), 0o755).unwrap();
2380 builder.add_file(path("a/f0"), b"0", 0o644).unwrap();
2381 builder.add_file(path("a/f1"), b"1", 0o644).unwrap();
2382 builder
2383 .add_file(path("a/sub/deep"), b"deep", 0o600)
2384 .unwrap();
2385 builder.add_file(path("b/f2"), b"2", 0o644).unwrap();
2386 builder.build().unwrap()
2387 }
2388
2389 fn apply_diff(state: &mut BTreeMap<VPath, NodeState>, diff: &CanonicalDiff) {
2390 for entry in diff.entries() {
2391 match entry.after {
2392 Some(after) => {
2393 state.insert(entry.path.clone(), after);
2394 }
2395 None => {
2396 state.remove(&entry.path);
2397 }
2398 }
2399 }
2400 }
2401
2402 fn apply_generated_operation(vfs: &mut VirtualFs, random: &mut Lcg) {
2403 const FILES: [&str; 6] = ["a/f0", "a/f1", "a/sub/deep", "b/f2", "c/f3", "d/f4"];
2404 const DIRECTORIES: [&str; 4] = ["a/sub", "c", "d", "b/sub"];
2405 const RENAMES: [(&str, &str); 6] = [
2406 ("a/f0", "b/f0"),
2407 ("b/f0", "a/f0"),
2408 ("a/sub", "b/sub"),
2409 ("b/sub", "a/sub"),
2410 ("c", "d"),
2411 ("d", "c"),
2412 ];
2413
2414 match random.next() % 7 {
2415 0 => {
2416 let target = path(FILES[random.index(FILES.len())]);
2417 let payload = random.next().to_le_bytes();
2418 let _ = vfs.write(&target, &payload);
2419 }
2420 1 => {
2421 let target = path(DIRECTORIES[random.index(DIRECTORIES.len())]);
2422 let _ = vfs.mkdir(&target, 0o755);
2423 }
2424 2 => {
2425 let target = path(FILES[random.index(FILES.len())]);
2426 let _ = vfs.unlink(&target);
2427 }
2428 3 => {
2429 let target = path(DIRECTORIES[random.index(DIRECTORIES.len())]);
2430 let _ = vfs.rmdir(&target);
2431 }
2432 4 => {
2433 let target = path(DIRECTORIES[random.index(DIRECTORIES.len())]);
2434 let _ = vfs.remove_tree(&target);
2435 }
2436 5 => {
2437 let (from, to) = RENAMES[random.index(RENAMES.len())];
2438 let _ = vfs.rename(&path(from), &path(to));
2439 }
2440 6 => {
2441 let target = path(FILES[random.index(FILES.len())]);
2442 let _ = vfs.append(&target, b"+");
2443 }
2444 _ => unreachable!(),
2445 }
2446 }
2447
2448 struct Lcg(u64);
2449
2450 impl Lcg {
2451 fn new(seed: u64) -> Self {
2452 Self(seed ^ 0x9e37_79b9_7f4a_7c15)
2453 }
2454
2455 fn next(&mut self) -> u64 {
2456 self.0 = self
2457 .0
2458 .wrapping_mul(6_364_136_223_846_793_005)
2459 .wrapping_add(1_442_695_040_888_963_407);
2460 self.0
2461 }
2462
2463 fn index(&mut self, len: usize) -> usize {
2464 usize::try_from(self.next() % len as u64).unwrap()
2465 }
2466 }
2467}