Skip to main content

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}