@wasm-oj/server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/THIRD_PARTY_NOTICES.md +302 -0
  4. package/crates/runtime-core/Cargo.lock +5099 -0
  5. package/crates/runtime-core/Cargo.toml +66 -0
  6. package/crates/runtime-core/README.md +47 -0
  7. package/crates/runtime-core/src/bin/wasm-oj-compiler.rs +418 -0
  8. package/crates/runtime-core/src/bin/wasm-oj-runner.rs +294 -0
  9. package/crates/runtime-core/src/capabilities.rs +118 -0
  10. package/crates/runtime-core/src/compiler.rs +658 -0
  11. package/crates/runtime-core/src/contract.rs +5 -0
  12. package/crates/runtime-core/src/deterministic.rs +1051 -0
  13. package/crates/runtime-core/src/error.rs +58 -0
  14. package/crates/runtime-core/src/filesystem.rs +547 -0
  15. package/crates/runtime-core/src/filesystem_quota.rs +167 -0
  16. package/crates/runtime-core/src/go_compiler_session.rs +297 -0
  17. package/crates/runtime-core/src/interactive.rs +1019 -0
  18. package/crates/runtime-core/src/judge_package.rs +1539 -0
  19. package/crates/runtime-core/src/lib.rs +98 -0
  20. package/crates/runtime-core/src/memory.rs +84 -0
  21. package/crates/runtime-core/src/meter.rs +549 -0
  22. package/crates/runtime-core/src/module_imports.rs +149 -0
  23. package/crates/runtime-core/src/module_policy.rs +714 -0
  24. package/crates/runtime-core/src/output.rs +204 -0
  25. package/crates/runtime-core/src/run/mod.rs +208 -0
  26. package/crates/runtime-core/src/run/native.rs +260 -0
  27. package/crates/runtime-core/src/run/web.rs +229 -0
  28. package/crates/runtime-core/src/run/web_runtime.rs +109 -0
  29. package/crates/runtime-core/src/types.rs +268 -0
  30. package/crates/runtime-core/src/web.rs +83 -0
  31. package/dist/chunks/go-toolchain-Dbt-lp2L.js +426 -0
  32. package/dist/chunks/java-toolchain-DajoRCHu.js +44 -0
  33. package/dist/chunks/python-toolchain-Dx834o2A.js +4 -0
  34. package/dist/chunks/rust-toolchain-CJ3sMxPE.js +252 -0
  35. package/dist/chunks/toolchains-C6KuA1yM.js +224 -0
  36. package/dist/go-stage.mjs +193 -0
  37. package/dist/index.d.ts +221 -0
  38. package/dist/index.js +4393 -0
  39. package/dist/java-stage.mjs +111 -0
  40. package/dist/python-stage.mjs +90 -0
  41. package/dist/rustc-stage.mjs +301 -0
  42. package/dist/server-build-stage.mjs +2564 -0
  43. package/dist/server-runner-stage.mjs +155 -0
  44. package/licenses/fflate-MIT.txt +21 -0
  45. package/licenses/runtime-core-dependencies.html +6253 -0
  46. package/licenses/runtime-core-dependencies.json +3041 -0
  47. package/licenses/wasmer-sdk-MIT.txt +21 -0
  48. package/licenses/wasmer-sdk-dependencies.html +6901 -0
  49. package/licenses/wasmer-sdk-dependencies.json +3013 -0
  50. package/package.json +70 -0
  51. package/rust-toolchain.toml +5 -0
  52. package/testdata/wojjdg02-v2-text.hex +1 -0
  53. package/vendor/shared-buffer/Cargo.toml +22 -0
  54. package/vendor/shared-buffer/LICENSE_APACHE.md +176 -0
  55. package/vendor/shared-buffer/LICENSE_MIT.md +25 -0
  56. package/vendor/shared-buffer/README.md +34 -0
  57. package/vendor/shared-buffer/src/lib.rs +58 -0
  58. package/vendor/shared-buffer/src/mmap.rs +250 -0
  59. package/vendor/shared-buffer/src/owned.rs +389 -0
  60. package/vendor/virtual-fs/Cargo.toml +181 -0
  61. package/vendor/virtual-fs/LICENSE +25 -0
  62. package/vendor/virtual-fs/src/arc_box_file.rs +142 -0
  63. package/vendor/virtual-fs/src/arc_file.rs +182 -0
  64. package/vendor/virtual-fs/src/arc_fs.rs +68 -0
  65. package/vendor/virtual-fs/src/buffer_file.rs +103 -0
  66. package/vendor/virtual-fs/src/builder.rs +232 -0
  67. package/vendor/virtual-fs/src/combine_file.rs +101 -0
  68. package/vendor/virtual-fs/src/cow_file.rs +345 -0
  69. package/vendor/virtual-fs/src/dual_write_file.rs +113 -0
  70. package/vendor/virtual-fs/src/empty_fs.rs +81 -0
  71. package/vendor/virtual-fs/src/filesystems.rs +108 -0
  72. package/vendor/virtual-fs/src/host_fs.rs +1390 -0
  73. package/vendor/virtual-fs/src/lib.rs +782 -0
  74. package/vendor/virtual-fs/src/limiter.rs +252 -0
  75. package/vendor/virtual-fs/src/mem_fs/file.rs +1799 -0
  76. package/vendor/virtual-fs/src/mem_fs/file_opener.rs +941 -0
  77. package/vendor/virtual-fs/src/mem_fs/filesystem.rs +2134 -0
  78. package/vendor/virtual-fs/src/mem_fs/mod.rs +245 -0
  79. package/vendor/virtual-fs/src/mem_fs/offloaded_file.rs +474 -0
  80. package/vendor/virtual-fs/src/mem_fs/stdio.rs +318 -0
  81. package/vendor/virtual-fs/src/mount_fs.rs +2225 -0
  82. package/vendor/virtual-fs/src/null_file.rs +87 -0
  83. package/vendor/virtual-fs/src/ops.rs +364 -0
  84. package/vendor/virtual-fs/src/overlay_fs.rs +2216 -0
  85. package/vendor/virtual-fs/src/passthru_fs.rs +119 -0
  86. package/vendor/virtual-fs/src/pipe.rs +603 -0
  87. package/vendor/virtual-fs/src/random_file.rs +88 -0
  88. package/vendor/virtual-fs/src/special_file.rs +108 -0
  89. package/vendor/virtual-fs/src/static_file.rs +133 -0
  90. package/vendor/virtual-fs/src/static_fs.rs +460 -0
  91. package/vendor/virtual-fs/src/tmp_fs.rs +95 -0
  92. package/vendor/virtual-fs/src/trace_fs.rs +258 -0
  93. package/vendor/virtual-fs/src/webc_volume_fs.rs +829 -0
  94. package/vendor/virtual-fs/src/zero_file.rs +90 -0
