Skip to main content

usn_journal_rs/path/
resolver.rs

1//! `PathResolver` and its builder.
2
3use lru::LruCache;
4use std::{cell::RefCell, num::NonZeroUsize, path::PathBuf};
5
6use crate::volume::Volume;
7
8use super::{
9    PathResolvableEntry,
10    resolve::{DirLruCache, resolve_path, resolve_path_with_cache},
11};
12
13/// Resolves current on-disk paths from file IDs on an NTFS/ReFS volume.
14///
15/// Use [`PathResolver::new`] to configure and construct an instance:
16///
17/// ```no_run
18/// use usn_journal_rs::{volume::Volume, path::PathResolver};
19///
20/// let volume = Volume::from_drive_letter('C').unwrap();
21///
22/// // Default resolver — live syscall resolution without caching:
23/// let resolver = PathResolver::new(&volume);
24///
25/// // Opt into caching for a stable directory tree:
26/// let resolver = PathResolver::new(&volume).with_directory_cache(8_192);
27///
28/// // Disable the directory cache entirely (pass 0):
29/// let resolver = PathResolver::new(&volume).with_directory_cache(0);
30/// ```
31///
32/// [`resolve_path`](Self::resolve_path) takes `&self`: the directory cache and
33/// scratch buffer are held behind interior mutability, so a single resolver can
34/// be shared across an iteration loop without a `mut` binding. `PathResolver` is
35/// intentionally `!Sync` because that interior state is not synchronized.
36#[derive(Debug)]
37pub struct PathResolver<'a> {
38    /// Volume on which file IDs will be resolved.
39    volume: &'a Volume,
40    /// Optional cache of previously resolved directory paths.
41    pub(super) dir_fid_path_cache: RefCell<Option<DirLruCache>>,
42    /// Reusable heap buffer for `GetFileInformationByHandleEx` calls.
43    buffer: RefCell<Vec<u8>>,
44}
45
46impl<'a> PathResolver<'a> {
47    /// Create an uncached resolver for the given `volume`.
48    ///
49    /// Use [`Self::with_directory_cache`] to opt into caching for stable trees.
50    ///
51    /// This resolver is intended for live/current path resolution against the
52    /// mounted volume.
53    #[must_use]
54    pub fn new(volume: &'a Volume) -> Self {
55        Self {
56            volume,
57            dir_fid_path_cache: RefCell::new(None),
58            buffer: RefCell::new(Vec::new()),
59        }
60    }
61
62    /// Create a resolver with the historical 4096-entry directory cache.
63    /// Only use caching while the directory tree is stable.
64    #[must_use]
65    pub fn new_with_cache(volume: &'a Volume) -> Self {
66        Self::new(volume).with_directory_cache(4096)
67    }
68
69    /// Set the directory path cache capacity.
70    ///
71    /// Pass a positive `capacity` to enable (or resize) the cache; pass `0`
72    /// to disable it entirely.  When the cache is disabled, each directory
73    /// lookup falls back to direct `OpenFileById` syscalls.
74    ///
75    /// Caching is disabled by default. Cached paths are not invalidated when
76    /// directories move or ancestors are renamed. Only enable caching while
77    /// the directory tree is stable; rebuild the cache after topology changes.
78    #[must_use]
79    pub fn with_directory_cache(mut self, capacity: usize) -> Self {
80        self.dir_fid_path_cache = RefCell::new(NonZeroUsize::new(capacity).map(LruCache::new));
81        self
82    }
83
84    /// Resolve `entry` to its current on-disk path, using the directory cache
85    /// when configured and falling back to `OpenFileById` syscalls.
86    ///
87    /// Takes `&self`: the cache and scratch buffer are updated through interior
88    /// mutability, so the same resolver can be reused across an iterator without
89    /// a `mut` binding.
90    ///
91    /// Standard 64-bit NTFS IDs and extended 128-bit IDs (for example ReFS
92    /// `USN_RECORD_V3` entries) are both resolved via the live volume.
93    #[must_use]
94    pub fn resolve_path<E: PathResolvableEntry + ?Sized>(&self, entry: &E) -> Option<PathBuf> {
95        self.try_resolve_path(entry).ok()
96    }
97
98    /// Resolve a path while preserving the underlying OS failure.
99    /// Tries the parent first, then the entry itself. If both fail, returns the
100    /// entry lookup error. Successful parent lookup uses the supplied leaf name.
101    pub fn try_resolve_path<E: PathResolvableEntry + ?Sized>(
102        &self,
103        entry: &E,
104    ) -> crate::UsnResult<PathBuf> {
105        let mut cache_guard = self.dir_fid_path_cache.borrow_mut();
106        if let Some(cache) = cache_guard.as_mut() {
107            Ok(resolve_path_with_cache(
108                self.volume,
109                entry.fid(),
110                entry.parent_fid(),
111                entry.file_name(),
112                entry.is_dir(),
113                cache,
114                &self.buffer,
115            )?)
116        } else {
117            drop(cache_guard);
118            Ok(resolve_path(
119                self.volume,
120                entry.fid(),
121                entry.parent_fid(),
122                entry.file_name(),
123                &self.buffer,
124            )?)
125        }
126    }
127}
128
129impl Volume {
130    /// Create a live [`PathResolver`] for this volume.
131    ///
132    /// Convenience for [`PathResolver::new`] (uncached live resolution).
133    #[must_use]
134    pub fn path_resolver(&self) -> PathResolver<'_> {
135        PathResolver::new(self)
136    }
137}
138
139impl Volume {
140    /// Create a resolver with a 4096-entry cache for a stable directory tree.
141    #[must_use]
142    pub fn path_resolver_with_cache(&self) -> PathResolver<'_> {
143        PathResolver::new_with_cache(self)
144    }
145}