Skip to main content

rapidgzip_core/
error.rs

1use crate::{DeflateIndex, Format, IndexError, IndexKind};
2use std::error::Error;
3use std::fmt::{self, Display, Formatter};
4use std::io;
5use std::sync::Arc;
6
7/// An analysis-owned collection or byte budget.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9#[non_exhaustive]
10pub enum AnalysisResource {
11    /// Parsed container streams (gzip members, or one zlib/raw stream).
12    Streams,
13    /// Parsed DEFLATE blocks.
14    Blocks,
15    /// Retained optional gzip-header bytes.
16    HeaderBytes,
17    /// Retained Huffman code lengths.
18    AlphabetCodeLengths,
19    /// Retained individual back-reference records.
20    Backreferences,
21}
22
23impl Display for AnalysisResource {
24    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
25        formatter.write_str(match self {
26            Self::Streams => "streams",
27            Self::Blocks => "DEFLATE blocks",
28            Self::HeaderBytes => "gzip header bytes",
29            Self::AlphabetCodeLengths => "Huffman code lengths",
30            Self::Backreferences => "back-reference records",
31        })
32    }
33}
34
35/// A checked counter maintained by structural analysis.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37#[non_exhaustive]
38pub enum AnalysisCounter {
39    /// Absolute compressed bit positions and sizes.
40    CompressedBits,
41    /// Absolute decompressed byte positions and sizes.
42    DecompressedBytes,
43    /// Literal, copy, block, stream, or reference counts.
44    StructuralItems,
45}
46
47impl Display for AnalysisCounter {
48    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
49        formatter.write_str(match self {
50            Self::CompressedBits => "compressed-bit",
51            Self::DecompressedBytes => "decompressed-byte",
52            Self::StructuralItems => "structural-item",
53        })
54    }
55}
56
57/// The reason structural analysis stopped without accepting the input.
58#[derive(Clone, Debug, Eq, PartialEq)]
59#[non_exhaustive]
60pub enum AnalysisErrorKind {
61    /// A caller-configurable collection or metadata limit was reached.
62    ResourceLimit {
63        /// Limited resource.
64        resource: AnalysisResource,
65        /// Configured maximum number of items or bytes.
66        limit: usize,
67    },
68    /// A bounded collection could not reserve memory.
69    AllocationFailed {
70        /// Collection whose allocation failed.
71        resource: AnalysisResource,
72        /// Additional items or bytes requested from the allocator.
73        additional: usize,
74    },
75    /// An exact `u64` structural counter overflowed.
76    CounterOverflow {
77        /// Counter that could no longer represent the input.
78        counter: AnalysisCounter,
79    },
80}
81
82impl Display for AnalysisErrorKind {
83    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::ResourceLimit { resource, limit } => {
86                write!(
87                    formatter,
88                    "{resource} exceeded the configured limit of {limit}"
89                )
90            }
91            Self::AllocationFailed {
92                resource,
93                additional,
94            } => write!(
95                formatter,
96                "could not reserve space for {additional} additional {resource}"
97            ),
98            Self::CounterOverflow { counter } => {
99                write!(formatter, "the {counter} analysis counter overflowed")
100            }
101        }
102    }
103}
104
105/// The reason a gzip container was rejected.
106#[derive(Clone, Debug, Eq, PartialEq)]
107#[non_exhaustive]
108pub enum GzipErrorKind {
109    /// The gzip identification bytes were absent.
110    BadMagic,
111    /// The member did not use the DEFLATE compression method.
112    UnsupportedCompressionMethod(u8),
113    /// One or more reserved flag bits were set.
114    ReservedFlags(u8),
115    /// The optional header checksum was incorrect.
116    HeaderChecksumMismatch {
117        /// Checksum stored in the header.
118        expected: u16,
119        /// Checksum computed over preceding header bytes.
120        actual: u16,
121    },
122    /// A zero-terminated header field reached the end of input.
123    UnterminatedHeaderField,
124    /// The member header or footer was truncated.
125    Truncated,
126    /// Bytes after a valid member were not another gzip member.
127    TrailingGarbage,
128}
129
130impl Display for GzipErrorKind {
131    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
132        match self {
133            Self::BadMagic => formatter.write_str("missing gzip magic bytes"),
134            Self::UnsupportedCompressionMethod(method) => {
135                write!(formatter, "unsupported gzip compression method {method}")
136            }
137            Self::ReservedFlags(flags) => {
138                write!(formatter, "reserved gzip flag bits are set: {flags:#04x}")
139            }
140            Self::HeaderChecksumMismatch { expected, actual } => write!(
141                formatter,
142                "gzip header checksum mismatch: expected {expected:#06x}, got {actual:#06x}"
143            ),
144            Self::UnterminatedHeaderField => formatter.write_str("unterminated gzip header field"),
145            Self::Truncated => formatter.write_str("truncated gzip header or footer"),
146            Self::TrailingGarbage => formatter.write_str("trailing non-gzip data"),
147        }
148    }
149}
150
151/// The reason an RFC 1950 zlib container was rejected.
152#[derive(Clone, Debug, Eq, PartialEq)]
153#[non_exhaustive]
154pub enum ZlibErrorKind {
155    /// The header selected a compression method other than DEFLATE.
156    UnsupportedCompressionMethod(u8),
157    /// CINFO requested a window larger than DEFLATE's 32 KiB maximum.
158    UnsupportedWindowSize(u8),
159    /// The CMF/FLG pair did not satisfy the FCHECK residue.
160    BadHeaderCheck,
161    /// Preset dictionaries are not supported by this decoder.
162    PresetDictionary,
163    /// The zlib header or Adler-32 trailer was truncated.
164    Truncated,
165    /// The Adler-32 stored in the trailer did not match the output.
166    ChecksumMismatch {
167        /// Trailer value.
168        expected: u32,
169        /// Computed value.
170        actual: u32,
171    },
172    /// Bytes remained after the one complete zlib stream.
173    TrailingGarbage,
174}
175
176impl Display for ZlibErrorKind {
177    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
178        match self {
179            Self::UnsupportedCompressionMethod(method) => {
180                write!(formatter, "unsupported zlib compression method {method}")
181            }
182            Self::UnsupportedWindowSize(cinfo) => {
183                write!(formatter, "unsupported zlib CINFO window value {cinfo}")
184            }
185            Self::BadHeaderCheck => formatter.write_str("invalid zlib FCHECK header residue"),
186            Self::PresetDictionary => {
187                formatter.write_str("zlib preset dictionaries are not supported")
188            }
189            Self::Truncated => formatter.write_str("truncated zlib header or trailer"),
190            Self::ChecksumMismatch { expected, actual } => write!(
191                formatter,
192                "zlib Adler-32 mismatch: expected {expected:#010x}, got {actual:#010x}"
193            ),
194            Self::TrailingGarbage => formatter.write_str("trailing data after zlib stream"),
195        }
196    }
197}
198
199/// The reason a DEFLATE stream was rejected.
200#[derive(Clone, Debug, Eq, PartialEq)]
201#[non_exhaustive]
202pub enum DeflateErrorKind {
203    /// The backend rejected the compressed stream.
204    InvalidData,
205    /// The stream unexpectedly requested a preset dictionary.
206    UnexpectedDictionary,
207    /// The backend returned an unexpected status code.
208    BackendStatus(i32),
209    /// No progress was possible before the end of the compressed input.
210    Truncated,
211    /// The decoder made no progress despite having input and output space.
212    Stalled,
213    /// Bytes remained after the final block of a raw DEFLATE stream.
214    TrailingGarbage,
215}
216
217impl Display for DeflateErrorKind {
218    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
219        match self {
220            Self::InvalidData => formatter.write_str("invalid DEFLATE data"),
221            Self::UnexpectedDictionary => {
222                formatter.write_str("DEFLATE stream requested a preset dictionary")
223            }
224            Self::BackendStatus(status) => {
225                write!(formatter, "unexpected DEFLATE backend status {status}")
226            }
227            Self::Truncated => formatter.write_str("truncated DEFLATE stream"),
228            Self::Stalled => formatter.write_str("DEFLATE decoder made no progress"),
229            Self::TrailingGarbage => formatter.write_str("trailing data after raw DEFLATE stream"),
230        }
231    }
232}
233
234/// A terminal decoding error.
235///
236/// This type is cloneable so a [`std::io::Read`] adapter can return the same
237/// logical failure on every read after the pipeline has failed.
238#[derive(Clone, Debug)]
239#[non_exhaustive]
240pub enum DecodeError {
241    /// A positional input read or output write failed.
242    Io {
243        /// Compressed offset, when the operation was tied to an input offset.
244        offset: Option<u64>,
245        /// Shared original I/O error.
246        source: Arc<io::Error>,
247    },
248    /// The gzip framing was invalid.
249    InvalidGzip {
250        /// Compressed byte offset.
251        offset: u64,
252        /// Detailed reason.
253        reason: GzipErrorKind,
254    },
255    /// Automatic detection found neither gzip nor zlib framing.
256    UnrecognizedFormat,
257    /// The zlib framing was invalid.
258    InvalidZlib {
259        /// Compressed byte offset.
260        offset: u64,
261        /// Detailed reason.
262        reason: ZlibErrorKind,
263    },
264    /// The raw DEFLATE payload was invalid.
265    InvalidDeflate {
266        /// Best-known compressed bit offset.
267        bit_offset: u64,
268        /// Detailed reason.
269        reason: DeflateErrorKind,
270    },
271    /// Structural analysis exhausted a configured budget or exact counter.
272    Analysis {
273        /// Typed resource or counter failure.
274        reason: AnalysisErrorKind,
275    },
276    /// A member's CRC32 did not match its footer.
277    ChecksumMismatch {
278        /// Zero-based member number.
279        member: u64,
280        /// Footer value.
281        expected: u32,
282        /// Computed value.
283        actual: u32,
284    },
285    /// A member's modulo-2^32 output size did not match its footer.
286    SizeMismatch {
287        /// Zero-based member number.
288        member: u64,
289        /// Footer value.
290        expected: u32,
291        /// Computed value.
292        actual_mod32: u32,
293    },
294    /// Decoded output would exceed the configured limit.
295    OutputLimitExceeded {
296        /// Configured maximum decoded byte count.
297        limit: u64,
298    },
299    /// Decoded output did not equal the caller's exact expectation.
300    UnexpectedOutputSize {
301        /// Required total output size.
302        expected: u64,
303        /// Observed total, or the total that the next handoff would produce.
304        actual: u64,
305    },
306    /// Inflation did not reach an index checkpoint at its declared bit offset.
307    IndexBoundaryMismatch {
308        /// Compressed bit offset declared by the index.
309        expected_bit_offset: u64,
310        /// Compressed bit offset reached by the inflater.
311        actual_bit_offset: u64,
312    },
313    /// A checkpoint's decompressed offset disagreed with decoded output.
314    IndexOutputMismatch {
315        /// Compressed checkpoint whose output offset was checked.
316        checkpoint_bit_offset: u64,
317        /// Decompressed bytes declared between the surrounding checkpoints.
318        expected_bytes: u64,
319        /// Decompressed bytes actually produced for that span.
320        actual_bytes: u64,
321    },
322    /// A checkpoint's imported line counter disagreed with decoded output.
323    IndexLineMismatch {
324        /// Decompressed checkpoint whose line counter was checked.
325        checkpoint_byte_offset: u64,
326        /// Newline count declared by the index.
327        expected_lines: u64,
328        /// Newline count observed in ordered decoded output.
329        actual_lines: u64,
330    },
331    /// An imported index's total line counter disagreed with decoded output.
332    IndexTotalLineMismatch {
333        /// Newline count declared by the index.
334        expected_lines: u64,
335        /// Newline count observed in the complete decoded output.
336        actual_lines: u64,
337    },
338    /// A decoder worker panicked.
339    WorkerPanicked,
340    /// Decoding was cancelled because the consumer stopped.
341    Cancelled,
342}
343
344impl DecodeError {
345    pub(crate) fn input_io(offset: u64, source: io::Error) -> Self {
346        Self::Io {
347            offset: Some(offset),
348            source: Arc::new(source),
349        }
350    }
351
352    pub(crate) fn output_io(source: io::Error) -> Self {
353        Self::Io {
354            offset: None,
355            source: Arc::new(source),
356        }
357    }
358
359    pub(crate) fn io_kind(&self) -> io::ErrorKind {
360        match self {
361            Self::Io { source, .. } => source.kind(),
362            Self::InvalidGzip {
363                reason: GzipErrorKind::Truncated,
364                ..
365            }
366            | Self::InvalidZlib {
367                reason: ZlibErrorKind::Truncated,
368                ..
369            }
370            | Self::InvalidDeflate {
371                reason: DeflateErrorKind::Truncated,
372                ..
373            } => io::ErrorKind::UnexpectedEof,
374            Self::InvalidGzip { .. }
375            | Self::UnrecognizedFormat
376            | Self::InvalidZlib { .. }
377            | Self::InvalidDeflate { .. }
378            | Self::ChecksumMismatch { .. }
379            | Self::SizeMismatch { .. }
380            | Self::UnexpectedOutputSize { .. }
381            | Self::IndexBoundaryMismatch { .. }
382            | Self::IndexOutputMismatch { .. }
383            | Self::IndexLineMismatch { .. }
384            | Self::IndexTotalLineMismatch { .. } => io::ErrorKind::InvalidData,
385            Self::OutputLimitExceeded { .. }
386            | Self::Analysis {
387                reason:
388                    AnalysisErrorKind::ResourceLimit { .. } | AnalysisErrorKind::AllocationFailed { .. },
389            } => io::ErrorKind::FileTooLarge,
390            Self::Analysis {
391                reason: AnalysisErrorKind::CounterOverflow { .. },
392            } => io::ErrorKind::InvalidData,
393            Self::WorkerPanicked => io::ErrorKind::Other,
394            Self::Cancelled => io::ErrorKind::Interrupted,
395        }
396    }
397
398    pub(crate) fn to_io_error(&self) -> io::Error {
399        io::Error::new(self.io_kind(), self.clone())
400    }
401}
402
403impl Display for DecodeError {
404    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
405        match self {
406            Self::Io {
407                offset: Some(offset),
408                source,
409            } => write!(
410                formatter,
411                "I/O error at compressed offset {offset}: {source}"
412            ),
413            Self::Io {
414                offset: None,
415                source,
416            } => write!(formatter, "output I/O error: {source}"),
417            Self::InvalidGzip { offset, reason } => {
418                write!(formatter, "invalid gzip data at byte {offset}: {reason}")
419            }
420            Self::UnrecognizedFormat => {
421                formatter.write_str("input is neither recognizable gzip nor zlib data")
422            }
423            Self::InvalidZlib { offset, reason } => {
424                write!(formatter, "invalid zlib data at byte {offset}: {reason}")
425            }
426            Self::InvalidDeflate { bit_offset, reason } => {
427                write!(
428                    formatter,
429                    "invalid DEFLATE data at bit {bit_offset}: {reason}"
430                )
431            }
432            Self::Analysis { reason } => write!(formatter, "analysis failed: {reason}"),
433            Self::ChecksumMismatch {
434                member,
435                expected,
436                actual,
437            } => write!(
438                formatter,
439                "gzip member {member} CRC32 mismatch: expected {expected:#010x}, got {actual:#010x}"
440            ),
441            Self::SizeMismatch {
442                member,
443                expected,
444                actual_mod32,
445            } => write!(
446                formatter,
447                "gzip member {member} ISIZE mismatch: expected {expected}, got {actual_mod32}"
448            ),
449            Self::OutputLimitExceeded { limit } => {
450                write!(formatter, "decoded output exceeded the {limit}-byte limit")
451            }
452            Self::UnexpectedOutputSize { expected, actual } => write!(
453                formatter,
454                "decoded output size mismatch: expected {expected} bytes, got {actual}"
455            ),
456            Self::IndexBoundaryMismatch {
457                expected_bit_offset,
458                actual_bit_offset,
459            } => write!(
460                formatter,
461                "index checkpoint at bit {expected_bit_offset} did not match the inflater boundary at bit {actual_bit_offset}"
462            ),
463            Self::IndexOutputMismatch {
464                checkpoint_bit_offset,
465                expected_bytes,
466                actual_bytes,
467            } => write!(
468                formatter,
469                "index span ending at bit {checkpoint_bit_offset} declared {expected_bytes} output bytes but produced {actual_bytes}"
470            ),
471            Self::IndexLineMismatch {
472                checkpoint_byte_offset,
473                expected_lines,
474                actual_lines,
475            } => write!(
476                formatter,
477                "index checkpoint at decoded byte {checkpoint_byte_offset} declared {expected_lines} preceding newlines but output contains {actual_lines}"
478            ),
479            Self::IndexTotalLineMismatch {
480                expected_lines,
481                actual_lines,
482            } => write!(
483                formatter,
484                "index declared {expected_lines} total newlines but output contains {actual_lines}"
485            ),
486            Self::WorkerPanicked => formatter.write_str("a decoder worker panicked"),
487            Self::Cancelled => formatter.write_str("decoding was cancelled"),
488        }
489    }
490}
491
492impl Error for DecodeError {
493    fn source(&self) -> Option<&(dyn Error + 'static)> {
494        match self {
495            Self::Io { source, .. } => Some(source.as_ref()),
496            _ => None,
497        }
498    }
499}
500
501/// Statistics produced after the complete stream has been verified.
502#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
503pub struct DecodeReport {
504    /// Compressed bytes consumed.
505    pub compressed_bytes: u64,
506    /// Decompressed bytes emitted.
507    pub decompressed_bytes: u64,
508    /// Number of completed framing units: gzip members, or one zlib/raw stream.
509    ///
510    /// A raw-DEFLATE unit is structurally complete but has no container
511    /// checksum to authenticate it.
512    pub member_count: u64,
513    /// Configured decoder-worker budget.
514    pub decoder_threads: usize,
515    /// Concrete container framing that was decoded.
516    pub format: Format,
517    /// Newline bytes in the decoded output when line counting was enabled.
518    ///
519    /// Enable this with
520    /// [`DecoderBuilder::count_lines`](crate::DecoderBuilder::count_lines).
521    /// The value counts `b'\n'` bytes; a final unterminated line does not add
522    /// one to the count.
523    pub line_count: Option<u64>,
524}
525
526impl AsRef<DecodeReport> for DecodeReport {
527    fn as_ref(&self) -> &DecodeReport {
528        self
529    }
530}
531
532/// Result of a verified decode that also collected a random-access index.
533#[derive(Clone, Debug, Eq, PartialEq)]
534pub struct IndexedDecodeReport {
535    /// Scalar statistics for the verified decode.
536    pub decode: DecodeReport,
537    /// Random-access index built from authoritative decode boundaries.
538    pub index: DeflateIndex,
539}
540
541impl IndexedDecodeReport {
542    /// Returns the scalar decode report.
543    #[must_use]
544    pub const fn report(&self) -> &DecodeReport {
545        &self.decode
546    }
547
548    /// Returns the collected random-access index.
549    #[must_use]
550    pub const fn index(&self) -> &DeflateIndex {
551        &self.index
552    }
553
554    /// Separates the scalar report from the owning index.
555    #[must_use]
556    pub fn into_parts(self) -> (DecodeReport, DeflateIndex) {
557        (self.decode, self.index)
558    }
559}
560
561impl AsRef<DecodeReport> for IndexedDecodeReport {
562    fn as_ref(&self) -> &DecodeReport {
563        &self.decode
564    }
565}
566
567/// Failure of an operation that decodes and builds an index.
568#[derive(Clone, Debug)]
569#[non_exhaustive]
570pub enum IndexingError {
571    /// The compressed input could not be decoded and verified.
572    Decode(DecodeError),
573    /// The index could not be constructed or finalized.
574    Index(IndexError),
575}
576
577/// Failure while decoding through a caller-supplied index.
578///
579/// Index-driven decoding is strict: an invalid, incomplete, or source-mismatched
580/// index is reported rather than silently falling back to an unindexed path.
581#[derive(Clone, Debug)]
582#[non_exhaustive]
583pub enum IndexDecodeError {
584    /// The compressed input could not be decoded and verified.
585    Decode(DecodeError),
586    /// The supplied index was invalid or did not describe the source.
587    Index(IndexError),
588    /// The decoder's selected container disagreed with the index provenance.
589    FormatMismatch {
590        /// Container selected on the decoder builder.
591        selected: Format,
592        /// Container provenance recorded by the index.
593        indexed: IndexKind,
594    },
595}
596
597impl From<DecodeError> for IndexDecodeError {
598    fn from(error: DecodeError) -> Self {
599        Self::Decode(error)
600    }
601}
602
603impl From<IndexError> for IndexDecodeError {
604    fn from(error: IndexError) -> Self {
605        Self::Index(error)
606    }
607}
608
609impl Display for IndexDecodeError {
610    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
611        match self {
612            Self::Decode(error) => Display::fmt(error, formatter),
613            Self::Index(error) => {
614                write!(formatter, "index-driven decode rejected the index: {error}")
615            }
616            Self::FormatMismatch { selected, indexed } => write!(
617                formatter,
618                "decoder selected {selected}, but the index describes {indexed:?} data"
619            ),
620        }
621    }
622}
623
624impl Error for IndexDecodeError {
625    fn source(&self) -> Option<&(dyn Error + 'static)> {
626        match self {
627            Self::Decode(error) => Some(error),
628            Self::Index(error) => Some(error),
629            Self::FormatMismatch { .. } => None,
630        }
631    }
632}
633
634impl IndexingError {
635    pub(crate) fn to_io_error(&self) -> io::Error {
636        match self {
637            Self::Decode(error) => error.to_io_error(),
638            Self::Index(_) => io::Error::other(self.clone()),
639        }
640    }
641}
642
643impl From<DecodeError> for IndexingError {
644    fn from(error: DecodeError) -> Self {
645        Self::Decode(error)
646    }
647}
648
649impl From<IndexError> for IndexingError {
650    fn from(error: IndexError) -> Self {
651        Self::Index(error)
652    }
653}
654
655impl Display for IndexingError {
656    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
657        match self {
658            Self::Decode(error) => Display::fmt(error, formatter),
659            Self::Index(error) => write!(formatter, "index construction failed: {error}"),
660        }
661    }
662}
663
664impl Error for IndexingError {
665    fn source(&self) -> Option<&(dyn Error + 'static)> {
666        match self {
667            Self::Decode(error) => Some(error),
668            Self::Index(error) => Some(error),
669        }
670    }
671}