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