yencoding_multi/error.rs
1use std::ops::Range;
2
3/// Errors produced by [`crate::Assembler`] operations.
4#[derive(Debug, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum AssemblyError {
7 /// A part's byte range overlaps with an already-accepted part.
8 ///
9 /// yEnc parts must cover non-overlapping, contiguous byte ranges of the
10 /// final file. When two parts claim the same byte(s), the second is
11 /// rejected. `existing` is the range already stored; `new` is the range
12 /// that was rejected.
13 ///
14 /// **Caller action**: de-duplicate incoming articles (the same article
15 /// may arrive multiple times from different Usenet servers).
16 OverlappingPart {
17 existing: Range<u64>,
18 new: Range<u64>,
19 },
20
21 /// A part's byte range falls outside `[0, total_size)`.
22 ///
23 /// Either the `=ypart begin=/end=` values were invalid, or the wrong
24 /// `total_size` was passed to [`Assembler::new`][crate::Assembler::new].
25 OutOfRange {
26 begin: u64,
27 end: u64,
28 total_size: u64,
29 },
30
31 /// Whole-file CRC32 mismatch on [`Assembler::finish`][crate::Assembler::finish].
32 ///
33 /// The reassembled bytes hash to a different CRC32 than the expected value
34 /// set via [`Assembler::set_expected_crc32`][crate::Assembler::set_expected_crc32].
35 CrcMismatch { expected: u32, actual: u32 },
36
37 /// [`Assembler::finish`][crate::Assembler::finish] was called before all
38 /// byte ranges were covered.
39 ///
40 /// `missing` lists the 0-based byte ranges that have not been received.
41 /// Call [`Assembler::is_complete`][crate::Assembler::is_complete] before
42 /// `finish()` to avoid this error.
43 Incomplete { missing: Vec<Range<u64>> },
44
45 /// A decoded part's data length does not match its declared `=ypart begin=/end=` range.
46 ///
47 /// This indicates a corrupt or malformed article: the decoded payload is
48 /// a different size than the byte range it claims to cover.
49 DataLengthMismatch {
50 /// The byte count implied by the `begin`/`end` range header.
51 declared_range_len: usize,
52 /// The actual number of decoded bytes.
53 actual_data_len: usize,
54 },
55
56 /// `total_size` exceeds the built-in safety cap or the addressable memory
57 /// on this platform.
58 ///
59 /// [`Assembler::new`][crate::Assembler::new] rejects any `total_size`
60 /// greater than [`MAX_TOTAL_SIZE`][crate::MAX_TOTAL_SIZE] (512 MiB) to
61 /// prevent an adversarial `=ybegin size=` field from forcing a multi-GiB
62 /// allocation. On 32-bit targets the platform `usize` limit applies first.
63 TotalSizeTooLarge {
64 /// The value that was rejected.
65 total_size: u64,
66 },
67
68 /// A decoded part has `part_begin` set but `part_end` absent, or vice versa.
69 ///
70 /// yEnc `=ypart begin=/end=` always provides both values or neither.
71 /// A `DecodedPart` with only one of the two fields set indicates a corrupt
72 /// or incorrectly constructed part.
73 MalformedPartRange,
74
75 /// Two parts carry conflicting whole-file CRC32 values.
76 ///
77 /// [`Assembler::add_part`][crate::Assembler::add_part] auto-extracts the
78 /// `crc32=` whole-file CRC from each part that carries it. If a later part
79 /// reports a different value than one already recorded, the series is
80 /// internally inconsistent and the assembly must be aborted.
81 ///
82 /// `first` is the CRC recorded from an earlier part; `new` is the
83 /// conflicting value from the current part.
84 InconsistentCrc { first: u32, new: u32 },
85}
86
87impl std::fmt::Display for AssemblyError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 AssemblyError::OverlappingPart { existing, new } => write!(
91 f,
92 "part byte range {}..{} overlaps with already-stored range {}..{} — \
93 check for duplicate articles",
94 new.start, new.end, existing.start, existing.end
95 ),
96 AssemblyError::OutOfRange {
97 begin,
98 end,
99 total_size,
100 } => write!(
101 f,
102 "part byte range {}..{} is outside total file size {} — \
103 verify =ypart begin=/end= and total_size",
104 begin, end, total_size
105 ),
106 AssemblyError::CrcMismatch { expected, actual } => write!(
107 f,
108 "whole-file CRC32 mismatch: expected {:#010x}, got {:#010x} — \
109 re-fetch all parts and retry",
110 expected, actual
111 ),
112 AssemblyError::Incomplete { missing } => {
113 write!(
114 f,
115 "assembly incomplete: missing {} byte range(s): ",
116 missing.len()
117 )?;
118 for (i, r) in missing.iter().enumerate() {
119 if i > 0 {
120 write!(f, ", ")?;
121 }
122 write!(f, "{}..{}", r.start, r.end)?;
123 }
124 Ok(())
125 }
126 AssemblyError::DataLengthMismatch {
127 declared_range_len,
128 actual_data_len,
129 } => write!(
130 f,
131 "part data length {actual_data_len} does not match declared range length \
132 {declared_range_len} — corrupt or malformed article"
133 ),
134 AssemblyError::TotalSizeTooLarge { total_size } => write!(
135 f,
136 "total_size {total_size} exceeds the 512 MiB safety cap or the \
137 addressable memory on this platform (usize::MAX = {})",
138 usize::MAX
139 ),
140 AssemblyError::MalformedPartRange => write!(
141 f,
142 "part_begin and part_end must both be Some or both be None"
143 ),
144 AssemblyError::InconsistentCrc { first, new } => write!(
145 f,
146 "inconsistent whole-file CRC32 across parts: \
147 first seen {first:#010x}, new part reports {new:#010x} — \
148 corrupt or mismatched article series"
149 ),
150 }
151 }
152}
153
154impl std::error::Error for AssemblyError {}