Skip to main content

tzap_core/
format.rs

1use thiserror::Error;
2
3pub const FORMAT_VERSION: u16 = 1;
4pub const VOLUME_FORMAT_REV: u16 = 36;
5
6pub const VOLUME_HEADER_LEN: usize = 128;
7pub const CRYPTO_HEADER_FIXED_LEN: usize = 76;
8pub const MANIFEST_FOOTER_LEN: usize = 136;
9pub const VOLUME_TRAILER_LEN: usize = 128;
10pub const BOOTSTRAP_SIDECAR_HEADER_LEN: usize = 128;
11pub const BLOCK_RECORD_FRAMING_LEN: usize = 20;
12pub const CRYPTO_HEADER_HMAC_LEN: usize = 32;
13pub const CRYPTO_EXTENSION_HEADER_LEN: usize = 6;
14pub const CRYPTO_EXTENSION_MAX_VALUE_LEN: u32 = 256;
15pub const MASTER_KEY_LEN: usize = 32;
16pub const SUBKEY_LEN: usize = 32;
17pub const READER_MAX_ARGON2ID_M_COST_KIB: u32 = 4 * 1024 * 1024;
18pub const READER_MAX_ARGON2ID_T_COST: u32 = 100;
19pub const READER_MAX_ARGON2ID_PARALLELISM: u32 = 64;
20pub const READER_MAX_CRYPTO_HEADER_LEN: u32 = 64 * 1024;
21pub const READER_MAX_CHUNK_SIZE: u32 = 64 * 1024 * 1024;
22pub const READER_MAX_ENVELOPE_TARGET_SIZE: u32 = 64 * 1024 * 1024;
23pub const READER_MAX_BLOCK_SIZE: u32 = 1024 * 1024;
24pub const READER_MAX_STRIPE_WIDTH: u32 = 4096;
25pub const READER_MAX_FEC_CLASS_SHARDS: u32 = 4096;
26pub const READER_MAX_INDEX_FEC_CLASS_SHARDS: u32 = 4096;
27pub const READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS: u32 = 131_070;
28pub const READER_MAX_PATH_LENGTH: u32 = 4096;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[repr(u16)]
32pub enum CompressionAlgo {
33    None = 0,
34    ZstdFramed = 1,
35}
36
37impl TryFrom<u16> for CompressionAlgo {
38    type Error = FormatError;
39
40    fn try_from(value: u16) -> Result<Self, Self::Error> {
41        match value {
42            0 => Ok(Self::None),
43            1 => Ok(Self::ZstdFramed),
44            other => Err(FormatError::UnknownCompressionAlgo(other)),
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[repr(u16)]
51pub enum AeadAlgo {
52    AesGcmSiv256 = 1,
53    XChaCha20Poly1305 = 2,
54    AesGcm256 = 3,
55}
56
57impl TryFrom<u16> for AeadAlgo {
58    type Error = FormatError;
59
60    fn try_from(value: u16) -> Result<Self, Self::Error> {
61        match value {
62            1 => Ok(Self::AesGcmSiv256),
63            2 => Ok(Self::XChaCha20Poly1305),
64            3 => Ok(Self::AesGcm256),
65            other => Err(FormatError::UnknownAeadAlgo(other)),
66        }
67    }
68}
69
70impl AeadAlgo {
71    pub const fn nonce_len(self) -> usize {
72        match self {
73            Self::AesGcmSiv256 | Self::AesGcm256 => 12,
74            Self::XChaCha20Poly1305 => 24,
75        }
76    }
77
78    pub const fn tag_len(self) -> usize {
79        16
80    }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[repr(u16)]
85pub enum FecAlgo {
86    None = 0,
87    ReedSolomonGF16 = 1,
88    Wirehair = 2,
89}
90
91impl TryFrom<u16> for FecAlgo {
92    type Error = FormatError;
93
94    fn try_from(value: u16) -> Result<Self, Self::Error> {
95        match value {
96            0 => Ok(Self::None),
97            1 => Ok(Self::ReedSolomonGF16),
98            2 => Ok(Self::Wirehair),
99            other => Err(FormatError::UnknownFecAlgo(other)),
100        }
101    }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[repr(u16)]
106pub enum KdfAlgo {
107    Raw = 0,
108    Argon2id = 1,
109}
110
111impl TryFrom<u16> for KdfAlgo {
112    type Error = FormatError;
113
114    fn try_from(value: u16) -> Result<Self, Self::Error> {
115        match value {
116            0 => Ok(Self::Raw),
117            1 => Ok(Self::Argon2id),
118            other => Err(FormatError::UnknownKdfAlgo(other)),
119        }
120    }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124#[repr(u8)]
125pub enum BlockKind {
126    PayloadData = 0,
127    PayloadParity = 1,
128    IndexRootData = 2,
129    IndexRootParity = 3,
130    IndexShardData = 4,
131    IndexShardParity = 5,
132    DictionaryData = 6,
133    DictionaryParity = 7,
134    DirectoryHintData = 8,
135    DirectoryHintParity = 9,
136}
137
138impl TryFrom<u8> for BlockKind {
139    type Error = FormatError;
140
141    fn try_from(value: u8) -> Result<Self, Self::Error> {
142        match value {
143            0 => Ok(Self::PayloadData),
144            1 => Ok(Self::PayloadParity),
145            2 => Ok(Self::IndexRootData),
146            3 => Ok(Self::IndexRootParity),
147            4 => Ok(Self::IndexShardData),
148            5 => Ok(Self::IndexShardParity),
149            6 => Ok(Self::DictionaryData),
150            7 => Ok(Self::DictionaryParity),
151            8 => Ok(Self::DirectoryHintData),
152            9 => Ok(Self::DirectoryHintParity),
153            other => Err(FormatError::UnknownBlockKind(other)),
154        }
155    }
156}
157
158impl BlockKind {
159    pub const fn is_data(self) -> bool {
160        matches!(
161            self,
162            Self::PayloadData
163                | Self::IndexRootData
164                | Self::IndexShardData
165                | Self::DictionaryData
166                | Self::DirectoryHintData
167        )
168    }
169
170    pub const fn is_parity(self) -> bool {
171        !self.is_data()
172    }
173}
174
175#[derive(Debug, Error, PartialEq, Eq)]
176pub enum FormatError {
177    #[error("unknown compression algorithm id {0}")]
178    UnknownCompressionAlgo(u16),
179
180    #[error("unknown AEAD algorithm id {0}")]
181    UnknownAeadAlgo(u16),
182
183    #[error("unknown FEC algorithm id {0}")]
184    UnknownFecAlgo(u16),
185
186    #[error("unknown KDF algorithm id {0}")]
187    UnknownKdfAlgo(u16),
188
189    #[error("unknown block kind {0}")]
190    UnknownBlockKind(u8),
191
192    #[error("invalid length for {structure}: expected {expected}, actual {actual}")]
193    InvalidLength {
194        structure: &'static str,
195        expected: usize,
196        actual: usize,
197    },
198
199    #[error("bad magic for {structure}")]
200    BadMagic { structure: &'static str },
201
202    #[error("bad CRC32C for {structure}")]
203    BadCrc { structure: &'static str },
204
205    #[error("unsupported format version {0}")]
206    UnsupportedFormatVersion(u16),
207
208    #[error("unsupported volume format revision {0}")]
209    UnsupportedVolumeFormatRevision(u16),
210
211    #[error("non-zero reserved bytes in {structure}")]
212    NonZeroReserved { structure: &'static str },
213
214    #[error("non-canonical CryptoHeader offset {0}")]
215    NonCanonicalCryptoHeaderOffset(u32),
216
217    #[error("stripe width must be non-zero")]
218    ZeroStripeWidth,
219
220    #[error("volume index {volume_index} is outside stripe width {stripe_width}")]
221    VolumeIndexOutOfRange {
222        volume_index: u32,
223        stripe_width: u32,
224    },
225
226    #[error(
227        "CryptoHeader length mismatch: fixed header says {fixed}, volume header says {volume}"
228    )]
229    CryptoHeaderLengthMismatch { fixed: u32, volume: u32 },
230
231    #[error("compression algorithm {0:?} is not valid for v0.36")]
232    UnsupportedCompressionForV36(CompressionAlgo),
233
234    #[error("FEC algorithm {0:?} is not valid for v0.36")]
235    UnsupportedFecForV36(FecAlgo),
236
237    #[error("invalid boolean field {field}={value}")]
238    InvalidBoolean { field: &'static str, value: u8 },
239
240    #[error("volume loss tolerance {volume_loss_tolerance} must be less than stripe width {stripe_width}")]
241    VolumeLossToleranceOutOfRange {
242        volume_loss_tolerance: u8,
243        stripe_width: u32,
244    },
245
246    #[error("bit rot buffer pct {0} exceeds 100")]
247    BitRotBufferPctTooLarge(u8),
248
249    #[error("data shard maximum {field} must be non-zero")]
250    ZeroDataShardMaximum { field: &'static str },
251
252    #[error("chunk_size must be non-zero")]
253    ZeroChunkSize,
254
255    #[error("envelope_target_size must be non-zero")]
256    ZeroEnvelopeTargetSize,
257
258    #[error("chunk_size {chunk_size} exceeds envelope_target_size {envelope_target_size}")]
259    ChunkSizeExceedsEnvelopeTarget {
260        chunk_size: u32,
261        envelope_target_size: u32,
262    },
263
264    #[error("block_size {0} is below the v0.36 minimum")]
265    BlockSizeTooSmall(u32),
266
267    #[error("block_size {0} must be even")]
268    OddBlockSize(u32),
269
270    #[error("reader resource cap exceeded for {field}: cap {cap}, actual {actual}")]
271    ReaderResourceLimitExceeded {
272        field: &'static str,
273        cap: u64,
274        actual: u64,
275    },
276
277    #[error("invalid block flags 0x{0:02x}")]
278    InvalidBlockFlags(u8),
279
280    #[error("parity block must not set the last-data flag")]
281    ParityBlockHasLastDataFlag,
282
283    #[error("invalid authoritative flag {0}")]
284    InvalidAuthoritativeFlag(u8),
285
286    #[error("invalid ManifestFooter length {0}")]
287    InvalidManifestFooterLength(u32),
288
289    #[error("IndexRoot encrypted size is not data_block_count * block_size")]
290    IndexRootSizeMismatch,
291
292    #[error("IndexRoot data block count and encrypted size must be non-zero")]
293    EmptyIndexRootExtent,
294
295    #[error("bootstrap sidecar version {0} is unsupported")]
296    UnsupportedBootstrapSidecarVersion(u32),
297
298    #[error("bootstrap sidecar has unknown flags 0x{0:08x}")]
299    UnknownBootstrapSidecarFlags(u32),
300
301    #[error("bootstrap sidecar present section has zero offset or length")]
302    EmptyBootstrapSidecarSection,
303
304    #[error("bootstrap sidecar absent section has non-zero offset or length")]
305    NonZeroAbsentBootstrapSidecarSection,
306
307    #[error("bootstrap sidecar sections are not packed canonically")]
308    NonCanonicalBootstrapSidecarLayout,
309
310    #[error("extension TLV header is truncated")]
311    TruncatedExtensionHeader,
312
313    #[error("extension TLV payload is truncated")]
314    TruncatedExtensionPayload,
315
316    #[error("extension TLV payload length {0} exceeds 256")]
317    ExtensionPayloadTooLarge(u32),
318
319    #[error("extension terminator is malformed")]
320    MalformedExtensionTerminator,
321
322    #[error("extension terminator is missing")]
323    MissingExtensionTerminator,
324
325    #[error("bytes follow extension terminator")]
326    BytesAfterExtensionTerminator,
327
328    #[error("CryptoHeader is too short: minimum {min}, actual {actual}")]
329    CryptoHeaderTooShort { min: usize, actual: usize },
330
331    #[error("KdfParams algo_tag {actual} does not match expected {expected}")]
332    KdfAlgoTagMismatch { expected: u16, actual: u16 },
333
334    #[error("KdfParams are truncated")]
335    TruncatedKdfParams,
336
337    #[error("invalid KdfParams: {0}")]
338    InvalidKdfParams(&'static str),
339
340    #[error("key material does not match KDF mode")]
341    KeyMaterialMismatch,
342
343    #[error("raw master key must be exactly 32 bytes")]
344    InvalidRawMasterKeyLength,
345
346    #[error("Argon2id derivation failed")]
347    Argon2idFailure,
348
349    #[error("HKDF expansion failed")]
350    HkdfExpandFailure,
351
352    #[error("HMAC verification failed for {structure}")]
353    HmacMismatch { structure: &'static str },
354
355    #[error("forbidden CryptoHeader extension tag 0x{0:04x}")]
356    ForbiddenExtensionTag(u16),
357
358    #[error("unknown critical CryptoHeader extension tag 0x{0:04x}")]
359    UnknownCriticalExtension(u16),
360
361    #[error("duplicate known CryptoHeader extension tag 0x{0:04x}")]
362    DuplicateKnownExtension(u16),
363
364    #[error("malformed known CryptoHeader extension tag 0x{0:04x}")]
365    MalformedKnownExtension(u16),
366
367    #[error("padding input is empty")]
368    EmptyPaddedPlaintext,
369
370    #[error("invalid suffix padding")]
371    InvalidSuffixPadding,
372
373    #[error("non-zero suffix padding bytes")]
374    NonZeroPaddingBytes,
375
376    #[error("padding arithmetic overflow")]
377    PaddingOverflow,
378
379    #[error("AEAD operation failed")]
380    AeadFailure,
381
382    #[error("nonce/AAD domain is too long")]
383    DomainTooLong,
384
385    #[error("invalid nonce length for {algo:?}: expected {expected}, actual {actual}")]
386    InvalidNonceLength {
387        algo: AeadAlgo,
388        expected: usize,
389        actual: usize,
390    },
391
392    #[error("invalid AEAD key length")]
393    InvalidAeadKeyLength,
394
395    #[error("zstd compression failed")]
396    ZstdCompressionFailure,
397
398    #[error("zstd frame is empty")]
399    EmptyZstdFrame,
400
401    #[error("zstd frame is not a standard non-skippable frame")]
402    NotStandardZstdFrame,
403
404    #[error("zstd frame is truncated or corrupt")]
405    InvalidZstdFrame,
406
407    #[error("zstd frame has trailing bytes after the first complete frame")]
408    TrailingBytesAfterZstdFrame,
409
410    #[error("zstd decompression failed")]
411    ZstdDecompressionFailure,
412
413    #[error("zstd decompressed size mismatch: expected {expected}, actual {actual}")]
414    ZstdDecompressedSizeMismatch { expected: usize, actual: usize },
415
416    #[error("FEC object must contain at least one data shard")]
417    FecZeroDataShards,
418
419    #[error("FEC object total shard count {0} exceeds ReedSolomonGF16 limit")]
420    FecTooManyShards(usize),
421
422    #[error("FEC shard size must be even")]
423    FecOddShardSize,
424
425    #[error("FEC shards have inconsistent sizes")]
426    FecInconsistentShardSize,
427
428    #[error("FEC repair has too few available shards")]
429    FecTooFewAvailableShards,
430
431    #[error("FEC repair matrix is singular")]
432    FecSingularMatrix,
433
434    #[error("invalid metadata in {structure}: {reason}")]
435    InvalidMetadata {
436        structure: &'static str,
437        reason: &'static str,
438    },
439
440    #[error("metadata arithmetic overflow in {structure}")]
441    MetadataArithmeticOverflow { structure: &'static str },
442
443    #[error("hash-prefix collision run exceeds resource caps")]
444    HashPrefixCollisionRunExceeded,
445
446    #[error("unsafe archive path")]
447    UnsafeArchivePath,
448
449    #[error("unsafe extraction overwrite")]
450    UnsafeOverwrite,
451
452    #[error("filesystem extraction failed: {0}")]
453    FilesystemExtractionFailed(&'static str),
454
455    #[error("writer unsupported case: {0}")]
456    WriterUnsupported(&'static str),
457
458    #[error("writer invariant failed: {0}")]
459    WriterInvariant(&'static str),
460
461    #[error("reader unsupported case: {0}")]
462    ReaderUnsupported(&'static str),
463
464    #[error("invalid archive: {0}")]
465    InvalidArchive(&'static str),
466}