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}