Skip to main content

vfsi_core/api/
visit.rs

1use crate::api::{DepthLimit, MetadataFields, ReadDirOptions, ResourceLimits, WalkOptions};
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    #[bits(4)]
12    _reserved: u8,
13}
14
15/// Shared directory traversal policy for collection and paged visiting.
16/// Defaults to immediate children only. Collection retains native batching;
17/// visiting delivers owned directory pages incrementally through callbacks.
18/// Unspecified budgets inherit the client's resource limits.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub struct VisitOptions {
21    flags: VisitFlags,
22    entries: Option<NonZeroUsize>,
23    bytes: Option<NonZeroUsize>,
24    depth: Option<DepthLimit>,
25    fields: Option<MetadataFields>,
26}
27
28impl VisitOptions {
29    /// Explicitly disable entry, path-byte and depth limits. This can permit
30    /// unbounded collection or traversal; prefer finite application budgets.
31    pub const fn unlimited() -> Self {
32        let mut flags = VisitFlags::new();
33        flags.set_entries_unlimited(true);
34        flags.set_bytes_unlimited(true);
35        Self {
36            flags,
37            entries: None,
38            bytes: None,
39            depth: Some(DepthLimit::unlimited()),
40            fields: None,
41        }
42    }
43    pub const fn new() -> Self {
44        Self {
45            flags: VisitFlags::new(),
46            entries: None,
47            bytes: None,
48            depth: None,
49            fields: None,
50        }
51    }
52    /// Descend into directories, without following entry symlinks.
53    pub const fn recursive(mut self, value: bool) -> Self {
54        self.flags.set_recursive(value);
55        self
56    }
57    /// Shared entry budget across every input root.
58    pub const fn max_entries(mut self, value: usize) -> Self {
59        self.entries = match value.checked_add(1) {
60            Some(encoded) => NonZeroUsize::new(encoded),
61            None => None,
62        };
63        self.flags.set_entries_unlimited(value == usize::MAX);
64        self
65    }
66    /// Shared path-byte budget. Recursive visiting also charges root paths.
67    pub const fn max_path_bytes(mut self, value: usize) -> Self {
68        self.bytes = match value.checked_add(1) {
69            Some(encoded) => NonZeroUsize::new(encoded),
70            None => None,
71        };
72        self.flags.set_bytes_unlimited(value == usize::MAX);
73        self
74    }
75    /// Root listing is depth 0; depth 1 also lists immediate subdirectories.
76    /// In recursive mode, encountering a directory beyond this limit is an
77    /// error unless `truncate_at_max_depth(true)` is selected. Shallow mode
78    /// ignores depth settings and intentionally visits only immediate children.
79    /// Finite depths range from 0 through 200; any larger value disables
80    /// the depth limit. Entry and path-byte budgets remain in effect.
81    pub const fn max_depth(mut self, value: usize) -> Self {
82        self.depth = Some(DepthLimit::new(value));
83        self
84    }
85    /// Intentionally stop descending at the depth limit instead of failing.
86    pub const fn truncate_at_max_depth(mut self, value: bool) -> Self {
87        self.flags.set_truncate(value);
88        self
89    }
90    /// Select metadata returned with directory pages, without per-entry stat
91    /// calls. Defaults to common stat fields. MODE is always requested for
92    /// traversal; backends may return additional fields or omit unsupported ones.
93    pub const fn fields(mut self, value: MetadataFields) -> Self {
94        self.fields = Some(value);
95        self
96    }
97    pub const fn is_recursive(self) -> bool {
98        self.flags.recursive()
99    }
100    pub fn metadata_fields(self) -> MetadataFields {
101        self.fields.unwrap_or_else(|| {
102            MetadataFields::stat()
103                | MetadataFields::UID
104                | MetadataFields::GID
105                | MetadataFields::ATIME
106                | MetadataFields::MTIME
107                | MetadataFields::CTIME
108                | MetadataFields::CHANGE
109        }) | MetadataFields::MODE
110    }
111    /// Resolve recursive traversal limits against the client's defaults.
112    pub fn walk_options(self, limits: ResourceLimits) -> WalkOptions {
113        let mut options = limits.walk_options();
114        if self.flags.entries_unlimited() {
115            options = options.max_entries(usize::MAX);
116        } else if let Some(encoded) = self.entries {
117            options = options.max_entries(encoded.get() - 1);
118        }
119        if self.flags.bytes_unlimited() {
120            options = options.max_path_bytes(usize::MAX);
121        } else if let Some(encoded) = self.bytes {
122            options = options.max_path_bytes(encoded.get() - 1);
123        }
124        if let Some(depth) = self.depth {
125            options = options.max_depth(depth.get());
126        }
127        options.truncate_at_max_depth(self.flags.truncate())
128    }
129    /// Resolve shallow listing limits against the client's defaults.
130    pub fn directory_options(self, limits: ResourceLimits) -> ReadDirOptions {
131        let mut options = limits.directory_options();
132        if self.flags.entries_unlimited() {
133            options = options.max_entries(usize::MAX);
134        } else if let Some(encoded) = self.entries {
135            options = options.max_entries(encoded.get() - 1);
136        }
137        if self.flags.bytes_unlimited() {
138            options = options.max_path_bytes(usize::MAX);
139        } else if let Some(encoded) = self.bytes {
140            options = options.max_path_bytes(encoded.get() - 1);
141        }
142        options
143    }
144}
145
146impl From<ReadDirOptions> for VisitOptions {
147    fn from(options: ReadDirOptions) -> Self {
148        Self::new()
149            .max_entries(options.entry_limit())
150            .max_path_bytes(options.path_byte_limit())
151    }
152}
153impl From<WalkOptions> for VisitOptions {
154    fn from(options: WalkOptions) -> Self {
155        Self::new()
156            .recursive(true)
157            .max_entries(options.entry_limit())
158            .max_path_bytes(options.path_byte_limit())
159            .max_depth(options.depth_limit())
160            .truncate_at_max_depth(options.truncates_at_depth_limit())
161    }
162}
163
164#[cfg(test)]
165mod option_layout_tests {
166    use super::*;
167    #[allow(dead_code)]
168    struct PreviousLayout {
169        recursive: bool,
170        entries: Option<usize>,
171        bytes: Option<usize>,
172        depth: Option<usize>,
173        truncate: bool,
174        fields: Option<MetadataFields>,
175    }
176    #[test]
177    fn compact_traversal_options_preserve_zero_overrides_and_independent_bits() {
178        assert_eq!(std::mem::size_of::<VisitFlags>(), 1);
179        assert!(std::mem::size_of::<VisitOptions>() < std::mem::size_of::<PreviousLayout>());
180        let limits = ResourceLimits::default();
181        let inherited = VisitOptions::new().walk_options(limits);
182        assert_eq!(inherited.entry_limit(), limits.walk_options().entry_limit());
183        assert_eq!(inherited.depth_limit(), limits.walk_options().depth_limit());
184        for value in [0, 1, 200, usize::MAX] {
185            let options = VisitOptions::new()
186                .max_entries(value)
187                .max_path_bytes(value)
188                .max_depth(value)
189                .recursive(true)
190                .truncate_at_max_depth(true)
191                .fields(MetadataFields::SIZE);
192            let walk = options.walk_options(limits);
193            assert_eq!(walk.entry_limit(), value);
194            assert_eq!(walk.path_byte_limit(), value);
195            assert_eq!(walk.depth_limit(), value);
196            assert!(walk.truncates_at_depth_limit());
197            assert!(options.is_recursive());
198            assert_eq!(
199                options.metadata_fields(),
200                MetadataFields::SIZE | MetadataFields::MODE
201            );
202            let changed = options.recursive(false).truncate_at_max_depth(false);
203            assert!(!changed.is_recursive());
204            assert!(!changed.walk_options(limits).truncates_at_depth_limit());
205            assert_eq!(changed.directory_options(limits).entry_limit(), value);
206            let zero = changed.max_entries(0).max_path_bytes(0).max_depth(0);
207            assert_eq!(zero.walk_options(limits).entry_limit(), 0);
208            assert_eq!(zero.walk_options(limits).path_byte_limit(), 0);
209            assert_eq!(zero.walk_options(limits).depth_limit(), 0);
210        }
211        assert_eq!(
212            VisitOptions::unlimited().walk_options(limits).depth_limit(),
213            usize::MAX
214        );
215        eprintln!(
216            "VisitOptions: {} -> {} bytes",
217            std::mem::size_of::<PreviousLayout>(),
218            std::mem::size_of::<VisitOptions>()
219        );
220    }
221}