qubit_fs/file_system_properties.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8// facade.
9
10//! Immutable filesystem property snapshots used by facades.
11
12use crate::error::FsError;
13use crate::error::FsErrorKind;
14use crate::error::FsOperation;
15use crate::error::FsResult;
16use crate::metadata::FileSystemCapabilities;
17use crate::metadata::FileSystemCapability;
18use crate::metadata::FileSystemInfo;
19use crate::metadata::FileSystemLimits;
20use crate::metadata::SymlinkPolicy;
21use crate::path::Path;
22use crate::path::PathConstraints;
23use crate::path::PathForm;
24use crate::path::PathSemantics;
25use crate::spi::ProviderOperation;
26use crate::spi::ProviderProperties;
27
28/// Immutable construction-time properties cached by a filesystem facade.
29///
30/// A facade exposes the provider's validated identity, limits, path rules, and
31/// effective capabilities through one stable snapshot. The capability set may
32/// include facts derived by the facade; it is therefore not necessarily a
33/// byte-for-byte copy of the provider declaration.
34///
35/// # Examples
36///
37/// ```
38/// use qubit_fs::metadata::{FileSystemCapabilities, FileSystemId, FileSystemInfo,
39/// FileSystemLimit, FileSystemLimits, SymlinkPolicy};
40/// use qubit_fs::path::{PathConstraints, PathForm, PathSemantics};
41/// use qubit_fs::metadata::FileSystemProperties;
42///
43/// let properties = FileSystemProperties::new(
44/// FileSystemInfo::new(FileSystemId::new("example")?, "example", PathSemantics::Hierarchical),
45/// FileSystemCapabilities::new(),
46/// FileSystemLimits::unknown().with_max_path_text_bytes(FileSystemLimit::Maximum(4096)),
47/// PathConstraints::absolute(),
48/// SymlinkPolicy::Reject,
49/// )?;
50/// assert_eq!(properties.info().provider_id(), "example");
51/// assert_eq!(properties.limits().max_path_text_bytes(), FileSystemLimit::Maximum(4096));
52/// assert_eq!(properties.path_constraints().form(), PathForm::Absolute);
53/// # Ok::<(), qubit_fs::FsError>(())
54/// ```
55#[derive(Clone, Debug)]
56pub struct FileSystemProperties {
57 /// Stable filesystem information.
58 info: FileSystemInfo,
59 /// Stable advertised capabilities.
60 capabilities: FileSystemCapabilities,
61 /// Stable provider limits.
62 limits: FileSystemLimits,
63 /// Accepted logical path forms.
64 path_constraints: PathConstraints,
65 /// Provider-declared symbolic-link traversal policy.
66 symlink_policy: SymlinkPolicy,
67}
68
69impl FileSystemProperties {
70 /// Derives the application-visible property snapshot from one validated
71 /// provider snapshot.
72 ///
73 /// The facade adds conditional copy support only when the provider exposes
74 /// metadata, reader, and writer entry points together with corresponding
75 /// read and write capabilities. This method performs no I/O.
76 ///
77 /// # Parameters
78 /// - `provider`: Validated provider operations, guarantees, and limits.
79 ///
80 /// # Returns
81 /// A validated application-visible property snapshot.
82 ///
83 /// # Errors
84 /// Returns an invalid-options error when the derived snapshot violates a
85 /// shared property invariant.
86 pub(crate) fn from_provider(provider: &ProviderProperties) -> FsResult<Self> {
87 let mut capabilities = provider.declared_capabilities();
88 let operations = provider.operations();
89 let streamed_copy = operations.supports(ProviderOperation::Stat)
90 && operations.supports(ProviderOperation::OpenReader)
91 && operations.supports(ProviderOperation::OpenWriter)
92 && capabilities.supports(FileSystemCapability::Read)
93 && capabilities.supports(FileSystemCapability::Write);
94 if streamed_copy && !capabilities.supports(FileSystemCapability::Copy) {
95 capabilities = capabilities.with_conditional(FileSystemCapability::Copy);
96 }
97 Self::new(
98 provider.info().clone(),
99 capabilities,
100 *provider.limits(),
101 provider.path_constraints().clone(),
102 provider.symlink_policy(),
103 )
104 }
105
106 /// Builds and validates an immutable filesystem property snapshot.
107 ///
108 /// This method performs no I/O.
109 ///
110 /// # Parameters
111 /// - `info`: Stable provider identity and path semantics.
112 /// - `capabilities`: Capabilities explicitly advertised by the provider.
113 /// - `limits`: Provider resource and operation limits.
114 /// - `path_constraints`: Accepted absolute and relative path forms.
115 /// - `symlink_policy`: Provider-declared symbolic-link traversal policy.
116 ///
117 /// # Returns
118 /// A validated immutable property snapshot.
119 ///
120 /// # Errors
121 /// Returns an invalid-options error when the provider identity is invalid,
122 /// advertised capabilities violate dependencies, or path configuration is
123 /// internally inconsistent.
124 #[inline]
125 pub fn new(
126 info: FileSystemInfo,
127 capabilities: FileSystemCapabilities,
128 limits: FileSystemLimits,
129 path_constraints: PathConstraints,
130 symlink_policy: SymlinkPolicy,
131 ) -> FsResult<Self> {
132 let properties = Self {
133 info,
134 capabilities,
135 limits,
136 path_constraints,
137 symlink_policy,
138 };
139 properties.validate()?;
140 Ok(properties)
141 }
142
143 /// Returns the stable filesystem identity and configuration.
144 ///
145 /// # Returns
146 /// The immutable provider information snapshot.
147 #[inline]
148 #[must_use]
149 pub const fn info(&self) -> &FileSystemInfo {
150 &self.info
151 }
152
153 /// Returns the effective application-visible capabilities.
154 ///
155 /// The snapshot contains provider-declared capabilities plus capabilities
156 /// derived by the facade, such as conditional streamed copy support.
157 ///
158 /// # Returns
159 /// Capabilities available to callers of the facade.
160 #[inline]
161 #[must_use]
162 pub const fn capabilities(&self) -> FileSystemCapabilities {
163 self.capabilities
164 }
165
166 /// Returns the stable filesystem limits.
167 ///
168 /// # Returns
169 /// The immutable provider limit snapshot.
170 #[inline]
171 #[must_use]
172 pub const fn limits(&self) -> &FileSystemLimits {
173 &self.limits
174 }
175
176 /// Returns the immutable accepted path constraints.
177 ///
178 /// # Returns
179 /// The accepted logical path forms.
180 #[inline]
181 #[must_use]
182 pub const fn path_constraints(&self) -> &PathConstraints {
183 &self.path_constraints
184 }
185
186 /// Returns the provider-declared symbolic-link traversal policy.
187 #[inline]
188 #[must_use = "the filesystem symbolic-link policy must be used"]
189 pub const fn symlink_policy(&self) -> SymlinkPolicy {
190 self.symlink_policy
191 }
192
193 /// Validates a logical path against the provider's semantics, form, and
194 /// byte limits without performing I/O.
195 ///
196 /// # Errors
197 /// Returns an enriched invalid-path or resource-limit error when the path
198 /// does not satisfy this filesystem's declared contract.
199 pub fn validate_path(&self, path: &Path, operation: FsOperation) -> FsResult<()> {
200 let result = if path.semantics() != self.info.path_semantics() {
201 Err(FsError::invalid_path(
202 operation,
203 "path semantics do not match this filesystem",
204 ))
205 } else {
206 self.path_constraints
207 .validate(path)
208 .and_then(|()| self.limits.validate_path(path, self.info.path_semantics(), operation))
209 };
210 result.map_err(|error| {
211 error
212 .with_operation(operation)
213 .with_missing_context(path, None, self.info.provider_id())
214 })
215 }
216
217 /// Defensively validates a provider-supplied snapshot at the facade
218 /// boundary.
219 ///
220 /// It performs no I/O and is intentionally crate-private.
221 ///
222 /// # Returns
223 /// `Ok(())` when all property invariants hold.
224 ///
225 /// # Errors
226 /// Returns an invalid-options error when the snapshot violates core value
227 /// invariants.
228 pub(crate) fn validate(&self) -> FsResult<()> {
229 if self.info.provider_id().is_empty() || self.info.provider_id().chars().any(char::is_control) {
230 return Err(invalid_properties(
231 "provider id must be non-empty and contain no controls",
232 ));
233 }
234 if let Some((_capability, _dependency)) = self.capabilities.missing_dependency() {
235 return Err(invalid_properties("advertised capability dependency is missing"));
236 }
237 if [
238 self.limits.max_path_text_bytes(),
239 self.limits.max_component_text_bytes(),
240 self.limits.max_read_range_bytes(),
241 self.limits.max_write_bytes(),
242 self.limits.max_list_page_entries(),
243 ]
244 .into_iter()
245 .any(|limit| matches!(limit, crate::metadata::FileSystemLimit::Maximum(0)))
246 {
247 return Err(invalid_properties(
248 "finite filesystem limits must have a positive value",
249 ));
250 }
251 if self.info.path_semantics() != PathSemantics::Hierarchical
252 && self.path_constraints.form() == PathForm::Absolute
253 {
254 return Err(invalid_properties(
255 "literal path semantics cannot require hierarchical absolute paths",
256 ));
257 }
258 Ok(())
259 }
260}
261
262/// Builds the shared property-validation failure.
263///
264/// # Parameters
265/// - `message`: Static explanation of the violated property invariant.
266///
267/// # Returns
268/// An invalid-options error scoped to provider configuration.
269fn invalid_properties(message: &'static str) -> FsError {
270 FsError::new(FsErrorKind::InvalidOptions, FsOperation::ValidateProperties, message)
271}