Skip to main content

acyclic_fs/
model.rs

1//! Explicit, backend-independent volume and checkout semantics.
2
3use crate::foundation::GenerationId;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7/// Filesystem behavior profile requested by a volume.
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum FilesystemProfile {
11    /// Semantics shared by native and browser implementations.
12    Portable,
13    /// POSIX-oriented names, links, ownership, and mode behavior.
14    Posix,
15    /// Windows-oriented names, reparse points, streams, and security metadata.
16    Windows,
17    /// Browser-safe semantics with no host mount assumptions.
18    Browser,
19}
20
21/// Whether a mounted checkout admits mutations.
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AccessMode {
25    /// Reads only.
26    ReadOnly,
27    /// Reads and admitted mutations.
28    ReadWrite,
29}
30
31/// How a checkout observes newer published generations.
32#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ConsistencyMode {
35    /// The selected generation never advances implicitly.
36    Pinned,
37    /// Advance only when observation and mutation proofs establish safety.
38    TrackingSafe,
39    /// Receive generation changes and apply them at safe boundaries.
40    Live,
41    /// Report newer heads and require an explicit caller refresh.
42    Manual,
43}
44
45/// How concurrent writers are admitted.
46#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ConcurrencyMode {
49    /// Every writable head checkout atomically advances the authority epoch;
50    /// opening a replacement writer permanently fences all prior writers.
51    ExclusiveWriter,
52    /// Writers publish against an expected immutable generation and receive a
53    /// typed conflict instead of implicit retry or merge.
54    Optimistic,
55    /// One authority orders every admitted direct-live mutation; safe disjoint
56    /// races may rebase and retry within the caller's explicit bounds.
57    SerializedAuthority,
58}
59
60/// Persistence expectation of a volume.
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum Lifecycle {
64    /// State may disappear when its owning runtime ends.
65    Ephemeral,
66    /// Acknowledged state must survive restart under the backend contract.
67    Durable,
68}
69
70/// Where checkout mutations accumulate before publication.
71#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum MutationMode {
74    /// Mutations accumulate in a private sparse copy-on-write overlay.
75    PrivateOverlay,
76    /// Mutations are submitted directly to a serialized live authority.
77    DirectLive,
78    /// The checkout cannot mutate.
79    None,
80}
81
82/// Case comparison policy for logical names.
83#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum CaseSensitivity {
86    /// Compare canonical name bytes exactly.
87    Sensitive,
88    /// Use the selected platform profile's case-folding rules.
89    ProfileFolded,
90}
91
92/// Unicode normalization policy applied at the parsing boundary.
93#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum UnicodePolicy {
96    /// Preserve admitted scalar sequences exactly.
97    Preserve,
98    /// Require canonical NFC input and reject non-normalized names.
99    RequireNfc,
100}
101
102/// Hard limits enforced before allocation, traversal, or mutation.
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
104pub struct VolumeLimits {
105    /// Maximum encoded path bytes.
106    pub maximum_path_bytes: u32,
107    /// Maximum encoded bytes in one path component.
108    pub maximum_component_bytes: u32,
109    /// Maximum path components.
110    pub maximum_path_depth: u16,
111    /// Maximum canonical bytes in one immutable object.
112    pub maximum_object_bytes: u64,
113    /// Maximum mutations admitted in one atomic batch.
114    pub maximum_mutations_per_batch: u32,
115    /// Maximum exact paths admitted in one shared lookup/stat batch.
116    pub maximum_paths_per_batch: u32,
117    /// Maximum distinct observation and mutation regions retained by one checkout.
118    pub maximum_checkout_dependencies: u32,
119    /// Maximum directory entries returned in one page.
120    pub maximum_directory_page_entries: u32,
121    /// Maximum authenticated B+tree levels followed by one operation.
122    pub maximum_page_height: u16,
123    /// Maximum bytes returned by one range request.
124    pub maximum_read_bytes: u64,
125    /// Maximum path-independent files authenticated in one generation.
126    pub maximum_files_per_generation: u64,
127    /// Maximum distinct immutable objects authenticated in one generation.
128    pub maximum_objects_per_generation: u64,
129    /// Maximum cumulative canonical object bytes in one generation closure.
130    pub maximum_generation_bytes: u64,
131}
132
133impl Default for VolumeLimits {
134    fn default() -> Self {
135        Self {
136            maximum_path_bytes: 32 * 1024,
137            maximum_component_bytes: 255,
138            maximum_path_depth: 1_024,
139            maximum_object_bytes: 64 * 1024 * 1024,
140            maximum_mutations_per_batch: 2_048,
141            maximum_paths_per_batch: 65_536,
142            maximum_checkout_dependencies: 262_144,
143            maximum_directory_page_entries: 1_024,
144            maximum_page_height: 64,
145            maximum_read_bytes: 16 * 1024 * 1024,
146            maximum_files_per_generation: 16 * 1024 * 1024,
147            maximum_objects_per_generation: 64 * 1024 * 1024,
148            maximum_generation_bytes: 1024 * 1024 * 1024 * 1024,
149        }
150    }
151}
152
153/// Complete immutable volume configuration.
154#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
155pub struct VolumeConfig {
156    /// Requested filesystem semantics.
157    pub profile: FilesystemProfile,
158    /// Writer coordination contract.
159    pub concurrency: ConcurrencyMode,
160    /// Persistence expectation.
161    pub lifecycle: Lifecycle,
162    /// Logical name comparison policy.
163    pub case_sensitivity: CaseSensitivity,
164    /// Unicode admission policy.
165    pub unicode: UnicodePolicy,
166    /// Whether symbolic links are representable.
167    pub symbolic_links: bool,
168    /// Whether multiple names may share one stable file record.
169    pub hard_links: bool,
170    /// Whether holes and allocated-zero extents remain distinct.
171    pub sparse_files: bool,
172    /// Mandatory resource and work bounds.
173    pub limits: VolumeLimits,
174}
175
176impl VolumeConfig {
177    /// Constructs the fully portable profile with explicit persistence.
178    #[must_use]
179    pub fn portable(lifecycle: Lifecycle) -> Self {
180        Self {
181            profile: FilesystemProfile::Portable,
182            concurrency: ConcurrencyMode::Optimistic,
183            lifecycle,
184            case_sensitivity: CaseSensitivity::Sensitive,
185            unicode: UnicodePolicy::Preserve,
186            symbolic_links: true,
187            hard_links: true,
188            sparse_files: true,
189            limits: VolumeLimits::default(),
190        }
191    }
192
193    /// Constructs the host-native filesystem profile for a mounted checkout.
194    #[must_use]
195    #[cfg(not(target_arch = "wasm32"))]
196    pub fn native(lifecycle: Lifecycle) -> Self {
197        let mut limits = VolumeLimits::default();
198        if cfg!(windows) {
199            // Windows names are encoded as UTF-16LE in the SDK; NTFS admits
200            // 255 code units per component, not 255 encoded bytes.
201            limits.maximum_component_bytes = 255 * 2;
202        }
203        Self {
204            profile: if cfg!(windows) {
205                FilesystemProfile::Windows
206            } else {
207                FilesystemProfile::Posix
208            },
209            limits,
210            ..Self::portable(lifecycle)
211        }
212    }
213
214    /// Validates all limits and cross-field semantic requirements.
215    ///
216    /// # Errors
217    ///
218    /// Returns a stable configuration error before any backend is opened.
219    pub fn validate(self) -> Result<Self, VolumeConfigError> {
220        let limits = self.limits;
221        if limits.maximum_path_bytes == 0
222            || limits.maximum_component_bytes == 0
223            || limits.maximum_path_depth == 0
224            || limits.maximum_object_bytes == 0
225            || limits.maximum_mutations_per_batch == 0
226            || limits.maximum_paths_per_batch == 0
227            || limits.maximum_checkout_dependencies == 0
228            || limits.maximum_directory_page_entries == 0
229            || limits.maximum_page_height == 0
230            || limits.maximum_read_bytes == 0
231            || limits.maximum_files_per_generation == 0
232            || limits.maximum_objects_per_generation == 0
233            || limits.maximum_generation_bytes == 0
234        {
235            return Err(VolumeConfigError::ZeroLimit);
236        }
237        if limits.maximum_component_bytes > limits.maximum_path_bytes {
238            return Err(VolumeConfigError::ComponentExceedsPath);
239        }
240        if limits.maximum_directory_page_entries < 2 {
241            return Err(VolumeConfigError::InsufficientPageFanout);
242        }
243        Ok(self)
244    }
245}
246
247/// Generation selected when a checkout opens.
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum GenerationSelector {
250    /// Resolve the volume head once at checkout admission.
251    Head,
252    /// Open one exact immutable generation.
253    Exact(GenerationId),
254}
255
256/// Complete checkout behavior chosen by a consumer.
257#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258pub struct CheckoutMode {
259    /// Whether mutations are admitted.
260    pub access: AccessMode,
261    /// How newer generations are observed.
262    pub consistency: ConsistencyMode,
263    /// Where admitted mutations accumulate.
264    pub mutations: MutationMode,
265}
266
267impl CheckoutMode {
268    /// Immutable exact-generation reader.
269    #[must_use]
270    pub const fn read_only_pinned() -> Self {
271        Self {
272            access: AccessMode::ReadOnly,
273            consistency: ConsistencyMode::Pinned,
274            mutations: MutationMode::None,
275        }
276    }
277
278    /// Optimistic writable sparse overlay with observation-safe tracking.
279    #[must_use]
280    pub const fn tracking_transaction() -> Self {
281        Self {
282            access: AccessMode::ReadWrite,
283            consistency: ConsistencyMode::TrackingSafe,
284            mutations: MutationMode::PrivateOverlay,
285        }
286    }
287
288    /// Validates cross-field checkout behavior.
289    ///
290    /// # Errors
291    ///
292    /// Rejects writable/read-only contradictions and direct-live use without
293    /// live consistency.
294    pub const fn validate(self) -> Result<Self, CheckoutModeError> {
295        match (self.access, self.mutations, self.consistency) {
296            (AccessMode::ReadOnly, MutationMode::None, _)
297            | (AccessMode::ReadWrite, MutationMode::DirectLive, ConsistencyMode::Live)
298            | (AccessMode::ReadWrite, MutationMode::PrivateOverlay, _) => Ok(self),
299            (AccessMode::ReadOnly, _, _) => Err(CheckoutModeError::ReadOnlyMutation),
300            (AccessMode::ReadWrite, MutationMode::None, _) => {
301                Err(CheckoutModeError::WritableWithoutMutationMode)
302            }
303            (AccessMode::ReadWrite, MutationMode::DirectLive, _) => {
304                Err(CheckoutModeError::DirectRequiresLive)
305            }
306        }
307    }
308}
309
310/// Volume configuration failures.
311#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
312pub enum VolumeConfigError {
313    /// Every work or allocation limit must be positive.
314    #[error("volume limits must be non-zero")]
315    ZeroLimit,
316    /// One component cannot be larger than the complete admitted path.
317    #[error("maximum component bytes exceeds maximum path bytes")]
318    ComponentExceedsPath,
319    /// Persistent trees require at least two entries per internal page to make progress.
320    #[error("maximum directory page entries must be at least two")]
321    InsufficientPageFanout,
322}
323
324/// Checkout configuration failures.
325#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
326pub enum CheckoutModeError {
327    /// A read-only checkout selected a mutation mode.
328    #[error("read-only checkout cannot admit mutations")]
329    ReadOnlyMutation,
330    /// A writable checkout omitted its mutation mode.
331    #[error("writable checkout requires a mutation mode")]
332    WritableWithoutMutationMode,
333    /// Direct mutation requires a live serialized view.
334    #[error("direct mutation requires live consistency")]
335    DirectRequiresLive,
336}
337
338#[cfg(test)]
339#[path = "tests/model.rs"]
340mod tests;