Skip to main content

acyclic_fs/kernel/
export.rs

1//! Canonical bounded generation-transfer manifests.
2
3use super::codec::{CanonicalDecodeError, Decoder, Encoder};
4use super::volume::{decode_config, encode_config};
5use crate::foundation::{Digest, GenerationId, VolumeId};
6use crate::model::{VolumeConfig, VolumeConfigError};
7use crate::storage::{ObjectId, ObjectKind};
8use std::cmp::Ordering;
9use thiserror::Error;
10
11const DOMAIN: &[u8] = b"acyclic-fs-generation-export-v1\0";
12const VERSION: u16 = 1;
13
14/// Deterministic complete object manifest for one immutable generation.
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct GenerationExportManifest {
17    /// Owning volume identity.
18    pub volume_id: VolumeId,
19    /// Immutable per-volume semantics required for restoration.
20    pub config: VolumeConfig,
21    /// Typed immutable generation-root object.
22    pub generation_root: ObjectId,
23    /// Content-addressed generation identity.
24    pub generation_id: GenerationId,
25    /// Stable sorted complete closure, including `generation_root`.
26    pub objects: Vec<ObjectId>,
27    /// Number of reachable path-independent file records.
28    pub file_count: u64,
29}
30
31/// Fail-closed transfer-manifest validation and codec failures.
32#[derive(Clone, Debug, Eq, Error, PartialEq)]
33pub enum GenerationExportManifestError {
34    /// Volume semantics are invalid or unbounded.
35    #[error(transparent)]
36    Config(#[from] VolumeConfigError),
37    /// Canonical bytes are malformed, excessive, or unsupported.
38    #[error(transparent)]
39    Decode(#[from] CanonicalDecodeError),
40    /// The selected root is not a generation-root object.
41    #[error("export manifest root is not a generation root")]
42    WrongRootKind,
43    /// Generation identity must equal the generation-root digest.
44    #[error("export manifest generation identity does not match its root")]
45    GenerationMismatch,
46    /// Closure must contain the selected generation root.
47    #[error("export manifest closure omits its generation root")]
48    MissingRoot,
49    /// Object identities must be in strict canonical order without duplicates.
50    #[error("export manifest objects are not strictly canonically ordered")]
51    NonCanonicalObjectOrder,
52    /// Closure exceeds either the volume or caller item bound.
53    #[error("export manifest object count exceeds its bound")]
54    TooManyObjects,
55    /// File count exceeds the immutable volume bound.
56    #[error("export manifest file count exceeds its bound")]
57    TooManyFiles,
58}
59
60/// Encodes one complete manifest using a serializer-independent binary format.
61///
62/// # Errors
63///
64/// Rejects invalid volume semantics, mismatched identities, absent roots,
65/// excessive counts, duplicate objects, and non-canonical object ordering.
66pub fn encode_generation_export_manifest(
67    manifest: &GenerationExportManifest,
68) -> Result<Vec<u8>, GenerationExportManifestError> {
69    validate_generation_export_manifest(
70        manifest,
71        manifest.config.limits.maximum_objects_per_generation,
72    )?;
73    let count = u32::try_from(manifest.objects.len())
74        .map_err(|_| GenerationExportManifestError::TooManyObjects)?;
75    let mut encoder = Encoder::new(DOMAIN, VERSION);
76    encoder.fixed(&manifest.volume_id.into_bytes());
77    encode_config(&mut encoder, manifest.config);
78    encode_object_id(&mut encoder, manifest.generation_root);
79    encoder.fixed(manifest.generation_id.digest().as_bytes());
80    encoder.u64(manifest.file_count);
81    encoder.u32(count);
82    for object in &manifest.objects {
83        encode_object_id(&mut encoder, *object);
84    }
85    Ok(encoder.finish())
86}
87
88/// Decodes one bounded canonical complete manifest.
89///
90/// # Errors
91///
92/// Rejects excessive bytes/items before allocation, malformed fields, invalid
93/// configuration, identity mismatch, missing root, duplicates, and disorder.
94pub fn decode_generation_export_manifest(
95    bytes: &[u8],
96    maximum_bytes: u64,
97    maximum_objects: u64,
98) -> Result<GenerationExportManifest, GenerationExportManifestError> {
99    let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, maximum_bytes)?;
100    let volume_id = VolumeId::from_bytes(decoder.fixed()?);
101    let config = decode_config(&mut decoder)?.validate()?;
102    let generation_root = decode_object_id(&mut decoder)?;
103    let generation_id = GenerationId::new(Digest::from_bytes(decoder.fixed()?));
104    let file_count = decoder.u64()?;
105    let count = decoder.u32()?;
106    let admitted = maximum_objects.min(config.limits.maximum_objects_per_generation);
107    if u64::from(count) > admitted {
108        return Err(GenerationExportManifestError::TooManyObjects);
109    }
110    let capacity =
111        usize::try_from(count).map_err(|_| GenerationExportManifestError::TooManyObjects)?;
112    let mut objects = Vec::new();
113    objects
114        .try_reserve_exact(capacity)
115        .map_err(|_| CanonicalDecodeError::AllocationFailed)?;
116    for _ in 0..count {
117        objects.push(decode_object_id(&mut decoder)?);
118    }
119    decoder.finish()?;
120    let manifest = GenerationExportManifest {
121        volume_id,
122        config,
123        generation_root,
124        generation_id,
125        objects,
126        file_count,
127    };
128    validate_generation_export_manifest(&manifest, maximum_objects)?;
129    Ok(manifest)
130}
131
132/// Validates one decoded or caller-constructed manifest without backend work.
133///
134/// # Errors
135///
136/// Rejects invalid semantics, identities, bounds, closure ordering, or a
137/// missing generation root.
138pub fn validate_generation_export_manifest(
139    manifest: &GenerationExportManifest,
140    maximum_objects: u64,
141) -> Result<(), GenerationExportManifestError> {
142    manifest.config.validate()?;
143    if manifest.generation_root.kind != ObjectKind::GenerationRoot {
144        return Err(GenerationExportManifestError::WrongRootKind);
145    }
146    if manifest.generation_id.digest() != manifest.generation_root.digest {
147        return Err(GenerationExportManifestError::GenerationMismatch);
148    }
149    if manifest.file_count > manifest.config.limits.maximum_files_per_generation {
150        return Err(GenerationExportManifestError::TooManyFiles);
151    }
152    let count = u64::try_from(manifest.objects.len()).unwrap_or(u64::MAX);
153    if count > maximum_objects || count > manifest.config.limits.maximum_objects_per_generation {
154        return Err(GenerationExportManifestError::TooManyObjects);
155    }
156    if !manifest.objects.contains(&manifest.generation_root) {
157        return Err(GenerationExportManifestError::MissingRoot);
158    }
159    if manifest.objects.windows(2).any(|pair| {
160        let [left, right] = pair else {
161            unreachable!("windows(2) always yields exactly two elements")
162        };
163        compare_object_id(*left, *right) != Ordering::Less
164    }) {
165        return Err(GenerationExportManifestError::NonCanonicalObjectOrder);
166    }
167    Ok(())
168}
169
170fn compare_object_id(left: ObjectId, right: ObjectId) -> Ordering {
171    left.kind
172        .canonical_tag()
173        .cmp(&right.kind.canonical_tag())
174        .then_with(|| left.digest.cmp(&right.digest))
175}
176
177fn encode_object_id(encoder: &mut Encoder, object: ObjectId) {
178    encoder.u8(object.kind.canonical_tag());
179    encoder.fixed(object.digest.as_bytes());
180}
181
182fn decode_object_id(decoder: &mut Decoder<'_>) -> Result<ObjectId, CanonicalDecodeError> {
183    let kind = ObjectKind::from_canonical_tag(decoder.u8()?)
184        .map_err(|error| CanonicalDecodeError::Invariant(error.to_string()))?;
185    Ok(ObjectId {
186        kind,
187        digest: Digest::from_bytes(decoder.fixed()?),
188    })
189}
190
191#[cfg(test)]
192#[path = "tests/export.rs"]
193mod tests;