Skip to main content

vfsi_core/api/
visit.rs

1use crate::api::{Attributes, DepthLimit, ResourceLimits};
2use std::num::NonZeroUsize;
3
4#[bitfields::bitfield(u8)]
5#[derive(PartialEq, Eq)]
6struct VisitFlags {
7    recursive: bool,
8    truncate: bool,
9    entries_unlimited: bool,
10    bytes_unlimited: bool,
11    enter_leave: bool,
12    sort_by_name: bool,
13    no_follow: bool,
14    #[bits(1)]
15    _reserved: u8,
16}
17
18/// Shared directory traversal policy for collection and paged visiting.
19/// Defaults to immediate children only. Collection retains native batching;
20/// visiting delivers owned directory pages incrementally through callbacks.
21/// Unspecified budgets inherit the client's resource limits.
22/// Require no symlink components when false; metadata's final-component
23/// option has a deliberately different name.
24///
25/// ```compile_fail
26/// use vfsi_core::api::ListDirOptions;
27/// ListDirOptions::new().with_follow_symlinks(false);
28/// ```
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30#[must_use = "configuration and requests have no effect until used"]
31pub struct ListDirOptions {
32    flags: VisitFlags,
33    entries: Option<NonZeroUsize>,
34    bytes: Option<NonZeroUsize>,
35    depth: Option<DepthLimit>,
36    fields: Option<Attributes>,
37}
38
39impl ListDirOptions {
40    /// Explicitly disable entry, path-byte and depth limits. This can permit
41    /// unbounded collection or traversal; prefer finite application budgets.
42    pub const fn unlimited() -> Self {
43        let mut flags = VisitFlags::new();
44        flags.set_entries_unlimited(true);
45        flags.set_bytes_unlimited(true);
46        Self {
47            flags,
48            entries: None,
49            bytes: None,
50            depth: Some(DepthLimit::unlimited()),
51            fields: None,
52        }
53    }
54    pub const fn new() -> Self {
55        Self {
56            flags: VisitFlags::new(),
57            entries: None,
58            bytes: None,
59            depth: None,
60            fields: None,
61        }
62    }
63    /// Descend into directories, without following entry symlinks.
64    pub const fn with_recursive(mut self, value: bool) -> Self {
65        self.flags.set_recursive(value);
66        self
67    }
68    /// Emit directory Enter/Leave events, including the root, in
69    /// [`super::VfsiExt::listdir`]. Otherwise only child Entry events are emitted.
70    /// Lifecycle traversal uses bounded directory buffers for depth-first order.
71    /// Other directory methods reject this setting, including [`super::Vfsi::vlistdirs`].
72    pub const fn with_enter_leave(mut self, value: bool) -> Self {
73        self.flags.set_enter_leave(value);
74        self
75    }
76    /// Sort siblings by filename in [`super::VfsiExt::listdir`]. Sorting
77    /// requires a complete, bounded directory listing before delivering entries.
78    /// Other directory methods reject this setting; page order remains backend-defined.
79    pub const fn with_sort_by_name(mut self, value: bool) -> Self {
80        self.flags.set_sort_by_name(value);
81        self
82    }
83    /// Whether resolving an input directory path may follow symlinks (default:
84    /// true). False requires no-follow resolution, including parent components;
85    /// backends unable to guarantee it return Unsupported. Recursive entry
86    /// symlinks are never traversed regardless of this setting. Buffered
87    /// recursive/lifecycle `listdir` enforces false when reopening directories.
88    pub const fn with_allow_symlink_components(mut self, value: bool) -> Self {
89        self.flags.set_no_follow(!value);
90        self
91    }
92    /// Shared entry budget across every input root.
93    pub const fn with_max_entries(mut self, value: usize) -> Self {
94        self.entries = match value.checked_add(1) {
95            Some(encoded) => NonZeroUsize::new(encoded),
96            None => None,
97        };
98        self.flags.set_entries_unlimited(value == usize::MAX);
99        self
100    }
101    /// Shared path-byte budget. Recursive visiting also charges root paths.
102    pub const fn with_max_path_bytes(mut self, value: usize) -> Self {
103        self.bytes = match value.checked_add(1) {
104            Some(encoded) => NonZeroUsize::new(encoded),
105            None => None,
106        };
107        self.flags.set_bytes_unlimited(value == usize::MAX);
108        self
109    }
110    /// Root listing is depth 0; depth 1 also lists immediate subdirectories.
111    /// In recursive mode, encountering a directory beyond this limit is an
112    /// error unless `truncate_at_max_depth(true)` is selected. Shallow mode
113    /// ignores depth settings and intentionally visits only immediate children.
114    /// Use a checked finite depth or explicitly select an unlimited depth.
115    pub const fn with_max_depth(mut self, value: DepthLimit) -> Self {
116        self.depth = Some(value);
117        self
118    }
119    /// Intentionally stop descending at the depth limit instead of failing.
120    pub const fn with_truncate_at_max_depth(mut self, value: bool) -> Self {
121        self.flags.set_truncate(value);
122        self
123    }
124    /// Select metadata returned with directory pages, without per-entry stat
125    /// calls. Defaults to common stat fields. MODE is always requested for
126    /// traversal; backends may return additional fields or omit unsupported ones.
127    pub const fn with_fields(mut self, value: Attributes) -> Self {
128        self.fields = Some(value);
129        self
130    }
131    pub const fn recursive(self) -> bool {
132        self.flags.recursive()
133    }
134    pub const fn enter_leave(self) -> bool {
135        self.flags.enter_leave()
136    }
137    pub const fn sort_by_name(self) -> bool {
138        self.flags.sort_by_name()
139    }
140    pub const fn allow_symlink_components(self) -> bool {
141        !self.flags.no_follow()
142    }
143    pub fn fields(self) -> Attributes {
144        self.fields.unwrap_or_else(Attributes::stat) | Attributes::MODE
145    }
146    /// Configured entry budget, or the built-in default before client resolution.
147    pub fn max_entries(self) -> usize {
148        if self.flags.entries_unlimited() {
149            usize::MAX
150        } else {
151            self.entries
152                .map_or(super::DEFAULT_DIRECTORY_MAX_ENTRIES, |v| v.get() - 1)
153        }
154    }
155    /// Configured path-byte budget, or the built-in default before client resolution.
156    pub fn max_path_bytes(self) -> usize {
157        if self.flags.bytes_unlimited() {
158            usize::MAX
159        } else {
160            self.bytes
161                .map_or(super::DEFAULT_DIRECTORY_MAX_PATH_BYTES, |v| v.get() - 1)
162        }
163    }
164    pub fn max_depth(self) -> DepthLimit {
165        self.depth
166            .unwrap_or_else(|| DepthLimit::new(super::DEFAULT_WALK_MAX_DEPTH).unwrap())
167    }
168    pub const fn truncate_at_max_depth(self) -> bool {
169        self.flags.truncate()
170    }
171    /// Apply client defaults while preserving explicit zero and unlimited budgets.
172    pub fn resolve(mut self, limits: ResourceLimits) -> Self {
173        let entries = if self.entries.is_some() || self.flags.entries_unlimited() {
174            self.max_entries()
175        } else {
176            limits.max_directory_entries()
177        };
178        let bytes = if self.bytes.is_some() || self.flags.bytes_unlimited() {
179            self.max_path_bytes()
180        } else {
181            limits.max_directory_path_bytes()
182        };
183        self.depth = Some(self.depth.unwrap_or_else(|| limits.max_walk_depth()));
184        self.with_max_entries(entries).with_max_path_bytes(bytes)
185    }
186    /// Resolve a recursive traversal against the client's defaults.
187    pub fn walk_options(self, limits: ResourceLimits) -> Self {
188        self.with_recursive(true).resolve(limits)
189    }
190    /// Resolve a shallow listing against the client's defaults.
191    pub fn directory_options(self, limits: ResourceLimits) -> Self {
192        self.with_recursive(false).resolve(limits)
193    }
194}
195
196#[cfg(test)]
197mod option_layout_tests {
198    use super::*;
199    #[allow(dead_code)]
200    struct PreviousLayout {
201        recursive: bool,
202        entries: Option<usize>,
203        bytes: Option<usize>,
204        depth: Option<usize>,
205        truncate: bool,
206        fields: Option<Attributes>,
207    }
208    #[test]
209    fn compact_traversal_options_preserve_zero_overrides_and_independent_bits() {
210        assert_eq!(std::mem::size_of::<VisitFlags>(), 1);
211        assert!(std::mem::size_of::<ListDirOptions>() < std::mem::size_of::<PreviousLayout>());
212        let limits = ResourceLimits::new()
213            .with_max_directory_entries(usize::MAX)
214            .with_max_directory_path_bytes(13)
215            .with_max_walk_depth(crate::api::DepthLimit::new(3).expect("finite depth"));
216        let inherited = ListDirOptions::new().walk_options(limits);
217        assert!(!ListDirOptions::new().enter_leave());
218        assert!(!ListDirOptions::new().sort_by_name());
219        assert!(ListDirOptions::new().allow_symlink_components());
220        assert_eq!(inherited.max_entries(), limits.walk_options().max_entries());
221        assert_eq!(
222            inherited.max_depth().get(),
223            limits.walk_options().max_depth().get()
224        );
225        assert_eq!(inherited.max_path_bytes(), 13);
226        for value in [0, 1, 200, usize::MAX] {
227            let options = ListDirOptions::new()
228                .with_max_entries(value)
229                .with_max_path_bytes(value)
230                .with_max_depth(if value == usize::MAX {
231                    crate::api::DepthLimit::unlimited()
232                } else {
233                    crate::api::DepthLimit::new(value).expect("finite depth")
234                })
235                .with_recursive(true)
236                .with_enter_leave(true)
237                .with_sort_by_name(true)
238                .with_allow_symlink_components(false)
239                .with_truncate_at_max_depth(true)
240                .with_fields(Attributes::SIZE);
241            let walk = options.walk_options(limits);
242            assert_eq!(walk.max_entries(), value);
243            assert_eq!(walk.fields(), options.fields());
244            assert!(!walk.allow_symlink_components());
245            assert_eq!(walk.max_path_bytes(), value);
246            assert_eq!(walk.max_depth().get(), value);
247            assert!(walk.truncate_at_max_depth());
248            assert!(options.recursive());
249            assert!(options.enter_leave());
250            assert!(options.sort_by_name());
251            assert!(!options.allow_symlink_components());
252            assert_eq!(options.fields(), Attributes::SIZE | Attributes::MODE);
253            let changed = options
254                .with_recursive(false)
255                .with_truncate_at_max_depth(false)
256                .with_enter_leave(false)
257                .with_sort_by_name(false);
258            let changed = changed.with_allow_symlink_components(true);
259            assert!(!changed.recursive());
260            assert!(!changed.enter_leave());
261            assert!(!changed.sort_by_name());
262            assert!(changed.allow_symlink_components());
263            assert!(!changed.walk_options(limits).truncate_at_max_depth());
264            assert_eq!(changed.directory_options(limits).max_entries(), value);
265            let zero = changed
266                .with_max_entries(0)
267                .with_max_path_bytes(0)
268                .with_max_depth(crate::api::DepthLimit::new(0).expect("finite depth"));
269            assert_eq!(zero.walk_options(limits).max_entries(), 0);
270            assert_eq!(zero.walk_options(limits).max_path_bytes(), 0);
271            assert_eq!(zero.walk_options(limits).max_depth().get(), 0);
272        }
273        assert_eq!(
274            ListDirOptions::unlimited()
275                .walk_options(limits)
276                .max_depth()
277                .get(),
278            usize::MAX
279        );
280        eprintln!(
281            "ListDirOptions: {} -> {} bytes",
282            std::mem::size_of::<PreviousLayout>(),
283            std::mem::size_of::<ListDirOptions>()
284        );
285    }
286}