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