Skip to main content

tzap_core/
format.rs

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