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