Skip to main content

acyclic_fs/kernel/
volume.rs

1//! Canonical volume-creation facts and deterministic authority ownership.
2
3use super::codec::{CanonicalDecodeError, Decoder, Encoder};
4use crate::foundation::{AuthorityId, Digest, VolumeId};
5use crate::model::{
6    CaseSensitivity, ConcurrencyMode, FilesystemProfile, Lifecycle, UnicodePolicy, VolumeConfig,
7    VolumeConfigError, VolumeLimits,
8};
9use crate::storage::{ObjectId, ObjectKind};
10use thiserror::Error;
11
12const DOMAIN: &[u8] = b"acyclic-fs-volume-created-v1\0";
13const VERSION: u16 = 1;
14
15/// Immutable creation fact at sequence one of a volume authority.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct VolumeCreated {
18    /// Stable independently versioned volume identity.
19    pub volume_id: VolumeId,
20    /// Complete immutable semantics selected for the volume.
21    pub config: VolumeConfig,
22    /// Initial authenticated generation root.
23    pub initial_generation_root: ObjectId,
24}
25
26/// Canonical volume-creation codec failures.
27#[derive(Debug, Error)]
28pub enum VolumeCreatedError {
29    /// The volume configuration is contradictory or unbounded.
30    #[error(transparent)]
31    Config(#[from] VolumeConfigError),
32    /// The canonical payload is malformed or unsupported.
33    #[error(transparent)]
34    Decode(#[from] CanonicalDecodeError),
35}
36
37/// Returns the sole authority identity for a volume.
38///
39/// This bijection eliminates a mutable volume catalog: resolving a volume does
40/// not need a third durable lookup besides authority and immutable objects.
41#[must_use]
42pub const fn volume_authority_id(volume_id: VolumeId) -> AuthorityId {
43    AuthorityId::from_bytes(volume_id.into_bytes())
44}
45
46/// Encodes one canonical immutable volume-creation authority fact.
47///
48/// # Errors
49///
50/// Rejects invalid configuration or a non-generation initial root.
51pub fn encode_volume_created(value: VolumeCreated) -> Result<Vec<u8>, VolumeCreatedError> {
52    value.config.validate()?;
53    if value.initial_generation_root.kind != ObjectKind::GenerationRoot {
54        return Err(invariant("initial volume object is not a generation root").into());
55    }
56    let mut encoder = Encoder::new(DOMAIN, VERSION);
57    encoder.fixed(&value.volume_id.into_bytes());
58    encode_config(&mut encoder, value.config);
59    encoder.u8(value.initial_generation_root.kind.canonical_tag());
60    encoder.fixed(value.initial_generation_root.digest.as_bytes());
61    Ok(encoder.finish())
62}
63
64/// Decodes one bounded canonical immutable volume-creation authority fact.
65///
66/// # Errors
67///
68/// Fails closed for malformed, trailing, unknown, contradictory, oversized, or
69/// non-generation-root data.
70pub fn decode_volume_created(
71    bytes: &[u8],
72    maximum_payload_bytes: u64,
73) -> Result<VolumeCreated, VolumeCreatedError> {
74    let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, maximum_payload_bytes)?;
75    let volume_id = VolumeId::from_bytes(decoder.fixed()?);
76    let config = decode_config(&mut decoder)?.validate()?;
77    let kind = ObjectKind::from_canonical_tag(decoder.u8()?)
78        .map_err(|_| invariant("unknown initial-generation object kind"))?;
79    if kind != ObjectKind::GenerationRoot {
80        return Err(invariant("initial volume object is not a generation root").into());
81    }
82    let initial_generation_root = ObjectId {
83        kind,
84        digest: Digest::from_bytes(decoder.fixed()?),
85    };
86    decoder.finish()?;
87    Ok(VolumeCreated {
88        volume_id,
89        config,
90        initial_generation_root,
91    })
92}
93
94pub(crate) fn encode_config(encoder: &mut Encoder, config: VolumeConfig) {
95    encoder.u8(match config.profile {
96        FilesystemProfile::Portable => 1,
97        FilesystemProfile::Posix => 2,
98        FilesystemProfile::Windows => 3,
99        FilesystemProfile::Browser => 4,
100    });
101    encoder.u8(match config.concurrency {
102        ConcurrencyMode::ExclusiveWriter => 1,
103        ConcurrencyMode::Optimistic => 2,
104        ConcurrencyMode::SerializedAuthority => 3,
105    });
106    encoder.u8(match config.lifecycle {
107        Lifecycle::Ephemeral => 1,
108        Lifecycle::Durable => 2,
109    });
110    encoder.u8(match config.case_sensitivity {
111        CaseSensitivity::Sensitive => 1,
112        CaseSensitivity::ProfileFolded => 2,
113    });
114    encoder.u8(match config.unicode {
115        UnicodePolicy::Preserve => 1,
116        UnicodePolicy::RequireNfc => 2,
117    });
118    encoder.u8(u8::from(config.symbolic_links));
119    encoder.u8(u8::from(config.hard_links));
120    encoder.u8(u8::from(config.sparse_files));
121    encoder.u32(config.limits.maximum_path_bytes);
122    encoder.u32(config.limits.maximum_component_bytes);
123    encoder.u32(u32::from(config.limits.maximum_path_depth));
124    encoder.u64(config.limits.maximum_object_bytes);
125    encoder.u32(config.limits.maximum_mutations_per_batch);
126    encoder.u32(config.limits.maximum_paths_per_batch);
127    encoder.u32(config.limits.maximum_checkout_dependencies);
128    encoder.u32(config.limits.maximum_directory_page_entries);
129    encoder.u32(u32::from(config.limits.maximum_page_height));
130    encoder.u64(config.limits.maximum_read_bytes);
131    encoder.u64(config.limits.maximum_files_per_generation);
132    encoder.u64(config.limits.maximum_objects_per_generation);
133    encoder.u64(config.limits.maximum_generation_bytes);
134}
135
136pub(crate) fn decode_config(
137    decoder: &mut Decoder<'_>,
138) -> Result<VolumeConfig, CanonicalDecodeError> {
139    let profile = match decoder.u8()? {
140        1 => FilesystemProfile::Portable,
141        2 => FilesystemProfile::Posix,
142        3 => FilesystemProfile::Windows,
143        4 => FilesystemProfile::Browser,
144        _ => return Err(invariant("unknown filesystem profile")),
145    };
146    let concurrency = match decoder.u8()? {
147        1 => ConcurrencyMode::ExclusiveWriter,
148        2 => ConcurrencyMode::Optimistic,
149        3 => ConcurrencyMode::SerializedAuthority,
150        _ => return Err(invariant("unknown concurrency mode")),
151    };
152    let lifecycle = match decoder.u8()? {
153        1 => Lifecycle::Ephemeral,
154        2 => Lifecycle::Durable,
155        _ => return Err(invariant("unknown lifecycle")),
156    };
157    let case_sensitivity = match decoder.u8()? {
158        1 => CaseSensitivity::Sensitive,
159        2 => CaseSensitivity::ProfileFolded,
160        _ => return Err(invariant("unknown case-sensitivity mode")),
161    };
162    let unicode = match decoder.u8()? {
163        1 => UnicodePolicy::Preserve,
164        2 => UnicodePolicy::RequireNfc,
165        _ => return Err(invariant("unknown Unicode policy")),
166    };
167    let symbolic_links = decode_bool(decoder.u8()?)?;
168    let hard_links = decode_bool(decoder.u8()?)?;
169    let sparse_files = decode_bool(decoder.u8()?)?;
170    let maximum_path_bytes = decoder.u32()?;
171    let maximum_component_bytes = decoder.u32()?;
172    let maximum_path_depth =
173        u16::try_from(decoder.u32()?).map_err(|_| invariant("maximum path depth exceeds u16"))?;
174    Ok(VolumeConfig {
175        profile,
176        concurrency,
177        lifecycle,
178        case_sensitivity,
179        unicode,
180        symbolic_links,
181        hard_links,
182        sparse_files,
183        limits: VolumeLimits {
184            maximum_path_bytes,
185            maximum_component_bytes,
186            maximum_path_depth,
187            maximum_object_bytes: decoder.u64()?,
188            maximum_mutations_per_batch: decoder.u32()?,
189            maximum_paths_per_batch: decoder.u32()?,
190            maximum_checkout_dependencies: decoder.u32()?,
191            maximum_directory_page_entries: decoder.u32()?,
192            maximum_page_height: u16::try_from(decoder.u32()?)
193                .map_err(|_| invariant("maximum page height exceeds u16"))?,
194            maximum_read_bytes: decoder.u64()?,
195            maximum_files_per_generation: decoder.u64()?,
196            maximum_objects_per_generation: decoder.u64()?,
197            maximum_generation_bytes: decoder.u64()?,
198        },
199    })
200}
201
202fn decode_bool(value: u8) -> Result<bool, CanonicalDecodeError> {
203    match value {
204        0 => Ok(false),
205        1 => Ok(true),
206        _ => Err(invariant("non-canonical Boolean value")),
207    }
208}
209
210fn invariant(message: &str) -> CanonicalDecodeError {
211    CanonicalDecodeError::Invariant(message.to_owned())
212}
213
214#[cfg(test)]
215#[path = "tests/volume.rs"]
216mod tests;