1use crate::foundation::GenerationId;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum FilesystemProfile {
11 Portable,
13 Posix,
15 Windows,
17 Browser,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AccessMode {
25 ReadOnly,
27 ReadWrite,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ConsistencyMode {
35 Pinned,
37 TrackingSafe,
39 Live,
41 Manual,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ConcurrencyMode {
49 ExclusiveWriter,
52 Optimistic,
55 SerializedAuthority,
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum Lifecycle {
64 Ephemeral,
66 Durable,
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum MutationMode {
74 PrivateOverlay,
76 DirectLive,
78 None,
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum CaseSensitivity {
86 Sensitive,
88 ProfileFolded,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum UnicodePolicy {
96 Preserve,
98 RequireNfc,
100}
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
104pub struct VolumeLimits {
105 pub maximum_path_bytes: u32,
107 pub maximum_component_bytes: u32,
109 pub maximum_path_depth: u16,
111 pub maximum_object_bytes: u64,
113 pub maximum_mutations_per_batch: u32,
115 pub maximum_paths_per_batch: u32,
117 pub maximum_checkout_dependencies: u32,
119 pub maximum_directory_page_entries: u32,
121 pub maximum_page_height: u16,
123 pub maximum_read_bytes: u64,
125 pub maximum_files_per_generation: u64,
127 pub maximum_objects_per_generation: u64,
129 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
155pub struct VolumeConfig {
156 pub profile: FilesystemProfile,
158 pub concurrency: ConcurrencyMode,
160 pub lifecycle: Lifecycle,
162 pub case_sensitivity: CaseSensitivity,
164 pub unicode: UnicodePolicy,
166 pub symbolic_links: bool,
168 pub hard_links: bool,
170 pub sparse_files: bool,
172 pub limits: VolumeLimits,
174}
175
176impl VolumeConfig {
177 #[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 #[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 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 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum GenerationSelector {
250 Head,
252 Exact(GenerationId),
254}
255
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
258pub struct CheckoutMode {
259 pub access: AccessMode,
261 pub consistency: ConsistencyMode,
263 pub mutations: MutationMode,
265}
266
267impl CheckoutMode {
268 #[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 #[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 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#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
312pub enum VolumeConfigError {
313 #[error("volume limits must be non-zero")]
315 ZeroLimit,
316 #[error("maximum component bytes exceeds maximum path bytes")]
318 ComponentExceedsPath,
319 #[error("maximum directory page entries must be at least two")]
321 InsufficientPageFanout,
322}
323
324#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
326pub enum CheckoutModeError {
327 #[error("read-only checkout cannot admit mutations")]
329 ReadOnlyMutation,
330 #[error("writable checkout requires a mutation mode")]
332 WritableWithoutMutationMode,
333 #[error("direct mutation requires live consistency")]
335 DirectRequiresLive,
336}
337
338#[cfg(test)]
339#[path = "tests/model.rs"]
340mod tests;