@@ -0,0 +1,2225 @@
1
+ //! A mount-topology filesystem that routes operations by path,
2
+ //! its not as simple as TmpFs. not currently used but was used by
3
+ //! the previously implementation of Deploy - now using TmpFs
4
+
5
+ use crate::*;
6
+
7
+ #[cfg(not(feature = "js"))]
8
+ use std::time::{SystemTime, UNIX_EPOCH};
9
+ use std::{
10
+ borrow::Cow,
11
+ collections::{BTreeMap, BTreeSet},
12
+ ffi::OsString,
13
+ path::{Path, PathBuf},
14
+ sync::{Arc, RwLock},
15
+ };
16
+ #[cfg(feature = "js")]
17
+ use web_time::{SystemTime, UNIX_EPOCH};
18
+
19
+ const MIN_METADATA_TIMESTAMP: u64 = 1_000_000_000; // 1 second in nano seconds
20
+
21
+ type DynFileSystem = Arc<dyn FileSystem + Send + Sync>;
22
+
23
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
24
+ pub enum ExactMountConflictMode {
25
+ Fail,
26
+ KeepExisting,
27
+ ReplaceExisting,
28
+ }
29
+
30
+ #[derive(Debug, Clone)]
31
+ struct MountedFileSystem {
32
+ fs: DynFileSystem,
33
+ source_path: PathBuf,
34
+ }
35
+
36
+ #[derive(Debug, Default)]
37
+ struct MountNode {
38
+ /// Creation timestamp in nanoseconds since the Unix epoch.
39
+ /// Set once when the node is first inserted into the tree.
40
+ created_at: u64,
41
+ mount: Option<MountedFileSystem>,
42
+ children: BTreeMap<OsString, MountNode>,
43
+ }
44
+
45
+ #[derive(Debug, Clone)]
46
+ struct ExactNode {
47
+ path: PathBuf,
48
+ fs: Option<DynFileSystem>,
49
+ source_path: PathBuf,
50
+ child_names: BTreeSet<OsString>,
51
+ /// Timestamp reported for this node in nanoseconds since the Unix epoch.
52
+ /// For synthetic non-mounted nodes, this may be generated at lookup time
53
+ /// rather than representing an original creation event.
54
+ created_at: u64,
55
+ }
56
+
57
+ impl ExactNode {
58
+ fn has_children(&self) -> bool {
59
+ !self.child_names.is_empty()
60
+ }
61
+ }
62
+
63
+ #[derive(Debug, Clone)]
64
+ struct ResolvedMount {
65
+ mount_path: PathBuf,
66
+ delegated_path: PathBuf,
67
+ fs: DynFileSystem,
68
+ }
69
+
70
+ #[derive(Debug, Clone)]
71
+ pub struct MountPoint {
72
+ pub path: PathBuf,
73
+ pub name: String,
74
+ pub fs: Option<DynFileSystem>,
75
+ pub children: Option<Arc<MountFileSystem>>,
76
+ }
77
+
78
+ #[derive(Debug, Clone)]
79
+ pub struct MountEntry {
80
+ pub path: PathBuf,
81
+ pub fs: DynFileSystem,
82
+ pub source_path: PathBuf,
83
+ }
84
+
85
+ impl MountPoint {
86
+ pub fn fs(&self) -> Option<&(dyn FileSystem + Send + Sync)> {
87
+ self.fs.as_deref()
88
+ }
89
+
90
+ pub fn mount_point_ref(&self) -> MountPointRef<'_> {
91
+ MountPointRef {
92
+ path: self.path.clone(),
93
+ name: self.name.clone(),
94
+ fs: self.fs.as_deref(),
95
+ }
96
+ }
97
+ }
98
+
99
+ /// Allows different filesystems of different types
100
+ /// to be mounted at various mount points
101
+ #[derive(Debug)]
102
+ pub struct MountFileSystem {
103
+ root: RwLock<MountNode>,
104
+ }
105
+
106
+ impl Default for MountFileSystem {
107
+ fn default() -> Self {
108
+ Self::new()
109
+ }
110
+ }
111
+
112
+ impl MountFileSystem {
113
+ pub fn new() -> Self {
114
+ let ts = Self::now_nanos();
115
+ Self {
116
+ root: RwLock::new(MountNode {
117
+ created_at: ts,
118
+ ..MountNode::default()
119
+ }),
120
+ }
121
+ }
122
+
123
+ pub fn mount(
124
+ &self,
125
+ path: impl AsRef<Path>,
126
+ fs: Arc<dyn FileSystem + Send + Sync>,
127
+ ) -> Result<()> {
128
+ self.mount_with_source(path, Path::new("/"), fs)
129
+ }
130
+
131
+ pub fn mount_with_source(
132
+ &self,
133
+ path: impl AsRef<Path>,
134
+ source_path: impl AsRef<Path>,
135
+ fs: Arc<dyn FileSystem + Send + Sync>,
136
+ ) -> Result<()> {
137
+ let path = self.prepare_path(path.as_ref())?;
138
+ let source_path = Self::normalize_source_path(source_path.as_ref());
139
+ let ts = Self::now_nanos();
140
+ let mut root = self.root.write().unwrap();
141
+ let node = Self::mount_node_mut(&mut root, &Self::path_components(&path), ts);
142
+
143
+ if node.mount.is_some() {
144
+ Err(FsError::AlreadyExists)
145
+ } else {
146
+ node.mount = Some(MountedFileSystem { fs, source_path });
147
+ Ok(())
148
+ }
149
+ }
150
+
151
+ pub fn filesystem_at(
152
+ &self,
153
+ path: impl AsRef<Path>,
154
+ ) -> Option<Arc<dyn FileSystem + Send + Sync>> {
155
+ self.exact_node(path.as_ref()).and_then(|node| node.fs)
156
+ }
157
+
158
+ pub fn clear(&mut self) {
159
+ *self.root.write().unwrap() = MountNode {
160
+ created_at: Self::now_nanos(),
161
+ ..MountNode::default()
162
+ };
163
+ }
164
+
165
+ fn prepare_path(&self, path: &Path) -> Result<PathBuf> {
166
+ let mut normalized = PathBuf::new();
167
+
168
+ for component in path.components() {
169
+ match component {
170
+ std::path::Component::RootDir | std::path::Component::CurDir => {}
171
+ std::path::Component::ParentDir => {
172
+ if !normalized.pop() {
173
+ return Err(FsError::InvalidInput);
174
+ }
175
+ }
176
+ std::path::Component::Normal(part) => normalized.push(part),
177
+ std::path::Component::Prefix(_) => return Err(FsError::InvalidInput),
178
+ }
179
+ }
180
+
181
+ Ok(normalized)
182
+ }
183
+
184
+ fn path_components(path: &Path) -> Vec<OsString> {
185
+ path.components()
186
+ .map(|component| component.as_os_str().to_os_string())
187
+ .collect()
188
+ }
189
+
190
+ fn absolute_path(components: &[OsString]) -> PathBuf {
191
+ let mut path = PathBuf::from("/");
192
+ for component in components {
193
+ path.push(component);
194
+ }
195
+ path
196
+ }
197
+
198
+ fn normalize_source_path(path: &Path) -> PathBuf {
199
+ let mut normalized = PathBuf::from("/");
200
+ normalized.push(path.strip_prefix("/").unwrap_or(path));
201
+ normalized
202
+ }
203
+
204
+ fn now_nanos() -> u64 {
205
+ SystemTime::now()
206
+ .duration_since(UNIX_EPOCH)
207
+ .map_or(0, |d| d.as_nanos() as u64)
208
+ .max(MIN_METADATA_TIMESTAMP)
209
+ }
210
+
211
+ fn directory_metadata_at(ts: u64) -> Metadata {
212
+ Metadata {
213
+ ft: FileType::new_dir(),
214
+ accessed: ts,
215
+ created: ts,
216
+ modified: ts,
217
+ len: 0,
218
+ }
219
+ }
220
+
221
+ fn should_fallback_to_synthetic_dir(error: &FsError) -> bool {
222
+ matches!(
223
+ error,
224
+ FsError::Unsupported | FsError::NotAFile | FsError::BaseNotDirectory
225
+ )
226
+ }
227
+
228
+ fn synthetic_entry(name: OsString, base: &Path, ts: u64) -> DirEntry {
229
+ DirEntry {
230
+ path: base.join(PathBuf::from(name)),
231
+ metadata: Ok(Self::directory_metadata_at(ts)),
232
+ }
233
+ }
234
+
235
+ fn mounted(node: &MountNode) -> Option<MountedFileSystem> {
236
+ node.mount.clone()
237
+ }
238
+
239
+ fn collect_mount_entries(node: &MountNode, path: &Path, entries: &mut Vec<MountEntry>) {
240
+ if let Some(mount) = Self::mounted(node) {
241
+ entries.push(MountEntry {
242
+ path: path.to_path_buf(),
243
+ fs: mount.fs,
244
+ source_path: mount.source_path,
245
+ });
246
+ }
247
+
248
+ for (child_name, child) in &node.children {
249
+ let child_path = path.join(child_name);
250
+ Self::collect_mount_entries(child, &child_path, entries);
251
+ }
252
+ }
253
+
254
+ fn find_node<'a>(node: &'a MountNode, components: &[OsString]) -> Option<&'a MountNode> {
255
+ let mut node = node;
256
+ for component in components {
257
+ node = node.children.get(component)?;
258
+ }
259
+ Some(node)
260
+ }
261
+
262
+ fn exact_node(&self, path: &Path) -> Option<ExactNode> {
263
+ let path = self.prepare_path(path).ok()?;
264
+ let components = Self::path_components(&path);
265
+ let visible_path = Path::new("/").join(&path);
266
+ let root = self.root.read().unwrap();
267
+ let node = Self::find_node(&root, &components)?;
268
+ let mounted = Self::mounted(node);
269
+
270
+ Some(ExactNode {
271
+ path: visible_path.clone(),
272
+ fs: mounted.as_ref().map(|mount| mount.fs.clone()),
273
+ created_at: node.created_at,
274
+ source_path: mounted
275
+ .map(|mount| mount.source_path)
276
+ .unwrap_or_else(|| PathBuf::from("/")),
277
+ child_names: node.children.keys().cloned().collect(),
278
+ })
279
+ }
280
+
281
+ fn resolve_mount(&self, path: impl AsRef<Path>) -> Option<ResolvedMount> {
282
+ let path = self.prepare_path(path.as_ref()).ok()?;
283
+ let components = Self::path_components(&path);
284
+ let root = self.root.read().unwrap();
285
+ let mut node = &*root;
286
+ let mut best = Self::mounted(node).map(|mount| ResolvedMount {
287
+ mount_path: PathBuf::from("/"),
288
+ delegated_path: mount.source_path.join(
289
+ Self::absolute_path(&components)
290
+ .strip_prefix("/")
291
+ .unwrap_or(Path::new("")),
292
+ ),
293
+ fs: mount.fs,
294
+ });
295
+
296
+ for (index, component) in components.iter().enumerate() {
297
+ let Some(child) = node.children.get(component) else {
298
+ break;
299
+ };
300
+ node = child;
301
+
302
+ if let Some(mount) = Self::mounted(node) {
303
+ best = Some(ResolvedMount {
304
+ mount_path: Self::absolute_path(&components[..=index]),
305
+ delegated_path: mount.source_path.join(
306
+ Self::absolute_path(&components[index + 1..])
307
+ .strip_prefix("/")
308
+ .unwrap_or(Path::new("")),
309
+ ),
310
+ fs: mount.fs,
311
+ });
312
+ }
313
+ }
314
+
315
+ best
316
+ }
317
+
318
+ fn rebase_entries(entries: &mut ReadDir, source_prefix: &Path, target_prefix: &Path) {
319
+ for entry in &mut entries.data {
320
+ let suffix = entry.path.strip_prefix(source_prefix).unwrap_or_else(|_| {
321
+ entry
322
+ .path
323
+ .strip_prefix(Path::new("/"))
324
+ .unwrap_or(&entry.path)
325
+ });
326
+ entry.path = target_prefix.join(suffix);
327
+ }
328
+ }
329
+
330
+ fn read_dir_from_exact_node(&self, node: &ExactNode) -> Result<ReadDir> {
331
+ let mut entries = Vec::new();
332
+
333
+ let backing = if let Some(fs) = &node.fs {
334
+ Some((
335
+ fs.read_dir(&node.source_path),
336
+ Cow::Borrowed(node.source_path.as_path()),
337
+ ))
338
+ } else {
339
+ self.resolve_mount(&node.path).map(|resolved| {
340
+ (
341
+ resolved.fs.read_dir(&resolved.delegated_path),
342
+ Cow::Owned(resolved.delegated_path),
343
+ )
344
+ })
345
+ };
346
+
347
+ if let Some((base_entries, source_path)) = backing {
348
+ match base_entries {
349
+ Ok(mut base_entries) => {
350
+ Self::rebase_entries(&mut base_entries, &source_path, &node.path);
351
+ entries.extend(base_entries.data.into_iter().filter(|entry| {
352
+ entry
353
+ .path
354
+ .file_name()
355
+ .map(|name| !node.child_names.contains(name))
356
+ .unwrap_or(true)
357
+ }));
358
+ }
359
+ Err(FsError::EntryNotFound) if node.has_children() => {}
360
+ Err(error)
361
+ if node.has_children() && Self::should_fallback_to_synthetic_dir(&error) => {}
362
+ Err(error) => return Err(error),
363
+ }
364
+ }
365
+
366
+ entries.extend(
367
+ node.child_names
368
+ .iter()
369
+ .cloned()
370
+ .map(|name| Self::synthetic_entry(name, &node.path, node.created_at)),
371
+ );
372
+
373
+ Ok(ReadDir::new(entries))
374
+ }
375
+
376
+ fn mount_node_mut<'a>(
377
+ node: &'a mut MountNode,
378
+ components: &[OsString],
379
+ ts: u64,
380
+ ) -> &'a mut MountNode {
381
+ let mut node = node;
382
+ for component in components {
383
+ node = node
384
+ .children
385
+ .entry(component.clone())
386
+ .or_insert_with(|| MountNode {
387
+ created_at: ts,
388
+ ..MountNode::default()
389
+ });
390
+ }
391
+
392
+ node
393
+ }
394
+
395
+ fn clear_descendants(node: &mut MountNode) {
396
+ node.children.clear();
397
+ }
398
+
399
+ /// Overwrite the mount at `path`.
400
+ pub fn set_mount(
401
+ &self,
402
+ path: impl AsRef<Path>,
403
+ fs: Arc<dyn FileSystem + Send + Sync>,
404
+ ) -> Result<()> {
405
+ let path = self.prepare_path(path.as_ref())?;
406
+ let ts = Self::now_nanos();
407
+ let mut root = self.root.write().unwrap();
408
+ let node = Self::mount_node_mut(&mut root, &Self::path_components(&path), ts);
409
+ node.mount = Some(MountedFileSystem {
410
+ fs,
411
+ source_path: PathBuf::from("/"),
412
+ });
413
+ Ok(())
414
+ }
415
+
416
+ pub fn add_mount_entries_with_mode(
417
+ &self,
418
+ entries: impl IntoIterator<Item = MountEntry>,
419
+ conflict_mode: ExactMountConflictMode,
420
+ ) -> Result<()> {
421
+ let mut skipped_subtrees = Vec::<PathBuf>::new();
422
+
423
+ for entry in entries {
424
+ if skipped_subtrees
425
+ .iter()
426
+ .any(|prefix| entry.path.starts_with(prefix))
427
+ {
428
+ continue;
429
+ }
430
+
431
+ let exact_conflict = self.filesystem_at(&entry.path).is_some();
432
+ if exact_conflict {
433
+ match conflict_mode {
434
+ ExactMountConflictMode::Fail => return Err(FsError::AlreadyExists),
435
+ ExactMountConflictMode::KeepExisting => {
436
+ skipped_subtrees.push(entry.path);
437
+ continue;
438
+ }
439
+ ExactMountConflictMode::ReplaceExisting => {
440
+ let ts = Self::now_nanos();
441
+ let mut root = self.root.write().unwrap();
442
+ let node = Self::mount_node_mut(
443
+ &mut root,
444
+ &Self::path_components(&self.prepare_path(&entry.path)?),
445
+ ts,
446
+ );
447
+ Self::clear_descendants(node);
448
+ node.mount = Some(MountedFileSystem {
449
+ fs: entry.fs,
450
+ source_path: entry.source_path,
451
+ });
452
+ continue;
453
+ }
454
+ }
455
+ }
456
+
457
+ self.mount_with_source(&entry.path, &entry.source_path, entry.fs)?;
458
+ }
459
+
460
+ Ok(())
461
+ }
462
+ pub fn mount_entries(&self) -> Vec<MountEntry> {
463
+ let mut entries = Vec::new();
464
+ let root = self.root.read().unwrap();
465
+ Self::collect_mount_entries(&root, Path::new("/"), &mut entries);
466
+ entries
467
+ }
468
+ }
469
+
470
+ impl FileSystem for MountFileSystem {
471
+ fn readlink(&self, path: &Path) -> Result<PathBuf> {
472
+ let path = self.prepare_path(path)?;
473
+
474
+ if path.as_os_str().is_empty() {
475
+ Err(FsError::NotAFile)
476
+ } else {
477
+ if let Some(node) = self.exact_node(&path)
478
+ && node.fs.is_none()
479
+ {
480
+ return Err(FsError::EntryNotFound);
481
+ }
482
+
483
+ match self.resolve_mount(path) {
484
+ Some(resolved) => resolved.fs.readlink(&resolved.delegated_path),
485
+ None => Err(FsError::EntryNotFound),
486
+ }
487
+ }
488
+ }
489
+
490
+ fn read_dir(&self, path: &Path) -> Result<ReadDir> {
491
+ let path = self.prepare_path(path)?;
492
+
493
+ if let Some(node) = self.exact_node(&path) {
494
+ return self.read_dir_from_exact_node(&node);
495
+ }
496
+
497
+ match self.resolve_mount(path.clone()) {
498
+ Some(resolved) => {
499
+ let mut entries = resolved.fs.read_dir(&resolved.delegated_path)?;
500
+ Self::rebase_entries(
501
+ &mut entries,
502
+ &resolved.delegated_path,
503
+ &Path::new("/").join(&path),
504
+ );
505
+ Ok(entries)
506
+ }
507
+ None => Err(FsError::EntryNotFound),
508
+ }
509
+ }
510
+
511
+ fn create_dir(&self, path: &Path) -> Result<()> {
512
+ let path = self.prepare_path(path)?;
513
+
514
+ if path.as_os_str().is_empty() {
515
+ return Ok(());
516
+ }
517
+
518
+ if let Some(node) = self.exact_node(&path) {
519
+ return if let Some(fs) = node.fs {
520
+ let result = fs.create_dir(Path::new("/"));
521
+
522
+ match result {
523
+ Ok(()) | Err(FsError::AlreadyExists) => Ok(()),
524
+ Err(error) if Self::should_fallback_to_synthetic_dir(&error) => Ok(()),
525
+ Err(error) => Err(error),
526
+ }
527
+ } else {
528
+ Ok(())
529
+ };
530
+ }
531
+
532
+ match self.resolve_mount(path) {
533
+ Some(resolved) => {
534
+ let result = resolved.fs.create_dir(&resolved.delegated_path);
535
+
536
+ if let Err(error) = result
537
+ && error == FsError::AlreadyExists
538
+ {
539
+ return Ok(());
540
+ }
541
+
542
+ result
543
+ }
544
+ None => Err(FsError::EntryNotFound),
545
+ }
546
+ }
547
+
548
+ fn create_symlink(&self, source: &Path, target: &Path) -> Result<()> {
549
+ let target = self.prepare_path(target)?;
550
+
551
+ if target.as_os_str().is_empty() {
552
+ return Err(FsError::AlreadyExists);
553
+ }
554
+
555
+ if self.exact_node(&target).is_some() {
556
+ return Err(FsError::AlreadyExists);
557
+ }
558
+
559
+ match self.resolve_mount(target) {
560
+ Some(resolved) => resolved.fs.create_symlink(source, &resolved.delegated_path),
561
+ None => Err(FsError::EntryNotFound),
562
+ }
563
+ }
564
+
565
+ fn hard_link(&self, source: &Path, target: &Path) -> Result<()> {
566
+ let source = self.prepare_path(source)?;
567
+ let target = self.prepare_path(target)?;
568
+
569
+ if source.as_os_str().is_empty() {
570
+ return Err(FsError::PermissionDenied);
571
+ }
572
+
573
+ if target.as_os_str().is_empty() {
574
+ return Err(FsError::AlreadyExists);
575
+ }
576
+
577
+ if let Some(node) = self.exact_node(&target)
578
+ && (node.fs.is_some() || node.has_children())
579
+ {
580
+ return Err(FsError::AlreadyExists);
581
+ }
582
+
583
+ match (self.resolve_mount(source), self.resolve_mount(target)) {
584
+ (Some(source_mount), Some(target_mount))
585
+ if source_mount.mount_path == target_mount.mount_path =>
586
+ {
587
+ source_mount
588
+ .fs
589
+ .hard_link(&source_mount.delegated_path, &target_mount.delegated_path)
590
+ }
591
+ (Some(_), Some(_)) => Err(FsError::Unsupported),
592
+ _ => Err(FsError::EntryNotFound),
593
+ }
594
+ }
595
+
596
+ fn remove_dir(&self, path: &Path) -> Result<()> {
597
+ let path = self.prepare_path(path)?;
598
+
599
+ if path.as_os_str().is_empty() {
600
+ return Err(FsError::PermissionDenied);
601
+ }
602
+
603
+ if let Some(node) = self.exact_node(&path) {
604
+ return if node.fs.is_some() || node.has_children() {
605
+ Err(FsError::PermissionDenied)
606
+ } else {
607
+ Err(FsError::EntryNotFound)
608
+ };
609
+ }
610
+
611
+ match self.resolve_mount(path) {
612
+ Some(resolved) => resolved.fs.remove_dir(&resolved.delegated_path),
613
+ None => Err(FsError::EntryNotFound),
614
+ }
615
+ }
616
+
617
+ fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> {
618
+ Box::pin(async move {
619
+ let from = self.prepare_path(from)?;
620
+ let to = self.prepare_path(to)?;
621
+
622
+ if from.as_os_str().is_empty() {
623
+ return Err(FsError::PermissionDenied);
624
+ }
625
+
626
+ if let Some(node) = self.exact_node(&from)
627
+ && (node.fs.is_some() || node.has_children())
628
+ {
629
+ return Err(FsError::PermissionDenied);
630
+ }
631
+
632
+ if let Some(node) = self.exact_node(&to)
633
+ && (node.fs.is_some() || node.has_children())
634
+ {
635
+ return Err(FsError::PermissionDenied);
636
+ }
637
+
638
+ match (self.resolve_mount(from), self.resolve_mount(to)) {
639
+ (Some(from_mount), Some(to_mount))
640
+ if from_mount.mount_path == to_mount.mount_path =>
641
+ {
642
+ from_mount
643
+ .fs
644
+ .rename(&from_mount.delegated_path, &to_mount.delegated_path)
645
+ .await
646
+ }
647
+ (Some(from_mount), Some(to_mount)) => {
648
+ ops::move_across_filesystems(
649
+ from_mount.fs.as_ref(),
650
+ to_mount.fs.as_ref(),
651
+ &from_mount.delegated_path,
652
+ &to_mount.delegated_path,
653
+ )
654
+ .await
655
+ }
656
+ _ => Err(FsError::EntryNotFound),
657
+ }
658
+ })
659
+ }
660
+
661
+ fn metadata(&self, path: &Path) -> Result<Metadata> {
662
+ let path = self.prepare_path(path)?;
663
+
664
+ if let Some(node) = self.exact_node(&path) {
665
+ return if let Some(fs) = node.fs {
666
+ fs.metadata(&node.source_path).or_else(|error| {
667
+ if Self::should_fallback_to_synthetic_dir(&error) {
668
+ Ok(Self::directory_metadata_at(node.created_at))
669
+ } else {
670
+ Err(error)
671
+ }
672
+ })
673
+ } else if node.has_children() {
674
+ Ok(Self::directory_metadata_at(node.created_at))
675
+ } else {
676
+ Err(FsError::EntryNotFound)
677
+ };
678
+ }
679
+
680
+ match self.resolve_mount(path) {
681
+ Some(resolved) => resolved.fs.metadata(&resolved.delegated_path),
682
+ None => Err(FsError::EntryNotFound),
683
+ }
684
+ }
685
+
686
+ fn symlink_metadata(&self, path: &Path) -> Result<Metadata> {
687
+ let path = self.prepare_path(path)?;
688
+
689
+ if let Some(node) = self.exact_node(&path) {
690
+ return if let Some(fs) = node.fs {
691
+ fs.symlink_metadata(&node.source_path).or_else(|error| {
692
+ if Self::should_fallback_to_synthetic_dir(&error) {
693
+ Ok(Self::directory_metadata_at(node.created_at))
694
+ } else {
695
+ Err(error)
696
+ }
697
+ })
698
+ } else if node.has_children() {
699
+ Ok(Self::directory_metadata_at(node.created_at))
700
+ } else {
701
+ Err(FsError::EntryNotFound)
702
+ };
703
+ }
704
+
705
+ match self.resolve_mount(path) {
706
+ Some(resolved) => resolved.fs.symlink_metadata(&resolved.delegated_path),
707
+ None => Err(FsError::EntryNotFound),
708
+ }
709
+ }
710
+
711
+ fn remove_file(&self, path: &Path) -> Result<()> {
712
+ let path = self.prepare_path(path)?;
713
+
714
+ if path.as_os_str().is_empty() {
715
+ return Err(FsError::NotAFile);
716
+ }
717
+
718
+ if let Some(node) = self.exact_node(&path) {
719
+ return if node.fs.is_some() || node.has_children() {
720
+ Err(FsError::PermissionDenied)
721
+ } else {
722
+ Err(FsError::EntryNotFound)
723
+ };
724
+ }
725
+
726
+ match self.resolve_mount(path) {
727
+ Some(resolved) => resolved.fs.remove_file(&resolved.delegated_path),
728
+ None => Err(FsError::EntryNotFound),
729
+ }
730
+ }
731
+
732
+ fn new_open_options(&self) -> OpenOptions<'_> {
733
+ OpenOptions::new(self)
734
+ }
735
+ }
736
+
737
+ #[derive(Debug)]
738
+ pub struct MountPointRef<'a> {
739
+ pub path: PathBuf,
740
+ pub name: String,
741
+ pub fs: Option<&'a (dyn FileSystem + Send + Sync)>,
742
+ }
743
+
744
+ impl FileOpener for MountFileSystem {
745
+ fn open(
746
+ &self,
747
+ path: &Path,
748
+ conf: &OpenOptionsConfig,
749
+ ) -> Result<Box<dyn VirtualFile + Send + Sync>> {
750
+ let path = self.prepare_path(path)?;
751
+
752
+ if path.as_os_str().is_empty() {
753
+ return Err(FsError::NotAFile);
754
+ }
755
+
756
+ if let Some(node) = self.exact_node(&path)
757
+ && node.fs.is_none()
758
+ {
759
+ return Err(FsError::NotAFile);
760
+ }
761
+
762
+ match self.resolve_mount(path) {
763
+ Some(resolved) => resolved
764
+ .fs
765
+ .new_open_options()
766
+ .options(conf.clone())
767
+ .open(resolved.delegated_path),
768
+ None => Err(FsError::EntryNotFound),
769
+ }
770
+ }
771
+ }
772
+
773
+ #[cfg(test)]
774
+ mod tests {
775
+ use std::{
776
+ collections::HashSet,
777
+ path::{Path, PathBuf},
778
+ sync::Arc,
779
+ };
780
+
781
+ use tokio::io::AsyncWriteExt;
782
+
783
+ use crate::{FileSystem as FileSystemTrait, FsError, MountFileSystem, TmpFileSystem, mem_fs};
784
+
785
+ use super::{FileOpener, OpenOptionsConfig};
786
+
787
+ #[derive(Debug, Clone, Default)]
788
+ struct MountlessFileSystem {
789
+ inner: mem_fs::FileSystem,
790
+ }
791
+
792
+ #[derive(Debug, Clone, Default)]
793
+ struct RootOpaqueFileSystem {
794
+ inner: mem_fs::FileSystem,
795
+ }
796
+
797
+ #[derive(Debug, Clone, Default)]
798
+ struct RootPermissionDeniedFileSystem;
799
+
800
+ impl FileSystemTrait for MountlessFileSystem {
801
+ fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
802
+ self.inner.readlink(path)
803
+ }
804
+
805
+ fn read_dir(&self, path: &Path) -> crate::Result<crate::ReadDir> {
806
+ self.inner.read_dir(path)
807
+ }
808
+
809
+ fn create_dir(&self, path: &Path) -> crate::Result<()> {
810
+ self.inner.create_dir(path)
811
+ }
812
+
813
+ fn remove_dir(&self, path: &Path) -> crate::Result<()> {
814
+ self.inner.remove_dir(path)
815
+ }
816
+
817
+ fn rename<'a>(
818
+ &'a self,
819
+ from: &'a Path,
820
+ to: &'a Path,
821
+ ) -> futures::future::BoxFuture<'a, crate::Result<()>> {
822
+ Box::pin(async move { self.inner.rename(from, to).await })
823
+ }
824
+
825
+ fn metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
826
+ self.inner.metadata(path)
827
+ }
828
+
829
+ fn symlink_metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
830
+ self.inner.symlink_metadata(path)
831
+ }
832
+
833
+ fn remove_file(&self, path: &Path) -> crate::Result<()> {
834
+ self.inner.remove_file(path)
835
+ }
836
+
837
+ fn new_open_options(&self) -> crate::OpenOptions<'_> {
838
+ self.inner.new_open_options()
839
+ }
840
+ }
841
+
842
+ impl FileOpener for MountlessFileSystem {
843
+ fn open(
844
+ &self,
845
+ path: &Path,
846
+ conf: &OpenOptionsConfig,
847
+ ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync>> {
848
+ self.inner
849
+ .new_open_options()
850
+ .options(conf.clone())
851
+ .open(path)
852
+ }
853
+ }
854
+
855
+ impl FileSystemTrait for RootOpaqueFileSystem {
856
+ fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
857
+ self.inner.readlink(path)
858
+ }
859
+
860
+ fn read_dir(&self, path: &Path) -> crate::Result<crate::ReadDir> {
861
+ if path == Path::new("/") {
862
+ Err(FsError::Unsupported)
863
+ } else {
864
+ self.inner.read_dir(path)
865
+ }
866
+ }
867
+
868
+ fn create_dir(&self, path: &Path) -> crate::Result<()> {
869
+ self.inner.create_dir(path)
870
+ }
871
+
872
+ fn remove_dir(&self, path: &Path) -> crate::Result<()> {
873
+ self.inner.remove_dir(path)
874
+ }
875
+
876
+ fn rename<'a>(
877
+ &'a self,
878
+ from: &'a Path,
879
+ to: &'a Path,
880
+ ) -> futures::future::BoxFuture<'a, crate::Result<()>> {
881
+ Box::pin(async move { self.inner.rename(from, to).await })
882
+ }
883
+
884
+ fn metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
885
+ if path == Path::new("/") {
886
+ Err(FsError::Unsupported)
887
+ } else {
888
+ self.inner.metadata(path)
889
+ }
890
+ }
891
+
892
+ fn symlink_metadata(&self, path: &Path) -> crate::Result<crate::Metadata> {
893
+ if path == Path::new("/") {
894
+ Err(FsError::Unsupported)
895
+ } else {
896
+ self.inner.symlink_metadata(path)
897
+ }
898
+ }
899
+
900
+ fn remove_file(&self, path: &Path) -> crate::Result<()> {
901
+ self.inner.remove_file(path)
902
+ }
903
+
904
+ fn new_open_options(&self) -> crate::OpenOptions<'_> {
905
+ self.inner.new_open_options()
906
+ }
907
+ }
908
+
909
+ impl FileOpener for RootOpaqueFileSystem {
910
+ fn open(
911
+ &self,
912
+ path: &Path,
913
+ conf: &OpenOptionsConfig,
914
+ ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync>> {
915
+ self.inner
916
+ .new_open_options()
917
+ .options(conf.clone())
918
+ .open(path)
919
+ }
920
+ }
921
+
922
+ impl FileSystemTrait for RootPermissionDeniedFileSystem {
923
+ fn readlink(&self, _path: &Path) -> crate::Result<PathBuf> {
924
+ Err(FsError::PermissionDenied)
925
+ }
926
+
927
+ fn read_dir(&self, _path: &Path) -> crate::Result<crate::ReadDir> {
928
+ Err(FsError::PermissionDenied)
929
+ }
930
+
931
+ fn create_dir(&self, _path: &Path) -> crate::Result<()> {
932
+ Err(FsError::PermissionDenied)
933
+ }
934
+
935
+ fn remove_dir(&self, _path: &Path) -> crate::Result<()> {
936
+ Err(FsError::PermissionDenied)
937
+ }
938
+
939
+ fn rename<'a>(
940
+ &'a self,
941
+ _from: &'a Path,
942
+ _to: &'a Path,
943
+ ) -> futures::future::BoxFuture<'a, crate::Result<()>> {
944
+ Box::pin(async { Err(FsError::PermissionDenied) })
945
+ }
946
+
947
+ fn metadata(&self, _path: &Path) -> crate::Result<crate::Metadata> {
948
+ Err(FsError::PermissionDenied)
949
+ }
950
+
951
+ fn symlink_metadata(&self, _path: &Path) -> crate::Result<crate::Metadata> {
952
+ Err(FsError::PermissionDenied)
953
+ }
954
+
955
+ fn remove_file(&self, _path: &Path) -> crate::Result<()> {
956
+ Err(FsError::PermissionDenied)
957
+ }
958
+
959
+ fn new_open_options(&self) -> crate::OpenOptions<'_> {
960
+ crate::OpenOptions::new(self)
961
+ }
962
+ }
963
+
964
+ impl FileOpener for RootPermissionDeniedFileSystem {
965
+ fn open(
966
+ &self,
967
+ _path: &Path,
968
+ _conf: &OpenOptionsConfig,
969
+ ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync>> {
970
+ Err(FsError::PermissionDenied)
971
+ }
972
+ }
973
+
974
+ fn gen_filesystem() -> MountFileSystem {
975
+ let union = MountFileSystem::new();
976
+ let a = mem_fs::FileSystem::default();
977
+ let b = mem_fs::FileSystem::default();
978
+ let c = mem_fs::FileSystem::default();
979
+ let d = mem_fs::FileSystem::default();
980
+ let e = mem_fs::FileSystem::default();
981
+ let f = mem_fs::FileSystem::default();
982
+ let g = mem_fs::FileSystem::default();
983
+ let h = mem_fs::FileSystem::default();
984
+
985
+ union
986
+ .mount(PathBuf::from("/test_new_filesystem").as_path(), Arc::new(a))
987
+ .unwrap();
988
+ union
989
+ .mount(PathBuf::from("/test_create_dir").as_path(), Arc::new(b))
990
+ .unwrap();
991
+ union
992
+ .mount(PathBuf::from("/test_remove_dir").as_path(), Arc::new(c))
993
+ .unwrap();
994
+ union
995
+ .mount(PathBuf::from("/test_rename").as_path(), Arc::new(d))
996
+ .unwrap();
997
+ union
998
+ .mount(PathBuf::from("/test_metadata").as_path(), Arc::new(e))
999
+ .unwrap();
1000
+ union
1001
+ .mount(PathBuf::from("/test_remove_file").as_path(), Arc::new(f))
1002
+ .unwrap();
1003
+ union
1004
+ .mount(PathBuf::from("/test_readdir").as_path(), Arc::new(g))
1005
+ .unwrap();
1006
+ union
1007
+ .mount(PathBuf::from("/test_canonicalize").as_path(), Arc::new(h))
1008
+ .unwrap();
1009
+
1010
+ union
1011
+ }
1012
+
1013
+ fn gen_nested_filesystem() -> MountFileSystem {
1014
+ let union = MountFileSystem::new();
1015
+ let a = mem_fs::FileSystem::default();
1016
+ a.open(
1017
+ &PathBuf::from("/data-a.txt"),
1018
+ &OpenOptionsConfig {
1019
+ read: true,
1020
+ write: true,
1021
+ create_new: false,
1022
+ create: true,
1023
+ append: false,
1024
+ truncate: false,
1025
+ },
1026
+ )
1027
+ .unwrap();
1028
+ let b = mem_fs::FileSystem::default();
1029
+ b.open(
1030
+ &PathBuf::from("/data-b.txt"),
1031
+ &OpenOptionsConfig {
1032
+ read: true,
1033
+ write: true,
1034
+ create_new: false,
1035
+ create: true,
1036
+ append: false,
1037
+ truncate: false,
1038
+ },
1039
+ )
1040
+ .unwrap();
1041
+
1042
+ union
1043
+ .mount(PathBuf::from("/app/a").as_path(), Arc::new(a))
1044
+ .unwrap();
1045
+ union
1046
+ .mount(PathBuf::from("/app/b").as_path(), Arc::new(b))
1047
+ .unwrap();
1048
+
1049
+ union
1050
+ }
1051
+
1052
+ #[tokio::test]
1053
+ async fn test_nested_read_dir() {
1054
+ let fs = gen_nested_filesystem();
1055
+
1056
+ let root_contents: Vec<PathBuf> = fs
1057
+ .read_dir(&PathBuf::from("/"))
1058
+ .unwrap()
1059
+ .map(|e| e.unwrap().path.clone())
1060
+ .collect();
1061
+ assert_eq!(root_contents, vec![PathBuf::from("/app")]);
1062
+
1063
+ let app_contents: HashSet<PathBuf> = fs
1064
+ .read_dir(&PathBuf::from("/app"))
1065
+ .unwrap()
1066
+ .map(|e| e.unwrap().path)
1067
+ .collect();
1068
+ assert_eq!(
1069
+ app_contents,
1070
+ HashSet::from_iter([PathBuf::from("/app/a"), PathBuf::from("/app/b")].into_iter())
1071
+ );
1072
+
1073
+ let a_contents: Vec<PathBuf> = fs
1074
+ .read_dir(&PathBuf::from("/app/a"))
1075
+ .unwrap()
1076
+ .map(|e| e.unwrap().path.clone())
1077
+ .collect();
1078
+ assert_eq!(a_contents, vec![PathBuf::from("/app/a/data-a.txt")]);
1079
+
1080
+ let b_contents: Vec<PathBuf> = fs
1081
+ .read_dir(&PathBuf::from("/app/b"))
1082
+ .unwrap()
1083
+ .map(|e| e.unwrap().path)
1084
+ .collect();
1085
+ assert_eq!(b_contents, vec![PathBuf::from("/app/b/data-b.txt")]);
1086
+ }
1087
+
1088
+ #[tokio::test]
1089
+ async fn test_nested_metadata() {
1090
+ let fs = gen_nested_filesystem();
1091
+
1092
+ assert!(fs.metadata(&PathBuf::from("/")).is_ok());
1093
+ assert!(fs.metadata(&PathBuf::from("/app")).is_ok());
1094
+ assert!(fs.metadata(&PathBuf::from("/app/a")).is_ok());
1095
+ assert!(fs.metadata(&PathBuf::from("/app/b")).is_ok());
1096
+ assert!(fs.metadata(&PathBuf::from("/app/a/data-a.txt")).is_ok());
1097
+ assert!(fs.metadata(&PathBuf::from("/app/b/data-b.txt")).is_ok());
1098
+ }
1099
+
1100
+ #[tokio::test]
1101
+ async fn test_nested_symlink_metadata() {
1102
+ let fs = gen_nested_filesystem();
1103
+
1104
+ assert!(fs.symlink_metadata(&PathBuf::from("/")).is_ok());
1105
+ assert!(fs.symlink_metadata(&PathBuf::from("/app")).is_ok());
1106
+ assert!(fs.symlink_metadata(&PathBuf::from("/app/a")).is_ok());
1107
+ assert!(fs.symlink_metadata(&PathBuf::from("/app/b")).is_ok());
1108
+ assert!(
1109
+ fs.symlink_metadata(&PathBuf::from("/app/a/data-a.txt"))
1110
+ .is_ok()
1111
+ );
1112
+ assert!(
1113
+ fs.symlink_metadata(&PathBuf::from("/app/b/data-b.txt"))
1114
+ .is_ok()
1115
+ );
1116
+ }
1117
+
1118
+ #[tokio::test]
1119
+ async fn test_import_mounts_preserves_nested_root_mounts() {
1120
+ let primary = MountFileSystem::new();
1121
+ let openssl = mem_fs::FileSystem::default();
1122
+ openssl.create_dir(Path::new("/certs")).unwrap();
1123
+ openssl
1124
+ .new_open_options()
1125
+ .write(true)
1126
+ .create_new(true)
1127
+ .open(Path::new("/certs/ca.pem"))
1128
+ .unwrap();
1129
+ primary
1130
+ .mount(Path::new("/openssl"), Arc::new(openssl))
1131
+ .unwrap();
1132
+
1133
+ let injected = MountFileSystem::new();
1134
+ let app = mem_fs::FileSystem::default();
1135
+ app.new_open_options()
1136
+ .write(true)
1137
+ .create_new(true)
1138
+ .open(Path::new("/index.php"))
1139
+ .unwrap();
1140
+ injected.mount(Path::new("/app"), Arc::new(app)).unwrap();
1141
+
1142
+ let assets = mem_fs::FileSystem::default();
1143
+ assets.create_dir(Path::new("/css")).unwrap();
1144
+ assets
1145
+ .new_open_options()
1146
+ .write(true)
1147
+ .create_new(true)
1148
+ .open(Path::new("/css/site.css"))
1149
+ .unwrap();
1150
+ injected
1151
+ .mount(Path::new("/opt/assets"), Arc::new(assets))
1152
+ .unwrap();
1153
+
1154
+ primary
1155
+ .add_mount_entries_with_mode(
1156
+ injected.mount_entries(),
1157
+ super::ExactMountConflictMode::Fail,
1158
+ )
1159
+ .unwrap();
1160
+
1161
+ let root_contents = read_dir_names(&primary, "/");
1162
+ assert!(root_contents.contains(&"app".to_string()));
1163
+ assert!(root_contents.contains(&"opt".to_string()));
1164
+ assert!(root_contents.contains(&"openssl".to_string()));
1165
+ assert!(primary.metadata(Path::new("/app/index.php")).is_ok());
1166
+ assert!(
1167
+ primary
1168
+ .metadata(Path::new("/opt/assets/css/site.css"))
1169
+ .is_ok()
1170
+ );
1171
+ assert!(primary.metadata(Path::new("/openssl/certs/ca.pem")).is_ok());
1172
+ }
1173
+
1174
+ #[tokio::test]
1175
+ async fn test_nested_mount_under_non_mountable_leaf_is_supported() {
1176
+ let fs = MountFileSystem::new();
1177
+
1178
+ let top = MountlessFileSystem::default();
1179
+ top.create_dir(Path::new("/bin")).unwrap();
1180
+ top.new_open_options()
1181
+ .write(true)
1182
+ .create_new(true)
1183
+ .open(Path::new("/bin/tool"))
1184
+ .unwrap();
1185
+
1186
+ let nested = mem_fs::FileSystem::default();
1187
+ nested.create_dir(Path::new("/css")).unwrap();
1188
+ nested
1189
+ .new_open_options()
1190
+ .write(true)
1191
+ .create_new(true)
1192
+ .open(Path::new("/css/site.css"))
1193
+ .unwrap();
1194
+
1195
+ fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1196
+ fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1197
+ .unwrap();
1198
+
1199
+ assert!(fs.metadata(Path::new("/opt/bin/tool")).is_ok());
1200
+ assert!(fs.metadata(Path::new("/opt/assets/css/site.css")).is_ok());
1201
+ }
1202
+
1203
+ #[tokio::test]
1204
+ async fn test_normalized_paths_still_route_to_deepest_mount() {
1205
+ let fs = MountFileSystem::new();
1206
+
1207
+ let top = MountlessFileSystem::default();
1208
+ top.create_dir(Path::new("/bin")).unwrap();
1209
+ top.new_open_options()
1210
+ .write(true)
1211
+ .create_new(true)
1212
+ .open(Path::new("/bin/tool"))
1213
+ .unwrap();
1214
+
1215
+ let nested = mem_fs::FileSystem::default();
1216
+ nested.create_dir(Path::new("/css")).unwrap();
1217
+ nested
1218
+ .new_open_options()
1219
+ .write(true)
1220
+ .create_new(true)
1221
+ .open(Path::new("/css/site.css"))
1222
+ .unwrap();
1223
+
1224
+ fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1225
+ fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1226
+ .unwrap();
1227
+
1228
+ assert!(
1229
+ fs.metadata(Path::new("/opt/./assets/../assets/css/site.css"))
1230
+ .unwrap()
1231
+ .is_file()
1232
+ );
1233
+ }
1234
+
1235
+ #[tokio::test]
1236
+ async fn test_invalid_above_root_path_is_rejected() {
1237
+ let fs = MountFileSystem::new();
1238
+ fs.mount(Path::new("/"), Arc::new(mem_fs::FileSystem::default()))
1239
+ .unwrap();
1240
+
1241
+ assert_eq!(fs.metadata(Path::new("../foo")), Err(FsError::InvalidInput));
1242
+ }
1243
+
1244
+ #[tokio::test]
1245
+ async fn test_exact_mount_metadata_falls_back_to_synthetic_directory() {
1246
+ let fs = MountFileSystem::new();
1247
+ fs.mount(
1248
+ Path::new("/opaque"),
1249
+ Arc::new(RootOpaqueFileSystem::default()),
1250
+ )
1251
+ .unwrap();
1252
+
1253
+ let meta1 = fs.metadata(Path::new("/opaque")).unwrap();
1254
+ let sym1 = fs.symlink_metadata(Path::new("/opaque")).unwrap();
1255
+ assert!(meta1.is_dir());
1256
+ assert!(sym1.is_dir());
1257
+
1258
+ // Timestamps must be non-zero (regression guard against the old `0` placeholders).
1259
+ assert!(meta1.created > 0, "created timestamp must be non-zero");
1260
+ assert!(meta1.modified > 0, "modified timestamp must be non-zero");
1261
+ assert!(meta1.accessed > 0, "accessed timestamp must be non-zero");
1262
+
1263
+ // Repeated calls must return the same stable timestamps (not re-sampled each time).
1264
+ let meta2 = fs.metadata(Path::new("/opaque")).unwrap();
1265
+ assert_eq!(meta1.created, meta2.created, "created must be stable");
1266
+ assert_eq!(meta1.modified, meta2.modified, "modified must be stable");
1267
+ assert_eq!(meta1.accessed, meta2.accessed, "accessed must be stable");
1268
+
1269
+ assert_eq!(fs.create_dir(Path::new("/opaque")), Ok(()));
1270
+ }
1271
+
1272
+ #[tokio::test]
1273
+ async fn test_exact_mount_read_dir_falls_back_to_child_mounts_when_root_is_unlistable() {
1274
+ let fs = MountFileSystem::new();
1275
+ fs.mount(
1276
+ Path::new("/opaque"),
1277
+ Arc::new(RootOpaqueFileSystem::default()),
1278
+ )
1279
+ .unwrap();
1280
+ fs.mount(
1281
+ Path::new("/opaque/assets"),
1282
+ Arc::new(mem_fs::FileSystem::default()),
1283
+ )
1284
+ .unwrap();
1285
+
1286
+ assert_eq!(read_dir_names(&fs, "/opaque"), vec!["assets".to_string()]);
1287
+ }
1288
+
1289
+ #[tokio::test]
1290
+ async fn test_exact_mount_fallback_does_not_mask_permission_denied() {
1291
+ let fs = MountFileSystem::new();
1292
+ fs.mount(
1293
+ Path::new("/denied"),
1294
+ Arc::new(RootPermissionDeniedFileSystem),
1295
+ )
1296
+ .unwrap();
1297
+ fs.mount(
1298
+ Path::new("/denied/assets"),
1299
+ Arc::new(mem_fs::FileSystem::default()),
1300
+ )
1301
+ .unwrap();
1302
+
1303
+ assert_eq!(
1304
+ fs.metadata(Path::new("/denied")),
1305
+ Err(FsError::PermissionDenied)
1306
+ );
1307
+ assert_eq!(
1308
+ fs.symlink_metadata(Path::new("/denied")),
1309
+ Err(FsError::PermissionDenied)
1310
+ );
1311
+ assert_eq!(
1312
+ fs.read_dir(Path::new("/denied")).map(|_| ()),
1313
+ Err(FsError::PermissionDenied)
1314
+ );
1315
+ assert_eq!(
1316
+ fs.create_dir(Path::new("/denied")),
1317
+ Err(FsError::PermissionDenied)
1318
+ );
1319
+ }
1320
+
1321
+ #[tokio::test]
1322
+ async fn test_keep_existing_conflict_skips_the_other_subtree() {
1323
+ let primary = MountFileSystem::new();
1324
+ let user_mount = mem_fs::FileSystem::default();
1325
+ user_mount
1326
+ .new_open_options()
1327
+ .write(true)
1328
+ .create_new(true)
1329
+ .open(Path::new("/user.txt"))
1330
+ .unwrap();
1331
+ primary
1332
+ .mount(Path::new("/python"), Arc::new(user_mount))
1333
+ .unwrap();
1334
+
1335
+ let injected = MountFileSystem::new();
1336
+ let package_mount = mem_fs::FileSystem::default();
1337
+ package_mount
1338
+ .new_open_options()
1339
+ .write(true)
1340
+ .create_new(true)
1341
+ .open(Path::new("/pkg.txt"))
1342
+ .unwrap();
1343
+ injected
1344
+ .mount(Path::new("/python"), Arc::new(package_mount))
1345
+ .unwrap();
1346
+
1347
+ let package_child = mem_fs::FileSystem::default();
1348
+ package_child
1349
+ .new_open_options()
1350
+ .write(true)
1351
+ .create_new(true)
1352
+ .open(Path::new("/child.txt"))
1353
+ .unwrap();
1354
+ injected
1355
+ .mount(Path::new("/python/lib"), Arc::new(package_child))
1356
+ .unwrap();
1357
+
1358
+ primary
1359
+ .add_mount_entries_with_mode(
1360
+ injected.mount_entries(),
1361
+ super::ExactMountConflictMode::KeepExisting,
1362
+ )
1363
+ .unwrap();
1364
+
1365
+ assert!(
1366
+ primary
1367
+ .metadata(Path::new("/python/user.txt"))
1368
+ .unwrap()
1369
+ .is_file()
1370
+ );
1371
+ assert_eq!(
1372
+ primary.metadata(Path::new("/python/pkg.txt")),
1373
+ Err(FsError::EntryNotFound)
1374
+ );
1375
+ assert_eq!(
1376
+ primary.metadata(Path::new("/python/lib/child.txt")),
1377
+ Err(FsError::EntryNotFound)
1378
+ );
1379
+ }
1380
+
1381
+ #[tokio::test]
1382
+ async fn test_replace_existing_conflict_replaces_the_whole_subtree() {
1383
+ let primary = MountFileSystem::new();
1384
+ let user_mount = mem_fs::FileSystem::default();
1385
+ user_mount
1386
+ .new_open_options()
1387
+ .write(true)
1388
+ .create_new(true)
1389
+ .open(Path::new("/user.txt"))
1390
+ .unwrap();
1391
+ let user_child = mem_fs::FileSystem::default();
1392
+ user_child
1393
+ .new_open_options()
1394
+ .write(true)
1395
+ .create_new(true)
1396
+ .open(Path::new("/user-child.txt"))
1397
+ .unwrap();
1398
+ primary
1399
+ .mount(Path::new("/python"), Arc::new(user_mount))
1400
+ .unwrap();
1401
+ primary
1402
+ .mount(Path::new("/python/lib"), Arc::new(user_child))
1403
+ .unwrap();
1404
+
1405
+ let injected = MountFileSystem::new();
1406
+ let package_mount = mem_fs::FileSystem::default();
1407
+ package_mount
1408
+ .new_open_options()
1409
+ .write(true)
1410
+ .create_new(true)
1411
+ .open(Path::new("/pkg.txt"))
1412
+ .unwrap();
1413
+ let package_child = mem_fs::FileSystem::default();
1414
+ package_child
1415
+ .new_open_options()
1416
+ .write(true)
1417
+ .create_new(true)
1418
+ .open(Path::new("/pkg-child.txt"))
1419
+ .unwrap();
1420
+ injected
1421
+ .mount(Path::new("/python"), Arc::new(package_mount))
1422
+ .unwrap();
1423
+ injected
1424
+ .mount(Path::new("/python/lib"), Arc::new(package_child))
1425
+ .unwrap();
1426
+
1427
+ primary
1428
+ .add_mount_entries_with_mode(
1429
+ injected.mount_entries(),
1430
+ super::ExactMountConflictMode::ReplaceExisting,
1431
+ )
1432
+ .unwrap();
1433
+
1434
+ assert_eq!(
1435
+ primary.metadata(Path::new("/python/user.txt")),
1436
+ Err(FsError::EntryNotFound)
1437
+ );
1438
+ assert_eq!(
1439
+ primary.metadata(Path::new("/python/lib/user-child.txt")),
1440
+ Err(FsError::EntryNotFound)
1441
+ );
1442
+ assert!(
1443
+ primary
1444
+ .metadata(Path::new("/python/pkg.txt"))
1445
+ .unwrap()
1446
+ .is_file()
1447
+ );
1448
+ assert!(
1449
+ primary
1450
+ .metadata(Path::new("/python/lib/pkg-child.txt"))
1451
+ .unwrap()
1452
+ .is_file()
1453
+ );
1454
+ }
1455
+
1456
+ #[tokio::test]
1457
+ async fn test_exact_mountpoints_reject_destructive_mutation() {
1458
+ let fs = MountFileSystem::new();
1459
+ let mounted = mem_fs::FileSystem::default();
1460
+ mounted.create_dir(Path::new("/dir")).unwrap();
1461
+ mounted
1462
+ .new_open_options()
1463
+ .write(true)
1464
+ .create_new(true)
1465
+ .open(Path::new("/file.txt"))
1466
+ .unwrap();
1467
+
1468
+ fs.mount(Path::new("/mounted"), Arc::new(mounted)).unwrap();
1469
+
1470
+ assert_eq!(
1471
+ fs.remove_dir(Path::new("/mounted")),
1472
+ Err(FsError::PermissionDenied)
1473
+ );
1474
+ assert_eq!(
1475
+ fs.remove_file(Path::new("/mounted")),
1476
+ Err(FsError::PermissionDenied)
1477
+ );
1478
+ assert_eq!(
1479
+ fs.rename(Path::new("/mounted"), Path::new("/other")).await,
1480
+ Err(FsError::PermissionDenied)
1481
+ );
1482
+ assert_eq!(
1483
+ fs.rename(Path::new("/mounted/file.txt"), Path::new("/mounted"))
1484
+ .await,
1485
+ Err(FsError::PermissionDenied)
1486
+ );
1487
+ }
1488
+
1489
+ #[tokio::test]
1490
+ async fn test_parent_read_dir_merges_leaf_entries_with_child_mounts() {
1491
+ let fs = MountFileSystem::new();
1492
+
1493
+ let top = MountlessFileSystem::default();
1494
+ top.create_dir(Path::new("/bin")).unwrap();
1495
+ top.new_open_options()
1496
+ .write(true)
1497
+ .create_new(true)
1498
+ .open(Path::new("/bin/tool"))
1499
+ .unwrap();
1500
+
1501
+ let nested = mem_fs::FileSystem::default();
1502
+ nested.create_dir(Path::new("/css")).unwrap();
1503
+ nested
1504
+ .new_open_options()
1505
+ .write(true)
1506
+ .create_new(true)
1507
+ .open(Path::new("/css/site.css"))
1508
+ .unwrap();
1509
+
1510
+ fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1511
+ fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1512
+ .unwrap();
1513
+
1514
+ let opt_contents = read_dir_names(&fs, "/opt");
1515
+ assert!(opt_contents.contains(&"bin".to_string()));
1516
+ assert!(opt_contents.contains(&"assets".to_string()));
1517
+ }
1518
+
1519
+ #[tokio::test]
1520
+ async fn test_child_mount_shadows_same_named_parent_entry() {
1521
+ let fs = MountFileSystem::new();
1522
+
1523
+ let top = MountlessFileSystem::default();
1524
+ top.new_open_options()
1525
+ .write(true)
1526
+ .create_new(true)
1527
+ .open(Path::new("/assets"))
1528
+ .unwrap();
1529
+
1530
+ let nested = mem_fs::FileSystem::default();
1531
+ nested.create_dir(Path::new("/css")).unwrap();
1532
+ nested
1533
+ .new_open_options()
1534
+ .write(true)
1535
+ .create_new(true)
1536
+ .open(Path::new("/css/site.css"))
1537
+ .unwrap();
1538
+
1539
+ fs.mount(Path::new("/opt"), Arc::new(top)).unwrap();
1540
+ fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1541
+ .unwrap();
1542
+
1543
+ assert!(fs.metadata(Path::new("/opt/assets")).unwrap().is_dir());
1544
+ assert_eq!(
1545
+ read_dir_names(&fs, "/opt")
1546
+ .into_iter()
1547
+ .filter(|entry| entry == "assets")
1548
+ .count(),
1549
+ 1,
1550
+ );
1551
+ assert!(fs.metadata(Path::new("/opt/assets/css/site.css")).is_ok());
1552
+ }
1553
+
1554
+ #[tokio::test]
1555
+ async fn test_read_dir_rebases_entries_under_nested_mount_subdirectory() {
1556
+ let fs = MountFileSystem::new();
1557
+
1558
+ let nested = mem_fs::FileSystem::default();
1559
+ nested.create_dir(Path::new("/css")).unwrap();
1560
+ nested
1561
+ .new_open_options()
1562
+ .write(true)
1563
+ .create_new(true)
1564
+ .open(Path::new("/css/site.css"))
1565
+ .unwrap();
1566
+
1567
+ fs.mount(Path::new("/opt/assets"), Arc::new(nested))
1568
+ .unwrap();
1569
+
1570
+ let css_contents: Vec<PathBuf> = fs
1571
+ .read_dir(Path::new("/opt/assets/css"))
1572
+ .unwrap()
1573
+ .map(|entry| entry.unwrap().path)
1574
+ .collect();
1575
+
1576
+ assert_eq!(
1577
+ css_contents,
1578
+ vec![PathBuf::from("/opt/assets/css/site.css")]
1579
+ );
1580
+ }
1581
+
1582
+ #[tokio::test]
1583
+ async fn test_mount_with_source_path_exposes_subtree() {
1584
+ let fs = MountFileSystem::new();
1585
+
1586
+ let source = mem_fs::FileSystem::default();
1587
+ source.create_dir(Path::new("/python")).unwrap();
1588
+ source
1589
+ .new_open_options()
1590
+ .write(true)
1591
+ .create_new(true)
1592
+ .open(Path::new("/python/lib.py"))
1593
+ .unwrap();
1594
+
1595
+ fs.mount_with_source(
1596
+ Path::new("/runtime"),
1597
+ Path::new("/python"),
1598
+ Arc::new(source),
1599
+ )
1600
+ .unwrap();
1601
+
1602
+ assert!(fs.metadata(Path::new("/runtime/lib.py")).unwrap().is_file());
1603
+ assert_eq!(read_dir_names(&fs, "/runtime"), vec!["lib.py".to_string()]);
1604
+ }
1605
+
1606
+ #[tokio::test]
1607
+ async fn test_nested_mount_inside_tree_preserves_sibling_files() {
1608
+ let fs = MountFileSystem::new();
1609
+
1610
+ let python = mem_fs::FileSystem::default();
1611
+ create_dir_all(&python, Path::new("/usr/local/lib/python3.13/encodings"));
1612
+ python
1613
+ .new_open_options()
1614
+ .write(true)
1615
+ .create_new(true)
1616
+ .open(Path::new("/usr/local/lib/python3.13/encodings/__init__.py"))
1617
+ .unwrap();
1618
+
1619
+ let host = mem_fs::FileSystem::default();
1620
+ host.new_open_options()
1621
+ .write(true)
1622
+ .create_new(true)
1623
+ .open(Path::new("/marker.txt"))
1624
+ .unwrap();
1625
+
1626
+ fs.mount(Path::new("/"), Arc::new(python)).unwrap();
1627
+ fs.mount(Path::new("/usr/local/lib/python3.13/test"), Arc::new(host))
1628
+ .unwrap();
1629
+
1630
+ assert!(
1631
+ fs.metadata(Path::new("/usr/local/lib/python3.13/encodings/__init__.py"))
1632
+ .unwrap()
1633
+ .is_file()
1634
+ );
1635
+ assert!(
1636
+ fs.metadata(Path::new("/usr/local/lib/python3.13/test/marker.txt"))
1637
+ .unwrap()
1638
+ .is_file()
1639
+ );
1640
+
1641
+ fs.new_open_options()
1642
+ .read(true)
1643
+ .open(Path::new("/usr/local/lib/python3.13/encodings/__init__.py"))
1644
+ .unwrap();
1645
+ fs.new_open_options()
1646
+ .read(true)
1647
+ .open(Path::new("/usr/local/lib/python3.13/test/marker.txt"))
1648
+ .unwrap();
1649
+
1650
+ let mut entries = read_dir_names(&fs, "/usr/local/lib/python3.13");
1651
+ entries.sort();
1652
+ assert_eq!(entries, vec!["encodings".to_string(), "test".to_string()]);
1653
+ }
1654
+
1655
+ #[tokio::test]
1656
+ async fn test_synthetic_parent_without_backing_dir_lists_child_mount() {
1657
+ let fs = MountFileSystem::new();
1658
+ fs.mount(Path::new("/"), Arc::new(mem_fs::FileSystem::default()))
1659
+ .unwrap();
1660
+
1661
+ let child = mem_fs::FileSystem::default();
1662
+ child
1663
+ .new_open_options()
1664
+ .write(true)
1665
+ .create_new(true)
1666
+ .open(Path::new("/marker.txt"))
1667
+ .unwrap();
1668
+ fs.mount(Path::new("/foo/bar"), Arc::new(child)).unwrap();
1669
+
1670
+ let entries = read_dir_names(&fs, "/foo");
1671
+ assert_eq!(entries, vec!["bar".to_string()]);
1672
+ }
1673
+
1674
+ #[tokio::test]
1675
+ async fn test_import_mounts_allows_shared_prefix_without_exact_mount_conflict() {
1676
+ let primary = MountFileSystem::new();
1677
+ let bin = mem_fs::FileSystem::default();
1678
+ bin.new_open_options()
1679
+ .write(true)
1680
+ .create_new(true)
1681
+ .open(Path::new("/tool"))
1682
+ .unwrap();
1683
+ primary.mount(Path::new("/opt/bin"), Arc::new(bin)).unwrap();
1684
+
1685
+ let injected = MountFileSystem::new();
1686
+ let assets = mem_fs::FileSystem::default();
1687
+ assets
1688
+ .new_open_options()
1689
+ .write(true)
1690
+ .create_new(true)
1691
+ .open(Path::new("/logo.svg"))
1692
+ .unwrap();
1693
+ injected
1694
+ .mount(Path::new("/opt/assets"), Arc::new(assets))
1695
+ .unwrap();
1696
+
1697
+ primary
1698
+ .add_mount_entries_with_mode(
1699
+ injected.mount_entries(),
1700
+ super::ExactMountConflictMode::Fail,
1701
+ )
1702
+ .unwrap();
1703
+
1704
+ assert!(primary.metadata(Path::new("/opt/bin/tool")).is_ok());
1705
+ assert!(primary.metadata(Path::new("/opt/assets/logo.svg")).is_ok());
1706
+ }
1707
+
1708
+ #[tokio::test]
1709
+ async fn test_import_mounts_rejects_exact_mount_conflict() {
1710
+ let primary = MountFileSystem::new();
1711
+ primary
1712
+ .mount(
1713
+ Path::new("/opt/bin"),
1714
+ Arc::new(mem_fs::FileSystem::default()),
1715
+ )
1716
+ .unwrap();
1717
+
1718
+ let injected = MountFileSystem::new();
1719
+ injected
1720
+ .mount(
1721
+ Path::new("/opt/bin"),
1722
+ Arc::new(mem_fs::FileSystem::default()),
1723
+ )
1724
+ .unwrap();
1725
+
1726
+ assert_eq!(
1727
+ primary.add_mount_entries_with_mode(
1728
+ injected.mount_entries(),
1729
+ super::ExactMountConflictMode::Fail,
1730
+ ),
1731
+ Err(FsError::AlreadyExists)
1732
+ );
1733
+ }
1734
+
1735
+ #[tokio::test]
1736
+ async fn test_new_filesystem() {
1737
+ let fs = gen_filesystem();
1738
+ assert!(
1739
+ fs.read_dir(Path::new("/test_new_filesystem")).is_ok(),
1740
+ "hostfs can read root"
1741
+ );
1742
+ let mut file_write = fs
1743
+ .new_open_options()
1744
+ .read(true)
1745
+ .write(true)
1746
+ .create_new(true)
1747
+ .open(Path::new("/test_new_filesystem/foo2.txt"))
1748
+ .unwrap();
1749
+ file_write.write_all(b"hello").await.unwrap();
1750
+ let _ = std::fs::remove_file("/test_new_filesystem/foo2.txt");
1751
+ }
1752
+
1753
+ #[tokio::test]
1754
+ async fn test_create_dir() {
1755
+ let fs = gen_filesystem();
1756
+
1757
+ assert_eq!(fs.create_dir(Path::new("/")), Ok(()));
1758
+
1759
+ assert_eq!(fs.create_dir(Path::new("/test_create_dir")), Ok(()));
1760
+
1761
+ assert_eq!(
1762
+ fs.create_dir(Path::new("/test_create_dir/foo")),
1763
+ Ok(()),
1764
+ "creating a directory",
1765
+ );
1766
+
1767
+ let cur_dir = read_dir_names(&fs, "/test_create_dir");
1768
+
1769
+ if !cur_dir.contains(&"foo".to_string()) {
1770
+ panic!("cur_dir does not contain foo: {cur_dir:#?}");
1771
+ }
1772
+
1773
+ assert!(
1774
+ cur_dir.contains(&"foo".to_string()),
1775
+ "the root is updated and well-defined"
1776
+ );
1777
+
1778
+ assert_eq!(
1779
+ fs.create_dir(Path::new("/test_create_dir/foo/bar")),
1780
+ Ok(()),
1781
+ "creating a sub-directory",
1782
+ );
1783
+
1784
+ let foo_dir = read_dir_names(&fs, "/test_create_dir/foo");
1785
+
1786
+ assert!(
1787
+ foo_dir.contains(&"bar".to_string()),
1788
+ "the foo directory is updated and well-defined"
1789
+ );
1790
+
1791
+ let bar_dir = read_dir_names(&fs, "/test_create_dir/foo/bar");
1792
+
1793
+ assert!(
1794
+ bar_dir.is_empty(),
1795
+ "the foo directory is updated and well-defined"
1796
+ );
1797
+ let _ = fs_extra::remove_items(&["/test_create_dir"]);
1798
+ }
1799
+
1800
+ #[tokio::test]
1801
+ async fn test_remove_dir() {
1802
+ let fs = gen_filesystem();
1803
+
1804
+ assert_eq!(
1805
+ fs.remove_dir(Path::new("/")),
1806
+ Err(FsError::PermissionDenied),
1807
+ "cannot remove the root directory",
1808
+ );
1809
+
1810
+ assert_eq!(
1811
+ fs.remove_dir(Path::new("/foo")),
1812
+ Err(FsError::EntryNotFound),
1813
+ "cannot remove a directory that doesn't exist",
1814
+ );
1815
+
1816
+ assert_eq!(fs.create_dir(Path::new("/test_remove_dir")), Ok(()));
1817
+
1818
+ assert_eq!(
1819
+ fs.create_dir(Path::new("/test_remove_dir/foo")),
1820
+ Ok(()),
1821
+ "creating a directory",
1822
+ );
1823
+
1824
+ assert_eq!(
1825
+ fs.create_dir(Path::new("/test_remove_dir/foo/bar")),
1826
+ Ok(()),
1827
+ "creating a sub-directory",
1828
+ );
1829
+
1830
+ assert!(
1831
+ read_dir_names(&fs, "/test_remove_dir/foo").contains(&"bar".to_string()),
1832
+ "./foo/bar exists"
1833
+ );
1834
+
1835
+ assert_eq!(
1836
+ fs.remove_dir(Path::new("/test_remove_dir/foo")),
1837
+ Err(FsError::DirectoryNotEmpty),
1838
+ "removing a directory that has children",
1839
+ );
1840
+
1841
+ assert_eq!(
1842
+ fs.remove_dir(Path::new("/test_remove_dir/foo/bar")),
1843
+ Ok(()),
1844
+ "removing a sub-directory",
1845
+ );
1846
+
1847
+ assert_eq!(
1848
+ fs.remove_dir(Path::new("/test_remove_dir/foo")),
1849
+ Ok(()),
1850
+ "removing a directory",
1851
+ );
1852
+
1853
+ assert!(
1854
+ !read_dir_names(&fs, "/test_remove_dir").contains(&"foo".to_string()),
1855
+ "the foo directory still exists"
1856
+ );
1857
+ }
1858
+
1859
+ fn read_dir_names(fs: &dyn crate::FileSystem, path: &str) -> Vec<String> {
1860
+ fs.read_dir(Path::new(path))
1861
+ .unwrap()
1862
+ .filter_map(|entry| Some(entry.ok()?.file_name().to_str()?.to_string()))
1863
+ .collect::<Vec<_>>()
1864
+ }
1865
+
1866
+ fn create_dir_all(fs: &mem_fs::FileSystem, path: &Path) {
1867
+ let mut current = PathBuf::from("/");
1868
+
1869
+ for component in path.iter().skip(1) {
1870
+ current.push(component);
1871
+
1872
+ if fs.metadata(&current).is_err() {
1873
+ fs.create_dir(&current).unwrap();
1874
+ }
1875
+ }
1876
+ }
1877
+
1878
+ #[tokio::test]
1879
+ async fn test_rename() {
1880
+ let fs = gen_filesystem();
1881
+
1882
+ assert_eq!(
1883
+ fs.rename(Path::new("/"), Path::new("/bar")).await,
1884
+ Err(FsError::PermissionDenied),
1885
+ "renaming a directory that has no parent",
1886
+ );
1887
+ assert_eq!(
1888
+ fs.rename(Path::new("/foo"), Path::new("/")).await,
1889
+ Err(FsError::PermissionDenied),
1890
+ "renaming to the synthetic root directory is rejected",
1891
+ );
1892
+
1893
+ assert_eq!(fs.create_dir(Path::new("/test_rename")), Ok(()));
1894
+ assert_eq!(fs.create_dir(Path::new("/test_rename/foo")), Ok(()));
1895
+ assert_eq!(fs.create_dir(Path::new("/test_rename/foo/qux")), Ok(()));
1896
+
1897
+ assert_eq!(
1898
+ fs.rename(
1899
+ Path::new("/test_rename/foo"),
1900
+ Path::new("/test_rename/bar/baz")
1901
+ )
1902
+ .await,
1903
+ Err(FsError::EntryNotFound),
1904
+ "renaming to a directory that has parent that doesn't exist",
1905
+ );
1906
+
1907
+ assert_eq!(fs.create_dir(Path::new("/test_rename/bar")), Ok(()));
1908
+
1909
+ assert_eq!(
1910
+ fs.rename(Path::new("/test_rename/foo"), Path::new("/test_rename/bar"))
1911
+ .await,
1912
+ Ok(()),
1913
+ "renaming to a directory that has parent that exists",
1914
+ );
1915
+
1916
+ assert!(
1917
+ fs.new_open_options()
1918
+ .write(true)
1919
+ .create_new(true)
1920
+ .open(Path::new("/test_rename/bar/hello1.txt"))
1921
+ .is_ok(),
1922
+ "creating a new file (`hello1.txt`)",
1923
+ );
1924
+ assert!(
1925
+ fs.new_open_options()
1926
+ .write(true)
1927
+ .create_new(true)
1928
+ .open(Path::new("/test_rename/bar/hello2.txt"))
1929
+ .is_ok(),
1930
+ "creating a new file (`hello2.txt`)",
1931
+ );
1932
+
1933
+ let cur_dir = read_dir_names(&fs, "/test_rename");
1934
+
1935
+ assert!(
1936
+ !cur_dir.contains(&"foo".to_string()),
1937
+ "the foo directory still exists"
1938
+ );
1939
+
1940
+ assert!(
1941
+ cur_dir.contains(&"bar".to_string()),
1942
+ "the bar directory still exists"
1943
+ );
1944
+
1945
+ let bar_dir = read_dir_names(&fs, "/test_rename/bar");
1946
+
1947
+ if !bar_dir.contains(&"qux".to_string()) {
1948
+ println!("qux does not exist: {bar_dir:?}")
1949
+ }
1950
+
1951
+ let qux_dir = read_dir_names(&fs, "/test_rename/bar/qux");
1952
+
1953
+ assert!(qux_dir.is_empty(), "the qux directory is empty");
1954
+
1955
+ assert!(
1956
+ read_dir_names(&fs, "/test_rename/bar").contains(&"hello1.txt".to_string()),
1957
+ "the /bar/hello1.txt file exists"
1958
+ );
1959
+
1960
+ assert!(
1961
+ read_dir_names(&fs, "/test_rename/bar").contains(&"hello2.txt".to_string()),
1962
+ "the /bar/hello2.txt file exists"
1963
+ );
1964
+
1965
+ assert_eq!(
1966
+ fs.create_dir(Path::new("/test_rename/foo")),
1967
+ Ok(()),
1968
+ "create ./foo again",
1969
+ );
1970
+
1971
+ assert_eq!(
1972
+ fs.rename(
1973
+ Path::new("/test_rename/bar/hello2.txt"),
1974
+ Path::new("/test_rename/foo/world2.txt")
1975
+ )
1976
+ .await,
1977
+ Ok(()),
1978
+ "renaming (and moving) a file",
1979
+ );
1980
+
1981
+ assert_eq!(
1982
+ fs.rename(
1983
+ Path::new("/test_rename/foo"),
1984
+ Path::new("/test_rename/bar/baz")
1985
+ )
1986
+ .await,
1987
+ Ok(()),
1988
+ "renaming a directory",
1989
+ );
1990
+
1991
+ assert_eq!(
1992
+ fs.rename(
1993
+ Path::new("/test_rename/bar/hello1.txt"),
1994
+ Path::new("/test_rename/bar/world1.txt")
1995
+ )
1996
+ .await,
1997
+ Ok(()),
1998
+ "renaming a file (in the same directory)",
1999
+ );
2000
+
2001
+ assert!(
2002
+ read_dir_names(&fs, "/test_rename").contains(&"bar".to_string()),
2003
+ "./bar exists"
2004
+ );
2005
+
2006
+ assert!(
2007
+ read_dir_names(&fs, "/test_rename/bar").contains(&"baz".to_string()),
2008
+ "/bar/baz exists"
2009
+ );
2010
+ assert!(
2011
+ !read_dir_names(&fs, "/test_rename").contains(&"foo".to_string()),
2012
+ "foo does not exist anymore"
2013
+ );
2014
+ assert!(
2015
+ read_dir_names(&fs, "/test_rename/bar/baz").contains(&"world2.txt".to_string()),
2016
+ "/bar/baz/world2.txt exists"
2017
+ );
2018
+ assert!(
2019
+ read_dir_names(&fs, "/test_rename/bar").contains(&"world1.txt".to_string()),
2020
+ "/bar/world1.txt (ex hello1.txt) exists"
2021
+ );
2022
+ assert!(
2023
+ !read_dir_names(&fs, "/test_rename/bar").contains(&"hello1.txt".to_string()),
2024
+ "hello1.txt was moved"
2025
+ );
2026
+ assert!(
2027
+ !read_dir_names(&fs, "/test_rename/bar").contains(&"hello2.txt".to_string()),
2028
+ "hello2.txt was moved"
2029
+ );
2030
+ assert!(
2031
+ read_dir_names(&fs, "/test_rename/bar/baz").contains(&"world2.txt".to_string()),
2032
+ "world2.txt was moved to the correct place"
2033
+ );
2034
+
2035
+ let _ = fs_extra::remove_items(&["/test_rename"]);
2036
+ }
2037
+
2038
+ #[tokio::test]
2039
+ async fn cross_mount_file_rename_copies_and_removes_source() {
2040
+ let fs = MountFileSystem::new();
2041
+ let left = TmpFileSystem::new();
2042
+ let right = TmpFileSystem::new();
2043
+
2044
+ left.new_open_options()
2045
+ .create(true)
2046
+ .write(true)
2047
+ .open(Path::new("/from.txt"))
2048
+ .unwrap();
2049
+
2050
+ fs.mount(Path::new("/left"), Arc::new(left.clone()))
2051
+ .unwrap();
2052
+ fs.mount(Path::new("/right"), Arc::new(right.clone()))
2053
+ .unwrap();
2054
+
2055
+ fs.rename(Path::new("/left/from.txt"), Path::new("/right/to.txt"))
2056
+ .await
2057
+ .unwrap();
2058
+
2059
+ assert_eq!(
2060
+ left.metadata(Path::new("/from.txt")),
2061
+ Err(FsError::EntryNotFound)
2062
+ );
2063
+ assert!(right.metadata(Path::new("/to.txt")).unwrap().is_file());
2064
+ }
2065
+
2066
+ #[tokio::test]
2067
+ async fn test_metadata() {
2068
+ use std::thread::sleep;
2069
+ use std::time::Duration;
2070
+
2071
+ let fs = gen_filesystem();
2072
+
2073
+ let root_metadata = fs.metadata(Path::new("/test_metadata")).unwrap();
2074
+
2075
+ assert!(root_metadata.ft.dir);
2076
+ assert_eq!(root_metadata.accessed, root_metadata.created);
2077
+ assert_eq!(root_metadata.modified, root_metadata.created);
2078
+ assert!(root_metadata.modified > 0);
2079
+
2080
+ assert_eq!(fs.create_dir(Path::new("/test_metadata/foo")), Ok(()));
2081
+
2082
+ let foo_metadata = fs.metadata(Path::new("/test_metadata/foo"));
2083
+ assert!(foo_metadata.is_ok());
2084
+ let foo_metadata = foo_metadata.unwrap();
2085
+
2086
+ assert!(foo_metadata.ft.dir);
2087
+ assert!(foo_metadata.accessed == foo_metadata.created);
2088
+ assert!(foo_metadata.modified == foo_metadata.created);
2089
+ assert!(foo_metadata.modified > 0);
2090
+
2091
+ sleep(Duration::from_secs(3));
2092
+
2093
+ assert_eq!(
2094
+ fs.rename(
2095
+ Path::new("/test_metadata/foo"),
2096
+ Path::new("/test_metadata/bar")
2097
+ )
2098
+ .await,
2099
+ Ok(())
2100
+ );
2101
+
2102
+ let bar_metadata = fs.metadata(Path::new("/test_metadata/bar")).unwrap();
2103
+ assert!(bar_metadata.ft.dir);
2104
+ assert!(bar_metadata.accessed == foo_metadata.accessed);
2105
+ assert!(bar_metadata.created == foo_metadata.created);
2106
+ assert!(bar_metadata.modified > foo_metadata.modified);
2107
+
2108
+ let root_metadata = fs.metadata(Path::new("/test_metadata/bar")).unwrap();
2109
+ assert!(
2110
+ root_metadata.modified > foo_metadata.modified,
2111
+ "the parent modified time was updated"
2112
+ );
2113
+
2114
+ let _ = fs_extra::remove_items(&["/test_metadata"]);
2115
+ }
2116
+
2117
+ #[tokio::test]
2118
+ async fn test_remove_file() {
2119
+ let fs = gen_filesystem();
2120
+
2121
+ assert!(
2122
+ fs.new_open_options()
2123
+ .write(true)
2124
+ .create_new(true)
2125
+ .open(Path::new("/test_remove_file/foo.txt"))
2126
+ .is_ok(),
2127
+ "creating a new file",
2128
+ );
2129
+
2130
+ assert!(read_dir_names(&fs, "/test_remove_file").contains(&"foo.txt".to_string()));
2131
+
2132
+ assert_eq!(
2133
+ fs.remove_file(Path::new("/test_remove_file/foo.txt")),
2134
+ Ok(()),
2135
+ "removing a file that exists",
2136
+ );
2137
+
2138
+ assert!(!read_dir_names(&fs, "/test_remove_file").contains(&"foo.txt".to_string()));
2139
+
2140
+ assert_eq!(
2141
+ fs.remove_file(Path::new("/test_remove_file/foo.txt")),
2142
+ Err(FsError::EntryNotFound),
2143
+ "removing a file that doesn't exists",
2144
+ );
2145
+
2146
+ let _ = fs_extra::remove_items(&["./test_remove_file"]);
2147
+ }
2148
+
2149
+ #[tokio::test]
2150
+ async fn test_readdir() {
2151
+ let fs = gen_filesystem();
2152
+
2153
+ assert_eq!(
2154
+ fs.create_dir(Path::new("/test_readdir/foo")),
2155
+ Ok(()),
2156
+ "creating `foo`"
2157
+ );
2158
+ assert_eq!(
2159
+ fs.create_dir(Path::new("/test_readdir/foo/sub")),
2160
+ Ok(()),
2161
+ "creating `sub`"
2162
+ );
2163
+ assert_eq!(
2164
+ fs.create_dir(Path::new("/test_readdir/bar")),
2165
+ Ok(()),
2166
+ "creating `bar`"
2167
+ );
2168
+ assert_eq!(
2169
+ fs.create_dir(Path::new("/test_readdir/baz")),
2170
+ Ok(()),
2171
+ "creating `bar`"
2172
+ );
2173
+ assert!(
2174
+ fs.new_open_options()
2175
+ .write(true)
2176
+ .create_new(true)
2177
+ .open(Path::new("/test_readdir/a.txt"))
2178
+ .is_ok(),
2179
+ "creating `a.txt`",
2180
+ );
2181
+ assert!(
2182
+ fs.new_open_options()
2183
+ .write(true)
2184
+ .create_new(true)
2185
+ .open(Path::new("/test_readdir/b.txt"))
2186
+ .is_ok(),
2187
+ "creating `b.txt`",
2188
+ );
2189
+
2190
+ println!("fs: {fs:?}");
2191
+
2192
+ let readdir = fs.read_dir(Path::new("/test_readdir"));
2193
+
2194
+ assert!(readdir.is_ok(), "reading the directory `/test_readdir/`");
2195
+
2196
+ let mut readdir = readdir.unwrap();
2197
+
2198
+ let next = readdir.next().unwrap().unwrap();
2199
+ assert!(next.path.ends_with("foo"), "checking entry #1");
2200
+ println!("entry 1: {next:#?}");
2201
+ assert!(next.file_type().unwrap().is_dir(), "checking entry #1");
2202
+
2203
+ let next = readdir.next().unwrap().unwrap();
2204
+ assert!(next.path.ends_with("bar"), "checking entry #2");
2205
+ assert!(next.file_type().unwrap().is_dir(), "checking entry #2");
2206
+
2207
+ let next = readdir.next().unwrap().unwrap();
2208
+ assert!(next.path.ends_with("baz"), "checking entry #3");
2209
+ assert!(next.file_type().unwrap().is_dir(), "checking entry #3");
2210
+
2211
+ let next = readdir.next().unwrap().unwrap();
2212
+ assert!(next.path.ends_with("a.txt"), "checking entry #2");
2213
+ assert!(next.file_type().unwrap().is_file(), "checking entry #4");
2214
+
2215
+ let next = readdir.next().unwrap().unwrap();
2216
+ assert!(next.path.ends_with("b.txt"), "checking entry #2");
2217
+ assert!(next.file_type().unwrap().is_file(), "checking entry #5");
2218
+
2219
+ if let Some(s) = readdir.next() {
2220
+ panic!("next: {s:?}");
2221
+ }
2222
+
2223
+ let _ = fs_extra::remove_items(&["./test_readdir"]);
2224
+ }
2225
+ }