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 DepthLimit {
70    /// Construct a finite limit. Out-of-range input never disables the limit.
71    pub const fn new(depth: usize) -> Option<Self> {
72        if depth <= u16::MAX as usize - 2 {
73            Some(Self(std::num::NonZeroU16::new((depth + 1) as u16).unwrap()))
74        } else {
75            None
76        }
77    }
78    /// Disable the depth limit while retaining other traversal budgets.
79    pub const fn unlimited() -> Self {
80        Self(std::num::NonZeroU16::MAX)
81    }
82    /// Effective finite limit, or usize::MAX for unlimited traversal.
83    pub const fn get(self) -> usize {
84        if self.is_unlimited() {
85            usize::MAX
86        } else {
87            self.0.get() as usize - 1
88        }
89    }
90    pub const fn is_unlimited(self) -> bool {
91        self.0.get() == u16::MAX
92    }
93}
94
95/// Default aggregate payload limit for owned vector reads.
96pub const DEFAULT_READV_MAX_TOTAL_BYTES: usize = DEFAULT_READ_MAX_BYTES;
97
98/// Application result for one positional vector read, in request order.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct FsReadResult {
101    pub offset: u64,
102    pub data: Vec<u8>,
103    pub eof: bool,
104}
105
106/// Application result for one read into caller storage, in request order.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct ReadIntoResult {
109    pub offset: u64,
110    pub bytes_read: usize,
111    pub eof: bool,
112}
113
114/// Application result for one vector write, in request order.
115/// For completed append writes, `offset + bytes_written` is the last reported end;
116/// concurrent writers can interleave between short-write waves.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct WriteResult {
119    pub offset: u64,
120    pub bytes_written: usize,
121    pub stable: bool,
122}
123
124/// Completion of a streaming read. Stopping does not mean EOF.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum StreamCompletion {
127    Complete,
128    Stopped { next_offset: u64 },
129}
130
131/// Completion of a callback-based directory traversal.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum TraversalCompletion {
134    Complete,
135    Stopped,
136}
137
138/// Default resource policy for a client. Per-call options override it.
139/// Byte limits bound logical payload, not allocator capacity or RPC overhead.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141#[must_use = "configuration and requests have no effect until used"]
142pub struct ResourceLimits {
143    /// Maximum collected scalar payload or aggregate vector read buffers.
144    /// Not a process-memory cap; standard `Read::read_to_end` and explicit
145    /// positional file reads remain caller-managed.
146    max_read_bytes: usize,
147    /// Default request size for client `read_stream`, not file `Read` or pools.
148    stream_chunk_bytes: std::num::NonZeroUsize,
149    /// Aggregate entries delivered/collected by a directory call or tree walk.
150    max_directory_entries: usize,
151    /// Aggregate logical path bytes, excluding allocator and metadata overhead.
152    max_directory_path_bytes: usize,
153    /// Maximum descent depth; the starting directory is depth zero.
154    max_walk_depth: DepthLimit,
155}
156
157impl Default for ResourceLimits {
158    fn default() -> Self {
159        Self {
160            max_read_bytes: DEFAULT_READ_MAX_BYTES,
161            stream_chunk_bytes: std::num::NonZeroUsize::new(DEFAULT_READ_STREAM_CHUNK_BYTES)
162                .unwrap(),
163            max_directory_entries: crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES,
164            max_directory_path_bytes: crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES,
165            max_walk_depth: DepthLimit::new(crate::api::DEFAULT_WALK_MAX_DEPTH).unwrap(),
166        }
167    }
168}
169
170impl ResourceLimits {
171    pub fn new() -> Self {
172        Self::default()
173    }
174    /// Set the collected read budget; zero permits only empty payloads.
175    pub const fn with_max_read_bytes(mut self, value: usize) -> Self {
176        self.max_read_bytes = value;
177        self
178    }
179    /// Read the configured policy value.
180    pub const fn max_read_bytes(self) -> usize {
181        self.max_read_bytes
182    }
183    /// Set the default streaming chunk size. Nonzero chunks guarantee progress.
184    pub const fn with_stream_chunk_size(mut self, value: std::num::NonZeroUsize) -> Self {
185        self.stream_chunk_bytes = value;
186        self
187    }
188    /// Read the configured policy value.
189    pub const fn stream_chunk_size(self) -> usize {
190        self.stream_chunk_bytes.get()
191    }
192    /// Set the aggregate directory entry budget; zero is a valid budget.
193    pub const fn with_max_directory_entries(mut self, value: usize) -> Self {
194        self.max_directory_entries = value;
195        self
196    }
197    /// Read the configured policy value.
198    pub const fn max_directory_entries(self) -> usize {
199        self.max_directory_entries
200    }
201    /// Set the aggregate path byte budget; zero is a valid budget.
202    pub const fn with_max_directory_path_bytes(mut self, value: usize) -> Self {
203        self.max_directory_path_bytes = value;
204        self
205    }
206    /// Read the configured policy value.
207    pub const fn max_directory_path_bytes(self) -> usize {
208        self.max_directory_path_bytes
209    }
210    /// Set maximum descent depth; zero includes only the starting directory.
211    pub const fn with_max_walk_depth(mut self, value: DepthLimit) -> Self {
212        self.max_walk_depth = value;
213        self
214    }
215    /// Read the configured policy value.
216    pub const fn max_walk_depth(self) -> DepthLimit {
217        self.max_walk_depth
218    }
219
220    pub fn directory_options(self) -> ListDirOptions {
221        ListDirOptions::new()
222            .with_max_entries(self.max_directory_entries)
223            .with_max_path_bytes(self.max_directory_path_bytes)
224    }
225
226    pub fn walk_options(self) -> crate::api::ListDirOptions {
227        crate::api::ListDirOptions::new()
228            .with_recursive(true)
229            .with_max_entries(self.max_directory_entries)
230            .with_max_path_bytes(self.max_directory_path_bytes)
231            .with_max_depth(self.max_walk_depth)
232    }
233}
234
235/// One directory and its entries, with attributes fetched during enumeration.
236#[derive(Clone, Debug, PartialEq, Eq)]
237pub struct DirectoryListing {
238    pub path: PathBuf,
239    pub entries: Vec<DirEntry>,
240}
241
242#[cfg(test)]
243mod depth_limit_tests {
244    use super::*;
245    #[test]
246    fn checked_depths_preserve_finite_limits_and_inheritance() {
247        for depth in [0, 1, 128, 200, 201, 500, 65533] {
248            let limit = DepthLimit::new(depth).unwrap();
249            assert!(!limit.is_unlimited());
250            let limits = ResourceLimits::new().with_max_walk_depth(limit);
251            assert_eq!(ListDirOptions::new().resolve(limits).max_depth(), limit);
252        }
253        for invalid in [65534, 65535, usize::MAX] {
254            assert!(DepthLimit::new(invalid).is_none());
255        }
256        assert!(
257            ListDirOptions::new()
258                .resolve(ResourceLimits::new().with_max_walk_depth(DepthLimit::unlimited()))
259                .max_depth()
260                .is_unlimited()
261        );
262    }
263}