Skip to main content

vfsi_core/api/
support.rs

1use super::*;
2use std::path::PathBuf;
3
4/// Default payload limit for APIs that allocate and return complete contents.
5pub const DEFAULT_READ_MAX_BYTES: usize = 16 * 1024 * 1024;
6
7/// Default size of each chunk delivered by the bounded single-file stream API.
8pub const DEFAULT_READ_STREAM_CHUNK_BYTES: usize = 1024 * 1024;
9
10/// Default aggregate payload limit for complete-file vector reads.
11pub const DEFAULT_READ_ALLV_MAX_TOTAL_BYTES: usize = DEFAULT_READ_MAX_BYTES;
12
13/// Tuning options for bounded single-file streaming reads.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15#[must_use = "configuration and requests have no effect until used"]
16pub struct StreamOptions {
17    chunk_size: std::num::NonZeroUsize,
18}
19
20impl StreamOptions {
21    pub const fn new() -> Self {
22        Self {
23            chunk_size: std::num::NonZeroUsize::new(DEFAULT_READ_STREAM_CHUNK_BYTES).unwrap(),
24        }
25    }
26
27    /// Set the nonzero maximum bytes delivered to the callback at once.
28    ///
29    /// ```compile_fail
30    /// use vfsi_core::api::StreamOptions;
31    /// let options = StreamOptions::new().with_chunk_size(0);
32    /// ```
33    ///
34    /// NFS and other backends may return smaller chunks due to negotiated
35    /// protocol limits. A larger setting does not bypass those limits.
36    pub const fn with_chunk_size(mut self, bytes: std::num::NonZeroUsize) -> Self {
37        self.chunk_size = bytes;
38        self
39    }
40
41    pub const fn chunk_size(self) -> usize {
42        self.chunk_size.get()
43    }
44}
45
46impl Default for StreamOptions {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52/// Default maximum number of entries returned by allocating directory APIs.
53pub const DEFAULT_DIRECTORY_MAX_ENTRIES: usize = 100_000;
54
55/// Default combined path-storage budget for allocating directory APIs.
56pub const DEFAULT_DIRECTORY_MAX_PATH_BYTES: usize = 16 * 1024 * 1024;
57
58/// Default recursion depth for recursive walks.
59pub const DEFAULT_WALK_MAX_DEPTH: usize = 128;
60
61/// A checked finite traversal depth or an explicitly unlimited traversal.
62/// The root has depth zero. Unlimited depth retains entry and byte budgets.
63/// Finite depths 0 through 65,533 are representable; larger values are rejected.
64/// Both this type and `Option<DepthLimit>` occupy two bytes.
65#[repr(transparent)]
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct DepthLimit(std::num::NonZeroU16);
68
69impl Default for DepthLimit {
70    fn default() -> Self {
71        Self::new(DEFAULT_WALK_MAX_DEPTH).unwrap()
72    }
73}
74
75impl DepthLimit {
76    /// Construct a finite limit. Out-of-range input never disables the limit.
77    pub const fn new(depth: usize) -> Option<Self> {
78        if depth <= u16::MAX as usize - 2 {
79            Some(Self(std::num::NonZeroU16::new((depth + 1) as u16).unwrap()))
80        } else {
81            None
82        }
83    }
84    /// Disable the depth limit while retaining other traversal budgets.
85    pub const fn unlimited() -> Self {
86        Self(std::num::NonZeroU16::MAX)
87    }
88    /// Effective finite limit, or usize::MAX for unlimited traversal.
89    pub const fn get(self) -> usize {
90        if self.is_unlimited() {
91            usize::MAX
92        } else {
93            self.0.get() as usize - 1
94        }
95    }
96    pub const fn is_unlimited(self) -> bool {
97        self.0.get() == u16::MAX
98    }
99}
100
101/// Default aggregate payload limit for owned vector reads.
102pub const DEFAULT_READV_MAX_TOTAL_BYTES: usize = DEFAULT_READ_MAX_BYTES;
103
104/// Application result for one positional vector read, in request order.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct FsReadResult {
107    pub offset: u64,
108    pub data: Vec<u8>,
109    pub eof: bool,
110}
111
112/// Application result for one read into caller storage, in request order.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct ReadIntoResult {
115    pub offset: u64,
116    pub bytes_read: usize,
117    pub eof: bool,
118}
119
120/// Application result for one vector write, in request order.
121/// For completed append writes, `offset + bytes_written` is the last reported end;
122/// concurrent writers can interleave between short-write waves.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct WriteResult {
125    pub offset: u64,
126    pub bytes_written: usize,
127    pub stable: bool,
128}
129
130/// Completion of a streaming read. Stopping does not mean EOF.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum StreamCompletion {
133    Complete,
134    Stopped { next_offset: u64 },
135}
136
137/// Completion of a callback-based directory traversal.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum TraversalCompletion {
140    Complete,
141    Stopped,
142}
143
144/// Default resource policy for a client. Per-call options override it.
145/// Byte limits bound logical payload, not allocator capacity or RPC overhead.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[must_use = "configuration and requests have no effect until used"]
148pub struct ResourceLimits {
149    /// Maximum collected scalar payload or aggregate vector read buffers.
150    /// Not a process-memory cap; standard `Read::read_to_end` and explicit
151    /// positional file reads remain caller-managed.
152    max_read_bytes: usize,
153    /// Default request size for client `read_stream`, not file `Read` or pools.
154    stream_chunk_bytes: std::num::NonZeroUsize,
155    /// Aggregate entries delivered/collected by a directory call or tree walk.
156    max_directory_entries: usize,
157    /// Aggregate logical path bytes, excluding allocator and metadata overhead.
158    max_directory_path_bytes: usize,
159    /// Maximum descent depth; the starting directory is depth zero.
160    max_walk_depth: DepthLimit,
161}
162
163impl Default for ResourceLimits {
164    fn default() -> Self {
165        Self {
166            max_read_bytes: DEFAULT_READ_MAX_BYTES,
167            stream_chunk_bytes: std::num::NonZeroUsize::new(DEFAULT_READ_STREAM_CHUNK_BYTES)
168                .unwrap(),
169            max_directory_entries: crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES,
170            max_directory_path_bytes: crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES,
171            max_walk_depth: DepthLimit::new(crate::api::DEFAULT_WALK_MAX_DEPTH).unwrap(),
172        }
173    }
174}
175
176impl ResourceLimits {
177    pub fn new() -> Self {
178        Self::default()
179    }
180    /// Set the collected read budget; zero permits only empty payloads.
181    pub const fn with_max_read_bytes(mut self, value: usize) -> Self {
182        self.max_read_bytes = value;
183        self
184    }
185    /// Read the configured policy value.
186    pub const fn max_read_bytes(self) -> usize {
187        self.max_read_bytes
188    }
189    /// Set the default streaming chunk size. Nonzero chunks guarantee progress.
190    pub const fn with_stream_chunk_size(mut self, value: std::num::NonZeroUsize) -> Self {
191        self.stream_chunk_bytes = value;
192        self
193    }
194    /// Read the configured policy value.
195    pub const fn stream_chunk_size(self) -> usize {
196        self.stream_chunk_bytes.get()
197    }
198    /// Set the aggregate directory entry budget; zero is a valid budget.
199    pub const fn with_max_directory_entries(mut self, value: usize) -> Self {
200        self.max_directory_entries = value;
201        self
202    }
203    /// Read the configured policy value.
204    pub const fn max_directory_entries(self) -> usize {
205        self.max_directory_entries
206    }
207    /// Set the aggregate path byte budget; zero is a valid budget.
208    pub const fn with_max_directory_path_bytes(mut self, value: usize) -> Self {
209        self.max_directory_path_bytes = value;
210        self
211    }
212    /// Read the configured policy value.
213    pub const fn max_directory_path_bytes(self) -> usize {
214        self.max_directory_path_bytes
215    }
216    /// Set maximum descent depth; zero includes only the starting directory.
217    pub const fn with_max_walk_depth(mut self, value: DepthLimit) -> Self {
218        self.max_walk_depth = value;
219        self
220    }
221    /// Read the configured policy value.
222    pub const fn max_walk_depth(self) -> DepthLimit {
223        self.max_walk_depth
224    }
225
226    pub fn directory_options(self) -> ListDirOptions {
227        ListDirOptions::new()
228            .with_max_entries(self.max_directory_entries)
229            .with_max_path_bytes(self.max_directory_path_bytes)
230    }
231
232    pub fn walk_options(self) -> crate::api::ListDirOptions {
233        crate::api::ListDirOptions::new()
234            .with_recursive(true)
235            .with_max_entries(self.max_directory_entries)
236            .with_max_path_bytes(self.max_directory_path_bytes)
237            .with_max_depth(self.max_walk_depth)
238    }
239}
240
241/// One directory and its entries, with attributes fetched during enumeration.
242#[derive(Clone, Debug, PartialEq, Eq)]
243pub struct DirectoryListing {
244    pub path: PathBuf,
245    pub entries: Vec<DirEntry>,
246}
247
248#[cfg(test)]
249mod depth_limit_tests {
250    use super::*;
251    #[test]
252    fn checked_depths_preserve_finite_limits_and_inheritance() {
253        for depth in [0, 1, 128, 200, 201, 500, 65533] {
254            let limit = DepthLimit::new(depth).unwrap();
255            assert!(!limit.is_unlimited());
256            let limits = ResourceLimits::new().with_max_walk_depth(limit);
257            assert_eq!(
258                ListDirOptions::new()
259                    .resolve(limits)
260                    .max_depth_or(Default::default()),
261                limit
262            );
263        }
264        for invalid in [65534, 65535, usize::MAX] {
265            assert!(DepthLimit::new(invalid).is_none());
266        }
267        assert!(
268            ListDirOptions::new()
269                .resolve(ResourceLimits::new().with_max_walk_depth(DepthLimit::unlimited()))
270                .max_depth_or(Default::default())
271                .is_unlimited()
272        );
273    }
274}