1use super::{
11 MountAttributePage, MountAttributeWriteMode, MountDirectoryEntry, MountDirectoryPage,
12 MountFilesystem, MountLookup, MountNode, MountNodeKind, MountOpenFile, MountPath,
13 MountRangeAllocation, MountSeekTarget, MountSourceError, MountViewLease,
14};
15use crate::FileId;
16use crate::kernel::FileMetadata;
17use bytes::Bytes;
18use std::collections::hash_map::DefaultHasher;
19use std::collections::{BTreeMap, HashMap};
20use std::ffi::OsString;
21use std::hash::{Hash, Hasher};
22use std::path::Path;
23use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
24use std::sync::{Arc, Condvar, Mutex, PoisonError, RwLock};
25
26static ROUTE_SEQUENCE: AtomicU64 = AtomicU64::new(1);
27
28fn next_route_tag(name: &[u8]) -> [u8; 16] {
33 let sequence = ROUTE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
34 let mut hasher = DefaultHasher::new();
35 name.hash(&mut hasher);
36 let mut tag = [0u8; 16];
37 tag[..8].copy_from_slice(&sequence.to_le_bytes());
38 tag[8..].copy_from_slice(&hasher.finish().to_le_bytes());
39 tag
40}
41
42fn remap_file_id(id: FileId, tag: [u8; 16]) -> FileId {
45 let mut bytes = id.into_bytes();
46 for (byte, tag_byte) in bytes.iter_mut().zip(tag) {
47 *byte ^= tag_byte;
48 }
49 FileId::from_bytes(bytes)
50}
51
52fn cross_route_error() -> MountSourceError {
53 MountSourceError::Unsupported("operation spans two different routes".to_owned())
54}
55
56#[cfg(windows)]
57fn host_route_name(name: &[u8]) -> Result<OsString, MountSourceError> {
58 use std::os::windows::ffi::OsStringExt;
59
60 let mut units = name.chunks_exact(2);
61 let wide = units
62 .by_ref()
63 .map(|pair| {
64 let Some((&lo, rest)) = pair.split_first() else {
65 return 0;
66 };
67 let Some(&hi) = rest.first() else {
68 return 0;
69 };
70 u16::from_le_bytes([lo, hi])
71 })
72 .collect::<Vec<_>>();
73 if !units.remainder().is_empty()
74 || wide.is_empty()
75 || wide.iter().any(|unit| matches!(*unit, 0 | 0x2f | 0x5c))
76 || wide == [u16::from(b'.')]
77 || wide == [u16::from(b'.'), u16::from(b'.')]
78 {
79 return Err(MountSourceError::Invalid("invalid route name".to_owned()));
80 }
81 Ok(OsString::from_wide(&wide))
82}
83
84#[cfg(unix)]
85fn host_route_name(name: &[u8]) -> Result<OsString, MountSourceError> {
86 use std::os::unix::ffi::OsStringExt;
87
88 if name.is_empty() || name == b"." || name == b".." || name.contains(&0) || name.contains(&b'/')
89 {
90 return Err(MountSourceError::Invalid("invalid route name".to_owned()));
91 }
92 Ok(OsString::from_vec(name.to_vec()))
93}
94
95struct Route {
96 source: Arc<dyn MountFilesystem>,
97 tag: [u8; 16],
98}
99
100type CaptureRouteBatch = BTreeMap<Vec<u8>, (Arc<dyn MountFilesystem>, Vec<MountPath>)>;
101
102struct Routed {
105 source: Arc<dyn MountFilesystem>,
106 tag: [u8; 16],
107 name: Vec<u8>,
108 sub_path: MountPath,
109}
110
111#[derive(Default)]
112struct RouteViewState {
113 readers: usize,
114 writer: bool,
115 waiting_writers: usize,
116}
117
118#[derive(Default)]
119struct RouteViewGate {
120 state: Mutex<RouteViewState>,
121 changed: Condvar,
122}
123
124struct RouteViewReadLease {
125 gate: Arc<RouteViewGate>,
126 _children: Vec<Box<dyn MountViewLease>>,
127}
128
129impl MountViewLease for RouteViewReadLease {}
130
131impl Drop for RouteViewReadLease {
132 fn drop(&mut self) {
133 let mut state = self
134 .gate
135 .state
136 .lock()
137 .unwrap_or_else(PoisonError::into_inner);
138 state.readers = state.readers.saturating_sub(1);
139 if state.readers == 0 {
140 self.gate.changed.notify_all();
141 }
142 }
143}
144
145struct RouteViewWriteLease {
146 gate: Arc<RouteViewGate>,
147}
148
149impl Drop for RouteViewWriteLease {
150 fn drop(&mut self) {
151 let mut state = self
152 .gate
153 .state
154 .lock()
155 .unwrap_or_else(PoisonError::into_inner);
156 state.writer = false;
157 self.gate.changed.notify_all();
158 }
159}
160
161impl RouteViewGate {
162 fn read(self: &Arc<Self>) -> RouteViewReadLease {
163 let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
164 while state.writer || state.waiting_writers != 0 {
165 state = self
166 .changed
167 .wait(state)
168 .unwrap_or_else(PoisonError::into_inner);
169 }
170 state.readers = state.readers.saturating_add(1);
171 drop(state);
172 RouteViewReadLease {
173 gate: Arc::clone(self),
174 _children: Vec::new(),
175 }
176 }
177
178 fn write(self: &Arc<Self>) -> RouteViewWriteLease {
179 let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
180 state.waiting_writers = state.waiting_writers.saturating_add(1);
181 while state.writer || state.readers != 0 {
182 state = self
183 .changed
184 .wait(state)
185 .unwrap_or_else(PoisonError::into_inner);
186 }
187 state.waiting_writers = state.waiting_writers.saturating_sub(1);
188 state.writer = true;
189 drop(state);
190 RouteViewWriteLease {
191 gate: Arc::clone(self),
192 }
193 }
194}
195
196pub struct RoutedMountSource {
207 view_gate: Arc<RouteViewGate>,
208 routes: RwLock<BTreeMap<Vec<u8>, Route>>,
209 file_id_index: RwLock<HashMap<FileId, Vec<u8>>>,
210 root_id: FileId,
211 revision: AtomicI64,
215}
216
217impl RoutedMountSource {
218 #[must_use]
220 pub fn new() -> Self {
221 Self {
222 view_gate: Arc::new(RouteViewGate::default()),
223 revision: AtomicI64::new(1),
224 routes: RwLock::new(BTreeMap::new()),
225 file_id_index: RwLock::new(HashMap::new()),
226 root_id: FileId::new(),
227 }
228 }
229
230 pub fn add_route(
237 &self,
238 name: Vec<u8>,
239 source: Arc<dyn MountFilesystem>,
240 ) -> Result<(), MountSourceError> {
241 if name.is_empty() || name == b"." || name == b".." {
242 return Err(MountSourceError::Invalid(
243 "route name must be a non-empty, non-relative path component".to_owned(),
244 ));
245 }
246 let tag = next_route_tag(&name);
247 let _view = self.view_gate.write();
248 let mut routes = self.routes.write().unwrap_or_else(PoisonError::into_inner);
249 if routes.contains_key(&name) {
250 return Err(MountSourceError::AlreadyExists);
251 }
252 routes.insert(name, Route { source, tag });
253 self.bump_revision();
254 Ok(())
255 }
256
257 pub fn remove_route(&self, name: &[u8]) -> bool {
262 let _view = self.view_gate.write();
263 let removed = {
264 let mut routes = self.routes.write().unwrap_or_else(PoisonError::into_inner);
265 routes.remove(name)
266 };
267 if removed.is_some() {
268 self.bump_revision();
269 let mut index = self
270 .file_id_index
271 .write()
272 .unwrap_or_else(PoisonError::into_inner);
273 index.retain(|_, owner| owner.as_slice() != name);
274 }
275 removed.is_some()
276 }
277
278 #[must_use]
280 pub fn is_empty(&self) -> bool {
281 self.routes
282 .read()
283 .unwrap_or_else(PoisonError::into_inner)
284 .is_empty()
285 }
286
287 #[must_use]
289 pub fn route_count(&self) -> usize {
290 self.routes
291 .read()
292 .unwrap_or_else(PoisonError::into_inner)
293 .len()
294 }
295
296 fn locate(
297 &self,
298 name: &[u8],
299 ) -> Result<(Arc<dyn MountFilesystem>, [u8; 16]), MountSourceError> {
300 let routes = self.routes.read().unwrap_or_else(PoisonError::into_inner);
301 let route = routes.get(name).ok_or(MountSourceError::NotFound)?;
302 Ok((Arc::clone(&route.source), route.tag))
303 }
304
305 fn route(&self, path: &MountPath) -> Result<Option<Routed>, MountSourceError> {
314 let Some((name, rest)) = path.components().split_first() else {
315 return Ok(None);
316 };
317 let (source, tag) = self.locate(name)?;
318 let mut sub_path = MountPath::root();
319 for component in rest {
320 sub_path = sub_path.child(component.clone());
321 }
322 Ok(Some(Routed {
323 source,
324 tag,
325 name: name.clone(),
326 sub_path,
327 }))
328 }
329
330 fn record_file_id(&self, id: FileId, name: Vec<u8>) {
331 let mut index = self
332 .file_id_index
333 .write()
334 .unwrap_or_else(PoisonError::into_inner);
335 index.insert(id, name);
336 }
337
338 fn remap_lookup(&self, mut lookup: MountLookup, tag: [u8; 16], name: &[u8]) -> MountLookup {
339 lookup.node.file_id = remap_file_id(lookup.node.file_id, tag);
340 self.record_file_id(lookup.node.file_id, name.to_vec());
341 lookup
342 }
343
344 fn synthetic_root_lookup(&self) -> MountLookup {
345 let revision = self.revision.load(Ordering::Acquire);
346 let metadata = FileMetadata {
347 modified_ns: crate::kernel::MetadataField::Value(revision),
348 changed_ns: crate::kernel::MetadataField::Value(revision),
349 ..FileMetadata::default()
350 };
351 MountLookup {
352 node: MountNode {
353 file_id: self.root_id,
354 kind: MountNodeKind::Directory,
355 logical_bytes: 0,
356 link_count: 1,
357 device: None,
358 },
359 metadata,
360 }
361 }
362
363 fn bump_revision(&self) {
364 self.revision.fetch_add(1, Ordering::AcqRel);
365 }
366
367 fn coherent_epoch_by(
368 &self,
369 mut select: impl FnMut(&dyn MountFilesystem) -> Option<u64>,
370 ) -> u64 {
371 let routes = self.routes.read().unwrap_or_else(PoisonError::into_inner);
372 let mut hasher = DefaultHasher::new();
373 self.revision.load(Ordering::Acquire).hash(&mut hasher);
374 for (name, route) in routes.iter() {
375 name.hash(&mut hasher);
376 select(route.source.as_ref()).hash(&mut hasher);
377 }
378 hasher.finish()
379 }
380
381 fn acquire_epoch_lease(
382 &self,
383 expected_epoch: Option<u64>,
384 binding: bool,
385 ) -> Result<Box<dyn MountViewLease>, MountSourceError> {
386 let mut lease = self.view_gate.read();
387 let epoch = if binding {
388 self.binding_epoch()
389 } else {
390 self.view_epoch()
391 };
392 if expected_epoch.is_some_and(|expected| Some(expected) != epoch) {
393 return Err(MountSourceError::Stale);
394 }
395 let sources = self
396 .routes
397 .read()
398 .unwrap_or_else(PoisonError::into_inner)
399 .values()
400 .map(|route| Arc::clone(&route.source))
401 .collect::<Vec<_>>();
402 let mut children = Vec::with_capacity(sources.len());
403 for source in sources {
404 children.push(if binding {
405 source.acquire_binding_lease(source.binding_epoch())?
406 } else {
407 source.acquire_view_lease(source.view_epoch())?
408 });
409 }
410 let current = if binding {
411 self.binding_epoch()
412 } else {
413 self.view_epoch()
414 };
415 if !self.view_is_stable() || current != epoch {
416 return Err(MountSourceError::Stale);
417 }
418 lease._children = children;
419 Ok(Box::new(lease))
420 }
421
422 #[allow(clippy::type_complexity)]
423 fn read_synthetic_root(
424 &self,
425 cursor: Option<&[u8]>,
426 maximum_entries: u32,
427 ) -> Result<MountDirectoryPage, MountSourceError> {
428 let mut candidates: Vec<(Vec<u8>, Arc<dyn MountFilesystem>, [u8; 16])> = {
429 let routes = self.routes.read().unwrap_or_else(PoisonError::into_inner);
430 routes
431 .iter()
432 .filter(|(name, _)| cursor.is_none_or(|after| name.as_slice() > after))
433 .map(|(name, route)| (name.clone(), Arc::clone(&route.source), route.tag))
434 .collect()
435 };
436 let limit = usize::try_from(maximum_entries).unwrap_or(usize::MAX);
437 let has_more = candidates.len() > limit;
438 candidates.truncate(limit);
439 let mut entries = Vec::with_capacity(candidates.len());
440 for (name, source, tag) in candidates {
441 let Some(lookup) = source.lookup(&MountPath::root())? else {
442 continue;
443 };
444 let remapped = self.remap_lookup(lookup, tag, &name);
445 entries.push(MountDirectoryEntry {
446 name,
447 node: remapped.node,
448 metadata: remapped.metadata,
449 });
450 }
451 let next_cursor = has_more
452 .then(|| entries.last().map(|entry| entry.name.clone()))
453 .flatten();
454 Ok(MountDirectoryPage {
455 entries,
456 next_cursor,
457 })
458 }
459}
460
461impl Default for RoutedMountSource {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467struct RemappedOpenFile {
468 inner: Arc<dyn MountOpenFile>,
469 tag: [u8; 16],
470}
471
472impl MountOpenFile for RemappedOpenFile {
473 fn lookup(&self) -> Result<MountLookup, MountSourceError> {
474 let mut lookup = self.inner.lookup()?;
475 lookup.node.file_id = remap_file_id(lookup.node.file_id, self.tag);
476 Ok(lookup)
477 }
478
479 fn read_range(&self, offset: u64, length: u32) -> Result<Bytes, MountSourceError> {
480 self.inner.read_range(offset, length)
481 }
482
483 fn read_up_to(&self, offset: u64, maximum_bytes: u32) -> Result<Bytes, MountSourceError> {
484 self.inner.read_up_to(offset, maximum_bytes)
485 }
486
487 fn seek(&self, offset: u64, target: MountSeekTarget) -> Result<Option<u64>, MountSourceError> {
488 self.inner.seek(offset, target)
489 }
490
491 fn write_range(&self, offset: u64, bytes: Bytes) -> Result<(), MountSourceError> {
492 self.inner.write_range(offset, bytes)
493 }
494
495 fn resize(&self, logical_bytes: u64) -> Result<(), MountSourceError> {
496 self.inner.resize(logical_bytes)
497 }
498
499 fn allocate_range(
500 &self,
501 offset: u64,
502 length: u64,
503 operation: MountRangeAllocation,
504 ) -> Result<(), MountSourceError> {
505 self.inner.allocate_range(offset, length, operation)
506 }
507
508 fn set_attributes(
509 &self,
510 metadata: FileMetadata,
511 logical_bytes: Option<u64>,
512 ) -> Result<(), MountSourceError> {
513 self.inner.set_attributes(metadata, logical_bytes)
514 }
515
516 fn read_attribute(&self, name: &[u8]) -> Result<Option<Bytes>, MountSourceError> {
517 self.inner.read_attribute(name)
518 }
519
520 fn list_attributes(
521 &self,
522 cursor: Option<&[u8]>,
523 maximum_entries: u32,
524 ) -> Result<MountAttributePage, MountSourceError> {
525 self.inner.list_attributes(cursor, maximum_entries)
526 }
527
528 fn write_attribute(
529 &self,
530 name: &[u8],
531 value: Bytes,
532 mode: MountAttributeWriteMode,
533 ) -> Result<(), MountSourceError> {
534 self.inner.write_attribute(name, value, mode)
535 }
536
537 fn remove_attribute(&self, name: &[u8]) -> Result<(), MountSourceError> {
538 self.inner.remove_attribute(name)
539 }
540}
541
542impl MountFilesystem for RoutedMountSource {
543 fn supports_posix_named_attributes(&self) -> bool {
544 let routes = self.routes.read().unwrap_or_else(PoisonError::into_inner);
545 !routes.is_empty()
546 && routes
547 .values()
548 .all(|route| route.source.supports_posix_named_attributes())
549 }
550
551 fn view_is_stable(&self) -> bool {
552 self.routes
553 .read()
554 .unwrap_or_else(PoisonError::into_inner)
555 .values()
556 .all(|route| route.source.view_is_stable())
557 }
558
559 fn view_epoch(&self) -> Option<u64> {
560 Some(self.coherent_epoch_by(MountFilesystem::view_epoch))
561 }
562
563 fn binding_epoch(&self) -> Option<u64> {
564 Some(self.coherent_epoch_by(MountFilesystem::binding_epoch))
565 }
566
567 fn acquire_view_lease(
568 &self,
569 expected_epoch: Option<u64>,
570 ) -> Result<Box<dyn MountViewLease>, MountSourceError> {
571 self.acquire_epoch_lease(expected_epoch, false)
572 }
573
574 fn acquire_binding_lease(
575 &self,
576 expected_epoch: Option<u64>,
577 ) -> Result<Box<dyn MountViewLease>, MountSourceError> {
578 self.acquire_epoch_lease(expected_epoch, true)
579 }
580
581 fn lookup(&self, path: &MountPath) -> Result<Option<MountLookup>, MountSourceError> {
582 let Some(routed) = self.route(path)? else {
583 return Ok(Some(self.synthetic_root_lookup()));
584 };
585 let result = routed.source.lookup(&routed.sub_path)?;
586 Ok(result.map(|lookup| self.remap_lookup(lookup, routed.tag, &routed.name)))
587 }
588
589 fn open_file(&self, path: &MountPath) -> Result<Arc<dyn MountOpenFile>, MountSourceError> {
590 let Some(routed) = self.route(path)? else {
591 return Err(MountSourceError::Invalid(
592 "the synthetic mount root is not a regular file".to_owned(),
593 ));
594 };
595 let inner = routed.source.open_file(&routed.sub_path)?;
596 Ok(Arc::new(RemappedOpenFile {
597 inner,
598 tag: routed.tag,
599 }))
600 }
601
602 fn detach_file(&self, path: &MountPath) -> Result<Arc<dyn MountOpenFile>, MountSourceError> {
603 let Some(routed) = self.route(path)? else {
604 return Err(MountSourceError::Invalid(
605 "the synthetic mount root is not a regular file".to_owned(),
606 ));
607 };
608 let inner = routed.source.detach_file(&routed.sub_path)?;
609 Ok(Arc::new(RemappedOpenFile {
610 inner,
611 tag: routed.tag,
612 }))
613 }
614
615 fn read_link(&self, path: &MountPath) -> Result<Bytes, MountSourceError> {
616 let Some(routed) = self.route(path)? else {
617 return Err(MountSourceError::Invalid(
618 "the synthetic mount root is not a symbolic link".to_owned(),
619 ));
620 };
621 routed.source.read_link(&routed.sub_path)
622 }
623
624 fn read_range(
625 &self,
626 path: &MountPath,
627 offset: u64,
628 length: u32,
629 ) -> Result<Bytes, MountSourceError> {
630 let Some(routed) = self.route(path)? else {
631 return Err(MountSourceError::Invalid(
632 "the synthetic mount root has no content".to_owned(),
633 ));
634 };
635 routed.source.read_range(&routed.sub_path, offset, length)
636 }
637
638 fn seek(
639 &self,
640 path: &MountPath,
641 offset: u64,
642 target: MountSeekTarget,
643 ) -> Result<Option<u64>, MountSourceError> {
644 let Some(routed) = self.route(path)? else {
645 return Err(MountSourceError::Invalid(
646 "the synthetic mount root has no content".to_owned(),
647 ));
648 };
649 routed.source.seek(&routed.sub_path, offset, target)
650 }
651
652 fn read_directory(
653 &self,
654 path: &MountPath,
655 cursor: Option<&[u8]>,
656 maximum_entries: u32,
657 ) -> Result<MountDirectoryPage, MountSourceError> {
658 let Some(routed) = self.route(path)? else {
659 return self.read_synthetic_root(cursor, maximum_entries);
660 };
661 let page = routed
662 .source
663 .read_directory(&routed.sub_path, cursor, maximum_entries)?;
664 let entries = page
665 .entries
666 .into_iter()
667 .map(|mut entry| {
668 entry.node.file_id = remap_file_id(entry.node.file_id, routed.tag);
669 self.record_file_id(entry.node.file_id, routed.name.clone());
670 entry
671 })
672 .collect();
673 Ok(MountDirectoryPage {
674 entries,
675 next_cursor: page.next_cursor,
676 })
677 }
678
679 fn create_file(
680 &self,
681 path: &MountPath,
682 metadata: FileMetadata,
683 ) -> Result<MountLookup, MountSourceError> {
684 let Some(routed) = self.route(path)? else {
685 return Err(MountSourceError::Unsupported(
686 "routes are managed through add_route, not directory creation".to_owned(),
687 ));
688 };
689 let lookup = routed.source.create_file(&routed.sub_path, metadata)?;
690 Ok(self.remap_lookup(lookup, routed.tag, &routed.name))
691 }
692
693 fn create_directory(
694 &self,
695 path: &MountPath,
696 metadata: FileMetadata,
697 ) -> Result<MountLookup, MountSourceError> {
698 let Some(routed) = self.route(path)? else {
699 return Err(MountSourceError::Unsupported(
700 "routes are managed through add_route, not directory creation".to_owned(),
701 ));
702 };
703 let lookup = routed.source.create_directory(&routed.sub_path, metadata)?;
704 Ok(self.remap_lookup(lookup, routed.tag, &routed.name))
705 }
706
707 fn create_symbolic_link(
708 &self,
709 path: &MountPath,
710 target: Bytes,
711 metadata: FileMetadata,
712 ) -> Result<MountLookup, MountSourceError> {
713 let Some(routed) = self.route(path)? else {
714 return Err(MountSourceError::Unsupported(
715 "routes are managed through add_route, not directory creation".to_owned(),
716 ));
717 };
718 let lookup = routed
719 .source
720 .create_symbolic_link(&routed.sub_path, target, metadata)?;
721 Ok(self.remap_lookup(lookup, routed.tag, &routed.name))
722 }
723
724 fn create_special(
725 &self,
726 path: &MountPath,
727 kind: MountNodeKind,
728 device: Option<(u32, u32)>,
729 metadata: FileMetadata,
730 ) -> Result<MountLookup, MountSourceError> {
731 let Some(routed) = self.route(path)? else {
732 return Err(MountSourceError::Unsupported(
733 "routes are managed through add_route, not directory creation".to_owned(),
734 ));
735 };
736 let lookup = routed
737 .source
738 .create_special(&routed.sub_path, kind, device, metadata)?;
739 Ok(self.remap_lookup(lookup, routed.tag, &routed.name))
740 }
741
742 fn set_attributes(
743 &self,
744 path: &MountPath,
745 metadata: FileMetadata,
746 logical_bytes: Option<u64>,
747 ) -> Result<(), MountSourceError> {
748 let Some(routed) = self.route(path)? else {
749 return Err(MountSourceError::Unsupported(
750 "the synthetic mount root has no settable attributes".to_owned(),
751 ));
752 };
753 routed
754 .source
755 .set_attributes(&routed.sub_path, metadata, logical_bytes)
756 }
757
758 fn read_attribute(
759 &self,
760 path: &MountPath,
761 name: &[u8],
762 ) -> Result<Option<Bytes>, MountSourceError> {
763 let Some(routed) = self.route(path)? else {
764 return Ok(None);
765 };
766 routed.source.read_attribute(&routed.sub_path, name)
767 }
768
769 fn list_attributes(
770 &self,
771 path: &MountPath,
772 cursor: Option<&[u8]>,
773 maximum_entries: u32,
774 ) -> Result<MountAttributePage, MountSourceError> {
775 let Some(routed) = self.route(path)? else {
776 return Ok(MountAttributePage {
777 names: Vec::new(),
778 next_cursor: None,
779 });
780 };
781 routed
782 .source
783 .list_attributes(&routed.sub_path, cursor, maximum_entries)
784 }
785
786 fn write_attribute(
787 &self,
788 path: &MountPath,
789 name: &[u8],
790 value: Bytes,
791 mode: MountAttributeWriteMode,
792 ) -> Result<(), MountSourceError> {
793 let Some(routed) = self.route(path)? else {
794 return Err(MountSourceError::Unsupported(
795 "the synthetic mount root has no settable attributes".to_owned(),
796 ));
797 };
798 routed
799 .source
800 .write_attribute(&routed.sub_path, name, value, mode)
801 }
802
803 fn remove_attribute(&self, path: &MountPath, name: &[u8]) -> Result<(), MountSourceError> {
804 let Some(routed) = self.route(path)? else {
805 return Err(MountSourceError::Unsupported(
806 "the synthetic mount root has no settable attributes".to_owned(),
807 ));
808 };
809 routed.source.remove_attribute(&routed.sub_path, name)
810 }
811
812 fn write_range(
813 &self,
814 path: &MountPath,
815 offset: u64,
816 bytes: Bytes,
817 ) -> Result<(), MountSourceError> {
818 let Some(routed) = self.route(path)? else {
819 return Err(MountSourceError::Unsupported(
820 "the synthetic mount root has no content".to_owned(),
821 ));
822 };
823 routed.source.write_range(&routed.sub_path, offset, bytes)
824 }
825
826 fn resize(&self, path: &MountPath, logical_bytes: u64) -> Result<(), MountSourceError> {
827 let Some(routed) = self.route(path)? else {
828 return Err(MountSourceError::Unsupported(
829 "the synthetic mount root has no content".to_owned(),
830 ));
831 };
832 routed.source.resize(&routed.sub_path, logical_bytes)
833 }
834
835 fn allocate_range(
836 &self,
837 path: &MountPath,
838 offset: u64,
839 length: u64,
840 operation: MountRangeAllocation,
841 ) -> Result<(), MountSourceError> {
842 let Some(routed) = self.route(path)? else {
843 return Err(MountSourceError::Unsupported(
844 "the synthetic mount root has no content".to_owned(),
845 ));
846 };
847 routed
848 .source
849 .allocate_range(&routed.sub_path, offset, length, operation)
850 }
851
852 fn clone_range(
853 &self,
854 source: &MountPath,
855 source_offset: u64,
856 destination: &MountPath,
857 destination_offset: u64,
858 length: u64,
859 ) -> Result<(), MountSourceError> {
860 let source_routed = self.route(source)?.ok_or_else(cross_route_error)?;
861 let destination_routed = self.route(destination)?.ok_or_else(cross_route_error)?;
862 if source_routed.name != destination_routed.name {
863 return Err(cross_route_error());
864 }
865 source_routed.source.clone_range(
866 &source_routed.sub_path,
867 source_offset,
868 &destination_routed.sub_path,
869 destination_offset,
870 length,
871 )
872 }
873
874 #[allow(clippy::too_many_arguments)]
875 fn clone_range_by_id(
876 &self,
877 source_file_id: FileId,
878 source_offset: u64,
879 destination_file_id: FileId,
880 destination_offset: u64,
881 length: u64,
882 ) -> Result<(), MountSourceError> {
883 let (source_name, destination_name) = {
884 let index = self
885 .file_id_index
886 .read()
887 .unwrap_or_else(PoisonError::into_inner);
888 let source_name = index
889 .get(&source_file_id)
890 .cloned()
891 .ok_or(MountSourceError::NotFound)?;
892 let destination_name = index
893 .get(&destination_file_id)
894 .cloned()
895 .ok_or(MountSourceError::NotFound)?;
896 (source_name, destination_name)
897 };
898 if source_name != destination_name {
899 return Err(cross_route_error());
900 }
901 let (source, tag) = self.locate(&source_name)?;
902 source.clone_range_by_id(
903 remap_file_id(source_file_id, tag),
904 source_offset,
905 remap_file_id(destination_file_id, tag),
906 destination_offset,
907 length,
908 )
909 }
910
911 fn remove(&self, path: &MountPath, expected: Option<FileId>) -> Result<(), MountSourceError> {
912 let Some(routed) = self.route(path)? else {
913 return Err(MountSourceError::Unsupported(
914 "routes are removed through remove_route, not unlink".to_owned(),
915 ));
916 };
917 let expected = expected.map(|id| remap_file_id(id, routed.tag));
918 routed.source.remove(&routed.sub_path, expected)
919 }
920
921 fn rename(
922 &self,
923 source: &MountPath,
924 destination: &MountPath,
925 replace: bool,
926 ) -> Result<(), MountSourceError> {
927 let source_routed = self.route(source)?.ok_or_else(cross_route_error)?;
928 let destination_routed = self.route(destination)?.ok_or_else(cross_route_error)?;
929 if source_routed.name != destination_routed.name {
930 return Err(cross_route_error());
931 }
932 source_routed.source.rename(
933 &source_routed.sub_path,
934 &destination_routed.sub_path,
935 replace,
936 )
937 }
938
939 fn hard_link(
940 &self,
941 source: &MountPath,
942 destination: &MountPath,
943 ) -> Result<(), MountSourceError> {
944 let source_routed = self.route(source)?.ok_or_else(cross_route_error)?;
945 let destination_routed = self.route(destination)?.ok_or_else(cross_route_error)?;
946 if source_routed.name != destination_routed.name {
947 return Err(cross_route_error());
948 }
949 source_routed
950 .source
951 .hard_link(&source_routed.sub_path, &destination_routed.sub_path)
952 }
953
954 fn flush(&self) -> Result<(), MountSourceError> {
955 let sources: Vec<Arc<dyn MountFilesystem>> = self
956 .routes
957 .read()
958 .unwrap_or_else(PoisonError::into_inner)
959 .values()
960 .map(|route| Arc::clone(&route.source))
961 .collect();
962 for source in sources {
963 source.flush()?;
964 }
965 Ok(())
966 }
967
968 fn capture_host_path(
969 &self,
970 source_root: &Path,
971 path: &MountPath,
972 ) -> Result<(), MountSourceError> {
973 self.capture_host_paths(source_root, std::slice::from_ref(path))
974 }
975
976 fn capture_host_paths(
977 &self,
978 source_root: &Path,
979 paths: &[MountPath],
980 ) -> Result<(), MountSourceError> {
981 let mut routes = CaptureRouteBatch::new();
982 for path in paths {
983 let routed = self.route(path)?.ok_or_else(|| {
984 MountSourceError::Unsupported(
985 "the synthetic mount root cannot capture host state".to_owned(),
986 )
987 })?;
988 routes
989 .entry(routed.name)
990 .or_insert_with(|| (Arc::clone(&routed.source), Vec::new()))
991 .1
992 .push(routed.sub_path);
993 }
994 for (name, (source, paths)) in routes {
995 source.capture_host_paths(&source_root.join(host_route_name(&name)?), &paths)?;
997 }
998 Ok(())
999 }
1000
1001 fn capture_host_subtree(
1002 &self,
1003 source_root: &Path,
1004 path: &MountPath,
1005 ) -> Result<(), MountSourceError> {
1006 let Some(routed) = self.route(path)? else {
1007 return Err(MountSourceError::Unsupported(
1008 "the synthetic mount root cannot capture host state".to_owned(),
1009 ));
1010 };
1011 let route_root = source_root.join(host_route_name(&routed.name)?);
1012 routed
1013 .source
1014 .capture_host_subtree(&route_root, &routed.sub_path)
1015 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021 use crate::kernel::NamespacePath;
1022 use crate::model::{
1023 AccessMode, CheckoutMode, ConsistencyMode, FilesystemProfile, GenerationSelector,
1024 Lifecycle, MutationMode, VolumeConfig, VolumeLimits,
1025 };
1026 use crate::path::PortablePath;
1027 use crate::{CancellationToken, CheckoutMountSource, Fs, SharedCheckout, WorkBudget};
1028
1029 type MemoryCheckout = crate::facade::MemoryCheckout;
1030
1031 fn metadata() -> FileMetadata {
1032 FileMetadata::default()
1033 }
1034
1035 fn component(name: &str) -> Vec<u8> {
1036 #[cfg(target_os = "windows")]
1037 {
1038 name.encode_utf16().flat_map(u16::to_le_bytes).collect()
1039 }
1040 #[cfg(not(target_os = "windows"))]
1041 {
1042 name.as_bytes().to_vec()
1043 }
1044 }
1045
1046 fn test_path(name: &str) -> MountPath {
1047 MountPath::root().child(component(name))
1048 }
1049
1050 fn namespace_path(
1051 limits: VolumeLimits,
1052 name: &str,
1053 ) -> Result<NamespacePath, Box<dyn std::error::Error>> {
1054 Ok(NamespacePath::from_portable(
1055 &PortablePath::parse(&format!("/{name}"), limits)?,
1056 limits,
1057 )?)
1058 }
1059
1060 fn wrap(
1061 config: VolumeConfig,
1062 checkout: MemoryCheckout,
1063 ) -> Result<Arc<dyn MountFilesystem>, Box<dyn std::error::Error>> {
1064 let shared = Arc::new(SharedCheckout::new(checkout));
1065 Ok(Arc::new(CheckoutMountSource::new(shared, config)?))
1066 }
1067
1068 fn memory_source() -> Result<Arc<dyn MountFilesystem>, Box<dyn std::error::Error>> {
1069 memory_source_with_profile(FilesystemProfile::Portable)
1070 }
1071
1072 fn memory_source_with_profile(
1073 profile: FilesystemProfile,
1074 ) -> Result<Arc<dyn MountFilesystem>, Box<dyn std::error::Error>> {
1075 let mut config = VolumeConfig::portable(Lifecycle::Ephemeral);
1076 config.profile = profile;
1077 let fs = Fs::memory();
1078 let runtime = tokio::runtime::Builder::new_current_thread().build()?;
1079 let checkout = runtime.block_on(async {
1080 let cancellation = CancellationToken::new();
1081 let volume = fs
1082 .create_volume(config, WorkBudget::UNBOUNDED, &cancellation)
1083 .await?
1084 .value;
1085 volume
1086 .checkout(
1087 GenerationSelector::Head,
1088 CheckoutMode {
1089 access: AccessMode::ReadWrite,
1090 consistency: ConsistencyMode::Pinned,
1091 mutations: MutationMode::PrivateOverlay,
1092 },
1093 WorkBudget::UNBOUNDED,
1094 &cancellation,
1095 )
1096 .await
1097 .map(|receipt| receipt.value)
1098 })?;
1099 wrap(config, checkout)
1100 }
1101
1102 #[allow(clippy::type_complexity)]
1106 fn two_routes_over_same_volume()
1107 -> Result<(Arc<dyn MountFilesystem>, Arc<dyn MountFilesystem>), Box<dyn std::error::Error>>
1108 {
1109 let config = VolumeConfig::portable(Lifecycle::Ephemeral);
1110 let limits = config.limits;
1111 let fs = Fs::memory();
1112 let runtime = tokio::runtime::Builder::new_current_thread().build()?;
1113 let (checkout_a, checkout_b) = runtime.block_on(async {
1114 let cancellation = CancellationToken::new();
1115 let volume = fs
1116 .create_volume(config, WorkBudget::UNBOUNDED, &cancellation)
1117 .await?
1118 .value;
1119 let mode = CheckoutMode {
1120 access: AccessMode::ReadWrite,
1121 consistency: ConsistencyMode::Pinned,
1122 mutations: MutationMode::PrivateOverlay,
1123 };
1124 let mut seed = volume
1125 .checkout(
1126 GenerationSelector::Head,
1127 mode,
1128 WorkBudget::UNBOUNDED,
1129 &cancellation,
1130 )
1131 .await?
1132 .value;
1133 seed.create_file(
1134 namespace_path(limits, "shared.bin")?,
1135 Bytes::from_static(b"same"),
1136 WorkBudget::UNBOUNDED,
1137 &cancellation,
1138 )
1139 .await?;
1140 crate::seal_checkout(
1141 &mut seed,
1142 crate::OperationId::new(),
1143 WorkBudget::UNBOUNDED,
1144 &cancellation,
1145 )
1146 .await?;
1147 drop(seed);
1148 let a = volume
1149 .checkout(
1150 GenerationSelector::Head,
1151 mode,
1152 WorkBudget::UNBOUNDED,
1153 &cancellation,
1154 )
1155 .await?
1156 .value;
1157 let b = volume
1158 .checkout(
1159 GenerationSelector::Head,
1160 mode,
1161 WorkBudget::UNBOUNDED,
1162 &cancellation,
1163 )
1164 .await?
1165 .value;
1166 Ok::<_, Box<dyn std::error::Error>>((a, b))
1167 })?;
1168 Ok((wrap(config, checkout_a)?, wrap(config, checkout_b)?))
1169 }
1170
1171 #[test]
1172 fn unknown_route_is_not_found() -> Result<(), Box<dyn std::error::Error>> {
1173 let router = RoutedMountSource::new();
1174 router.add_route(component("a"), memory_source()?)?;
1175 assert!(matches!(
1176 router.lookup(&test_path("b")),
1177 Err(MountSourceError::NotFound)
1178 ));
1179 assert!(matches!(
1180 router.read_range(&test_path("b").child(component("x")), 0, 1),
1181 Err(MountSourceError::NotFound)
1182 ));
1183 Ok(())
1184 }
1185
1186 #[test]
1187 fn root_lists_route_names_as_directories() -> Result<(), Box<dyn std::error::Error>> {
1188 let router = RoutedMountSource::new();
1189 assert_eq!(router.route_count(), 0);
1190 router.add_route(component("a"), memory_source()?)?;
1191 router.add_route(component("b"), memory_source()?)?;
1192 assert_eq!(router.route_count(), 2);
1193 let page = router.read_directory(&MountPath::root(), None, 16)?;
1194 let mut names: Vec<&[u8]> = page
1195 .entries
1196 .iter()
1197 .map(|entry| entry.name.as_slice())
1198 .collect();
1199 names.sort_unstable();
1200 let mut expected = [component("a"), component("b")];
1201 expected.sort_unstable();
1202 assert_eq!(
1203 names,
1204 expected.iter().map(Vec::as_slice).collect::<Vec<_>>()
1205 );
1206 assert!(
1207 page.entries
1208 .iter()
1209 .all(|entry| entry.node.kind == MountNodeKind::Directory)
1210 );
1211 Ok(())
1212 }
1213
1214 #[test]
1215 fn view_lease_pins_routes_until_callback_completion() -> Result<(), Box<dyn std::error::Error>>
1216 {
1217 let router = Arc::new(RoutedMountSource::new());
1218 let route = component("a");
1219 router.add_route(route.clone(), memory_source()?)?;
1220 let epoch = router.view_epoch();
1221 let lease = router.acquire_view_lease(epoch)?;
1222 let (started_tx, started_rx) = std::sync::mpsc::channel();
1223 let (finished_tx, finished_rx) = std::sync::mpsc::channel();
1224 let writer = Arc::clone(&router);
1225 let writer_route = route.clone();
1226 let thread = std::thread::spawn(move || {
1227 assert!(started_tx.send(()).is_ok(), "signal writer");
1228 let removed = writer.remove_route(&writer_route);
1229 assert!(finished_tx.send(removed).is_ok(), "signal completion");
1230 });
1231 started_rx.recv()?;
1232 std::thread::sleep(std::time::Duration::from_millis(10));
1233 assert!(
1234 finished_rx.try_recv().is_err(),
1235 "route mutation escaped an active callback lease"
1236 );
1237 drop(lease);
1238 assert!(finished_rx.recv_timeout(std::time::Duration::from_secs(1))?);
1239 thread
1240 .join()
1241 .map_err(|_| std::io::Error::other("writer thread panicked"))?;
1242 assert_ne!(router.view_epoch(), epoch);
1243 Ok(())
1244 }
1245
1246 #[test]
1247 fn routed_native_writes_invalidate_cache_without_rebinding_other_routes()
1248 -> Result<(), Box<dyn std::error::Error>> {
1249 let router = RoutedMountSource::new();
1250 let first = component("first");
1251 let second = component("second");
1252 router.add_route(first.clone(), memory_source()?)?;
1253 router.add_route(second.clone(), memory_source()?)?;
1254 let binding = router.binding_epoch();
1255 let cache = router.view_epoch();
1256 router.create_file(&test_path("first").child(component("file")), metadata())?;
1257 assert_eq!(router.binding_epoch(), binding);
1258 assert_ne!(router.view_epoch(), cache);
1259 let _lease = router.acquire_binding_lease(binding)?;
1260 Ok(())
1261 }
1262
1263 #[test]
1264 fn dispatch_reaches_the_correct_child_source() -> Result<(), Box<dyn std::error::Error>> {
1265 let router = RoutedMountSource::new();
1266 router.add_route(component("a"), memory_source()?)?;
1267 let path = test_path("a").child(component("hello.txt"));
1268 router.create_file(&path, metadata())?;
1269 router.write_range(&path, 0, Bytes::from_static(b"hi"))?;
1270 assert_eq!(router.read_range(&path, 0, 2)?.as_ref(), b"hi");
1271 Ok(())
1272 }
1273
1274 #[test]
1275 fn host_capture_is_rooted_inside_its_route() -> Result<(), Box<dyn std::error::Error>> {
1276 let router = RoutedMountSource::new();
1277 #[cfg(windows)]
1278 let profile = FilesystemProfile::Windows;
1279 #[cfg(not(windows))]
1280 let profile = FilesystemProfile::Portable;
1281 router.add_route(component("a"), memory_source_with_profile(profile)?)?;
1282 router.add_route(component("b"), memory_source_with_profile(profile)?)?;
1283 let host = tempfile::tempdir()?;
1284 std::fs::create_dir(host.path().join("a"))?;
1285 std::fs::create_dir(host.path().join("b"))?;
1286 std::fs::write(host.path().join("b").join("captured.bin"), b"inside-b")?;
1287 let path = test_path("b").child(component("captured.bin"));
1288 router.capture_host_path(host.path(), &path)?;
1289 assert_eq!(router.read_range(&path, 0, 8)?.as_ref(), b"inside-b");
1290 assert!(
1291 router
1292 .lookup(&test_path("a").child(component("captured.bin")))?
1293 .is_none()
1294 );
1295 Ok(())
1296 }
1297
1298 #[test]
1299 fn file_id_remap_is_unique_across_routes_of_the_same_volume()
1300 -> Result<(), Box<dyn std::error::Error>> {
1301 let (source_a, source_b) = two_routes_over_same_volume()?;
1302 let router = RoutedMountSource::new();
1303 router.add_route(component("a"), source_a)?;
1304 router.add_route(component("b"), source_b)?;
1305 let a = router
1306 .lookup(&test_path("a").child(component("shared.bin")))?
1307 .ok_or("route a lost the shared file")?;
1308 let b = router
1309 .lookup(&test_path("b").child(component("shared.bin")))?
1310 .ok_or("route b lost the shared file")?;
1311 assert_ne!(a.node.file_id, b.node.file_id);
1312 Ok(())
1313 }
1314
1315 #[test]
1316 fn add_and_remove_route_while_serving() -> Result<(), Box<dyn std::error::Error>> {
1317 let router = RoutedMountSource::new();
1318 assert!(router.is_empty());
1319 router.add_route(component("a"), memory_source()?)?;
1320 assert!(!router.is_empty());
1321 assert!(router.lookup(&test_path("a"))?.is_some());
1322 assert!(router.remove_route(&component("a")));
1323 assert!(router.is_empty());
1324 assert!(matches!(
1325 router.lookup(&test_path("a")),
1326 Err(MountSourceError::NotFound)
1327 ));
1328 assert!(!router.remove_route(&component("a")));
1329 Ok(())
1330 }
1331
1332 #[test]
1333 fn cross_route_rename_and_hard_link_are_rejected() -> Result<(), Box<dyn std::error::Error>> {
1334 let router = RoutedMountSource::new();
1335 router.add_route(component("a"), memory_source()?)?;
1336 router.add_route(component("b"), memory_source()?)?;
1337 let source = test_path("a").child(component("x.txt"));
1338 router.create_file(&source, metadata())?;
1339 let destination = test_path("b").child(component("y.txt"));
1340 assert!(matches!(
1341 router.rename(&source, &destination, false),
1342 Err(MountSourceError::Unsupported(_))
1343 ));
1344 assert!(matches!(
1345 router.hard_link(&source, &destination),
1346 Err(MountSourceError::Unsupported(_))
1347 ));
1348 Ok(())
1349 }
1350
1351 #[test]
1352 fn clone_range_by_id_rejects_cross_route_and_unknown_ids()
1353 -> Result<(), Box<dyn std::error::Error>> {
1354 let router = RoutedMountSource::new();
1355 router.add_route(component("a"), memory_source()?)?;
1356 router.add_route(component("b"), memory_source()?)?;
1357 let file_a = test_path("a").child(component("x.bin"));
1358 let file_b = test_path("b").child(component("y.bin"));
1359 let a = router.create_file(&file_a, metadata())?;
1360 let b = router.create_file(&file_b, metadata())?;
1361 assert!(matches!(
1362 router.clone_range_by_id(a.node.file_id, 0, b.node.file_id, 0, 0),
1363 Err(MountSourceError::Unsupported(_))
1364 ));
1365 assert!(matches!(
1366 router.clone_range_by_id(FileId::new(), 0, a.node.file_id, 0, 0),
1367 Err(MountSourceError::NotFound)
1368 ));
1369 Ok(())
1370 }
1371
1372 #[cfg(not(target_os = "windows"))]
1373 #[test]
1374 fn sub_path_reconstruction_preserves_non_utf8_multi_component_names()
1375 -> Result<(), Box<dyn std::error::Error>> {
1376 let router = RoutedMountSource::new();
1377 router.add_route(
1378 component("a"),
1379 memory_source_with_profile(FilesystemProfile::Posix)?,
1380 )?;
1381 let non_utf8 = vec![b'x', 0xFFu8, b'y'];
1382 let directory = test_path("a").child(non_utf8.clone());
1383 router.create_directory(&directory, metadata())?;
1384 let file = directory.child(b"leaf.bin".to_vec());
1385 router.create_file(&file, metadata())?;
1386 let page = router.read_directory(&directory, None, 16)?;
1387 assert_eq!(page.entries.len(), 1);
1388 assert_eq!(page.entries[0].name, b"leaf.bin");
1389 assert!(router.lookup(&file)?.is_some());
1390 Ok(())
1391 }
1392}