Skip to main content

qubit_fs/metadata/
file_system_limits.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Stable configured filesystem limits.
9
10use crate::error::FsError;
11use crate::error::FsErrorKind;
12use crate::error::FsOperation;
13use crate::error::FsResult;
14use crate::metadata::FileSystemLimit;
15use crate::path::Path;
16use crate::path::PathSemantics;
17
18/// Stable limits declared by a configured filesystem provider.
19///
20/// # Examples
21///
22/// ```rust
23/// use qubit_fs::metadata::{FileSystemLimit, FileSystemLimits};
24///
25/// let limits = FileSystemLimits::unknown();
26/// assert_eq!(FileSystemLimit::Unknown, limits.max_path_text_bytes());
27/// ```
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct FileSystemLimits {
30    /// Maximum encoded bytes accepted for a complete logical path.
31    max_path_text_bytes: FileSystemLimit,
32    /// Maximum encoded bytes accepted for one path component.
33    max_component_text_bytes: FileSystemLimit,
34    /// Maximum bytes accepted by one range-read request.
35    max_read_range_bytes: FileSystemLimit,
36    /// Maximum bytes accepted by one write session.
37    max_write_bytes: FileSystemLimit,
38    /// Maximum entries requested in one listing page.
39    max_list_page_entries: FileSystemLimit,
40}
41
42impl FileSystemLimits {
43    /// Creates a limit snapshot whose dimensions are all explicitly unknown.
44    #[inline]
45    #[must_use]
46    pub const fn unknown() -> Self {
47        Self {
48            max_path_text_bytes: FileSystemLimit::Unknown,
49            max_component_text_bytes: FileSystemLimit::Unknown,
50            max_read_range_bytes: FileSystemLimit::Unknown,
51            max_write_bytes: FileSystemLimit::Unknown,
52            max_list_page_entries: FileSystemLimit::Unknown,
53        }
54    }
55
56    /// Returns a copy with the path-text byte limit replaced by `limit`.
57    #[inline]
58    #[must_use]
59    pub const fn with_max_path_text_bytes(mut self, limit: FileSystemLimit) -> Self {
60        self.max_path_text_bytes = limit;
61        self
62    }
63
64    /// Returns a copy with the component-text byte limit replaced by `limit`.
65    #[inline]
66    #[must_use]
67    pub const fn with_max_component_text_bytes(mut self, limit: FileSystemLimit) -> Self {
68        self.max_component_text_bytes = limit;
69        self
70    }
71
72    /// Returns a copy with the range-read byte limit replaced by `limit`.
73    #[inline]
74    #[must_use]
75    pub const fn with_max_read_range_bytes(mut self, limit: FileSystemLimit) -> Self {
76        self.max_read_range_bytes = limit;
77        self
78    }
79
80    /// Returns a copy with the write-session byte limit replaced by `limit`.
81    #[inline]
82    #[must_use]
83    pub const fn with_max_write_bytes(mut self, limit: FileSystemLimit) -> Self {
84        self.max_write_bytes = limit;
85        self
86    }
87
88    /// Returns a copy with the native list-page entry limit replaced by
89    /// `limit`.
90    #[inline]
91    #[must_use]
92    pub const fn with_max_list_page_entries(mut self, limit: FileSystemLimit) -> Self {
93        self.max_list_page_entries = limit;
94        self
95    }
96
97    /// Returns the maximum canonical [`crate::path::Path`] text length in UTF-8
98    /// bytes.
99    #[inline]
100    #[must_use]
101    pub const fn max_path_text_bytes(&self) -> FileSystemLimit {
102        self.max_path_text_bytes
103    }
104
105    /// Returns the maximum path-component text length in UTF-8 bytes.
106    #[inline]
107    #[must_use]
108    pub const fn max_component_text_bytes(&self) -> FileSystemLimit {
109        self.max_component_text_bytes
110    }
111
112    /// Returns the maximum byte count accepted by one logical range read.
113    #[inline]
114    #[must_use]
115    pub const fn max_read_range_bytes(&self) -> FileSystemLimit {
116        self.max_read_range_bytes
117    }
118
119    /// Returns the maximum total byte count accepted by one write session.
120    #[inline]
121    #[must_use]
122    pub const fn max_write_bytes(&self) -> FileSystemLimit {
123        self.max_write_bytes
124    }
125
126    /// Returns the maximum entry count in one provider-native list page.
127    #[inline]
128    #[must_use]
129    pub const fn max_list_page_entries(&self) -> FileSystemLimit {
130        self.max_list_page_entries
131    }
132
133    /// Clamps a requested list-page size to the declared finite maximum.
134    ///
135    /// Unknown, unbounded, and inapplicable limits leave the hint unchanged.
136    /// A missing hint remains absent so the provider can select its natural
137    /// page size while still honoring its declared limit.
138    ///
139    /// # Parameters
140    /// - `requested`: Optional caller-supplied page-size hint.
141    ///
142    /// # Returns
143    /// The effective page-size hint forwarded to the provider.
144    #[inline]
145    #[must_use]
146    pub fn clamp_list_page_size(&self, requested: Option<usize>) -> Option<usize> {
147        let requested = requested?;
148        match self.max_list_page_entries {
149            FileSystemLimit::Maximum(maximum) => {
150                usize::try_from(maximum).map_or(Some(requested), |maximum| Some(requested.min(maximum)))
151            }
152            FileSystemLimit::Unknown | FileSystemLimit::NotApplicable | FileSystemLimit::Unbounded => Some(requested),
153        }
154    }
155
156    /// Validates a canonical filesystem path against provider path limits.
157    ///
158    /// Component limits are checked only for hierarchical path semantics.
159    ///
160    /// # Errors
161    /// Returns [`FsErrorKind::ResourceLimitExceeded`] when the complete path
162    /// text or a hierarchical component exceeds its declared finite maximum.
163    pub fn validate_path(&self, path: &Path, semantics: PathSemantics, operation: FsOperation) -> FsResult<()> {
164        if exceeds_usize(self.max_path_text_bytes, path.as_str().len()) {
165            return Err(limit_error(
166                operation,
167                "path text exceeds the provider byte limit",
168                path,
169            ));
170        }
171        if semantics == PathSemantics::Hierarchical
172            && path
173                .as_str()
174                .split('/')
175                .any(|component| !component.is_empty() && exceeds_usize(self.max_component_text_bytes, component.len()))
176        {
177            return Err(limit_error(
178                operation,
179                "path component exceeds the provider byte limit",
180                path,
181            ));
182        }
183        Ok(())
184    }
185
186    /// Validates a requested logical range-read length.
187    ///
188    /// A missing length cannot be preflighted and remains the provider's
189    /// responsibility during execution.
190    ///
191    /// # Errors
192    /// Returns [`FsErrorKind::ResourceLimitExceeded`] when `length` exceeds
193    /// the declared finite range-read maximum.
194    pub fn validate_read_range(&self, path: &Path, length: Option<u64>) -> FsResult<()> {
195        if length.is_some_and(|length| self.max_read_range_bytes.is_exceeded_by(length)) {
196            Err(limit_error(
197                FsOperation::OpenReader,
198                "requested range exceeds the provider byte limit",
199                path,
200            ))
201        } else {
202            Ok(())
203        }
204    }
205
206    /// Validates the total bytes supplied to one write session.
207    ///
208    /// # Errors
209    /// Returns [`FsErrorKind::ResourceLimitExceeded`] when `bytes` exceeds the
210    /// declared finite write-session maximum.
211    pub fn validate_write_size(&self, path: &Path, bytes: usize) -> FsResult<()> {
212        if exceeds_usize(self.max_write_bytes, bytes) {
213            Err(limit_error(
214                FsOperation::Write,
215                "write session exceeds the provider byte limit",
216                path,
217            ))
218        } else {
219            Ok(())
220        }
221    }
222
223    /// Validates a streamed write-session byte length without narrowing it to
224    /// the native pointer-sized integer.
225    ///
226    /// Returns [`FsErrorKind::ResourceLimitExceeded`] when `bytes` exceeds the
227    /// declared finite write-session maximum.
228    pub(crate) fn validate_write_size_u64(&self, path: &Path, bytes: u64) -> FsResult<()> {
229        if self.max_write_bytes.is_exceeded_by(bytes) {
230            Err(limit_error(
231                FsOperation::Write,
232                "write session exceeds the provider byte limit",
233                path,
234            ))
235        } else {
236            Ok(())
237        }
238    }
239}
240
241/// Tests whether a `usize` count exceeds the declared filesystem limit.
242///
243/// Values that cannot be represented as `u64` exceed every finite maximum.
244fn exceeds_usize(limit: FileSystemLimit, actual: usize) -> bool {
245    match u64::try_from(actual) {
246        Ok(actual) => limit.is_exceeded_by(actual),
247        Err(_) => matches!(limit, FileSystemLimit::Maximum(_)),
248    }
249}
250
251/// Builds a path-contextual resource-limit error for `operation`.
252fn limit_error(operation: FsOperation, message: &'static str, path: &Path) -> FsError {
253    FsError::new(FsErrorKind::ResourceLimitExceeded, operation, message).with_path(path.clone())
254}