Skip to main content

tzap_core/
format.rs

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