Skip to main content

acyclic_fs/native_mount/
routed.rs

1//! One native mount fronting many independently checked-out sources.
2//!
3//! [`RoutedMountSource`] dispatches every callback by the first path
4//! component ("the route name") to a live-mutable set of child
5//! [`MountFilesystem`] sources. It exists so N logical checkouts (for
6//! example, N repository forks) can share exactly one native mount session:
7//! adding or removing a checkout becomes a map mutation instead of a
8//! `mount_native`/unmount cycle.
9
10use 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
28/// Derives one process-unique 16-byte tag for a freshly added route.
29///
30/// Uniqueness comes from the monotonic sequence, not from hashing `name`;
31/// the name only perturbs the tag for readability under a debugger.
32fn 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
42/// XORs one [`FileId`] with a route tag. Self-inverse: applying it twice
43/// with the same tag returns the original identity.
44fn 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
102/// One route resolved from an incoming [`MountPath`]: the child source, its
103/// remap tag, its route name, and the remainder of the path within it.
104struct 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
196/// A [`MountFilesystem`] that routes by first path component to a
197/// live-mutable set of child sources, each exposed as a subdirectory of one
198/// synthetic root.
199///
200/// Every [`FileId`] a route hands back is `XOR`ed with that route's tag before
201/// leaving this type, since routes fronting checkouts of the same volume
202/// otherwise return colliding stable ids for unchanged files. The mapping is
203/// deliberately one-way: `clone_range_by_id` and `remove`'s expected-id
204/// precondition resolve a remapped id back to its owning route through an
205/// index recorded every time an id is emitted, then undo the same XOR.
206pub 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    /// Advances on every route change and is served as the synthetic root's
212    /// mtime/ctime: kernels re-validate cached children when the parent
213    /// changes, which is what makes a removed route disappear promptly.
214    revision: AtomicI64,
215}
216
217impl RoutedMountSource {
218    /// Creates one router with no routes.
219    #[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    /// Adds one route under `name`, exposed as `/name` from the mount root.
231    ///
232    /// # Errors
233    ///
234    /// Returns [`MountSourceError::Invalid`] for an empty or `.`/`..` name,
235    /// and [`MountSourceError::AlreadyExists`] for a name already routed.
236    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    /// Removes the route named `name`, if any. Open handles into it keep
258    /// working detached; only new lookups stop finding it.
259    ///
260    /// Returns whether a route was actually removed.
261    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    /// Returns whether no route is currently registered.
279    #[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    /// Number of routes currently projected by this source.
288    #[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    /// Splits `path` into its route and remainder. `Ok(None)` means `path`
306    /// is the synthetic root itself.
307    ///
308    /// Unlike [`MountFilesystem::lookup`]'s usual "authentically absent"
309    /// convention, an unrecognized route name is reported as
310    /// [`MountSourceError::NotFound`] here rather than as an absent-but-valid
311    /// result: it means there is no such mounted filesystem to query at all,
312    /// not that a query against a known filesystem came back empty.
313    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            // The child source's path and host root are both route-relative.
996            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    /// Two independent checkouts of one volume that already shares a file
1103    /// at the same generation, reproducing the real `FileId` collision case
1104    /// that motivates remapping: forks are checkouts of one volume.
1105    #[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}