Skip to main content

rars_format/
error.rs

1use crate::version::{ArchiveFamily, ArchiveVersion};
2use std::sync::Arc;
3
4pub type Result<T> = std::result::Result<T, Error>;
5
6#[derive(Debug, Clone)]
7pub struct IoError {
8    pub kind: std::io::ErrorKind,
9    pub message: String,
10    source: Arc<std::io::Error>,
11}
12
13impl IoError {
14    pub fn source(&self) -> &(dyn std::error::Error + 'static) {
15        self.source.as_ref()
16    }
17}
18
19impl PartialEq for IoError {
20    fn eq(&self, other: &Self) -> bool {
21        self.kind == other.kind && self.message == other.message
22    }
23}
24
25impl Eq for IoError {}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Error {
30    TooShort,
31    UnsupportedSignature,
32    InvalidHeader(&'static str),
33    AtArchiveOffset {
34        offset: usize,
35        source: Box<Error>,
36    },
37    AtEntry {
38        name: Vec<u8>,
39        operation: &'static str,
40        source: Box<Error>,
41    },
42    UnsupportedVersion(ArchiveVersion),
43    UnsupportedFeature {
44        version: ArchiveVersion,
45        feature: &'static str,
46    },
47    Rar50BufferedDecodeLimitExceeded {
48        limit: u64,
49        required: u64,
50    },
51    UnsupportedFamilyFeature {
52        family: ArchiveFamily,
53        feature: &'static str,
54    },
55    UnsupportedCompression {
56        family: &'static str,
57        unpack_version: u8,
58        method: u8,
59    },
60    UnsupportedEncryption {
61        family: &'static str,
62        unpack_version: u8,
63    },
64    Io(IoError),
65    NeedPassword,
66    WrongPasswordOrCorruptData,
67    CrcMismatch {
68        expected: u16,
69        actual: u16,
70    },
71    Crc32Mismatch {
72        expected: u32,
73        actual: u32,
74    },
75    HashMismatch {
76        hash_type: u64,
77    },
78    Codec(rars_codec::Error),
79    Rar3Recovery(rars_recovery::rar3::Error),
80    Rar5Recovery(rars_recovery::rar5::Error),
81    Rar20Crypto(rars_crypto::rar20::Error),
82    Rar30Crypto(rars_crypto::rar30::Error),
83    Rar50Crypto(rars_crypto::rar50::Error),
84}
85
86impl std::fmt::Display for Error {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::TooShort => write!(f, "input is too short"),
90            Self::UnsupportedSignature => write!(f, "unsupported archive signature"),
91            Self::InvalidHeader(msg) => write!(f, "invalid header: {msg}"),
92            Self::AtArchiveOffset { offset, source } => {
93                write!(f, "at archive offset {offset:#x}: {source}")
94            }
95            Self::AtEntry {
96                name,
97                operation,
98                source,
99            } => {
100                write!(
101                    f,
102                    "while {operation} entry '{}': {source}",
103                    String::from_utf8_lossy(name)
104                )
105            }
106            Self::UnsupportedVersion(version) => write!(f, "unsupported version: {version:?}"),
107            Self::UnsupportedFeature { version, feature } => {
108                write!(f, "feature {feature} is not supported by {version:?}")
109            }
110            Self::Rar50BufferedDecodeLimitExceeded { limit, required } => write!(
111                f,
112                "RAR 5 filtered member requires buffered decoding of {required} bytes, above the configured limit of {limit} bytes"
113            ),
114            Self::UnsupportedFamilyFeature { family, feature } => {
115                write!(f, "feature {feature} is not supported by {family:?}")
116            }
117            Self::UnsupportedCompression {
118                family,
119                unpack_version,
120                method,
121            } => write!(
122                f,
123                "{family} compression is not supported: unpack version {unpack_version}, method {method:#04x}"
124            ),
125            Self::UnsupportedEncryption {
126                family,
127                unpack_version,
128            } => write!(
129                f,
130                "{family} encryption is not supported: unpack version {unpack_version}"
131            ),
132            Self::Io(error) => write!(f, "I/O error: {}", error.message),
133            Self::NeedPassword => write!(f, "a password is required"),
134            Self::WrongPasswordOrCorruptData => {
135                write!(f, "wrong password or corrupt encrypted data")
136            }
137            Self::CrcMismatch { expected, actual } => {
138                write!(
139                    f,
140                    "checksum mismatch: expected {expected:#06x}, got {actual:#06x}"
141                )
142            }
143            Self::Crc32Mismatch { expected, actual } => {
144                write!(
145                    f,
146                    "checksum mismatch: expected {expected:#010x}, got {actual:#010x}"
147                )
148            }
149            Self::HashMismatch { hash_type } => {
150                write!(f, "hash mismatch for hash type {hash_type}")
151            }
152            Self::Codec(error) => write!(f, "{error}"),
153            Self::Rar3Recovery(error) => write!(f, "{error}"),
154            Self::Rar5Recovery(error) => write!(f, "{error}"),
155            Self::Rar20Crypto(error) => write!(f, "{error}"),
156            Self::Rar30Crypto(error) => write!(f, "{error}"),
157            Self::Rar50Crypto(error) => write!(f, "{error}"),
158        }
159    }
160}
161
162impl From<std::io::Error> for Error {
163    fn from(error: std::io::Error) -> Self {
164        Self::Io(IoError {
165            kind: error.kind(),
166            message: error.to_string(),
167            source: Arc::new(error),
168        })
169    }
170}
171
172impl std::error::Error for Error {
173    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
174        match self {
175            Self::AtArchiveOffset { source, .. } | Self::AtEntry { source, .. } => Some(source),
176            Self::Codec(source) => Some(source),
177            Self::Rar3Recovery(source) => Some(source),
178            Self::Rar5Recovery(source) => Some(source),
179            Self::Rar20Crypto(source) => Some(source),
180            Self::Rar30Crypto(source) => Some(source),
181            Self::Rar50Crypto(source) => Some(source),
182            Self::Io(source) => Some(source.source()),
183            _ => None,
184        }
185    }
186}
187
188impl Error {
189    pub fn at_archive_offset(self, offset: usize) -> Self {
190        Self::AtArchiveOffset {
191            offset,
192            source: Box::new(self),
193        }
194    }
195
196    pub fn at_entry(self, name: Vec<u8>, operation: &'static str) -> Self {
197        Self::AtEntry {
198            name,
199            operation,
200            source: Box::new(self),
201        }
202    }
203}
204
205impl From<rars_codec::Error> for Error {
206    fn from(error: rars_codec::Error) -> Self {
207        Self::Codec(error)
208    }
209}
210
211impl From<rars_recovery::rar5::Error> for Error {
212    fn from(error: rars_recovery::rar5::Error) -> Self {
213        Self::Rar5Recovery(error)
214    }
215}
216
217impl From<rars_recovery::rar3::Error> for Error {
218    fn from(error: rars_recovery::rar3::Error) -> Self {
219        Self::Rar3Recovery(error)
220    }
221}
222
223impl From<rars_crypto::rar20::Error> for Error {
224    fn from(error: rars_crypto::rar20::Error) -> Self {
225        Self::Rar20Crypto(error)
226    }
227}
228
229impl From<rars_crypto::rar30::Error> for Error {
230    fn from(error: rars_crypto::rar30::Error) -> Self {
231        Self::Rar30Crypto(error)
232    }
233}
234
235impl From<rars_crypto::rar50::Error> for Error {
236    fn from(error: rars_crypto::rar50::Error) -> Self {
237        Self::Rar50Crypto(error)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn archive_offset_context_exposes_source_error() {
247        let error = Error::InvalidHeader("bad block").at_archive_offset(0x1234);
248
249        assert_eq!(
250            error.to_string(),
251            "at archive offset 0x1234: invalid header: bad block"
252        );
253        assert_eq!(
254            std::error::Error::source(&error).map(ToString::to_string),
255            Some("invalid header: bad block".to_string())
256        );
257    }
258
259    #[test]
260    fn entry_context_exposes_source_error() {
261        let error = Error::Crc32Mismatch {
262            expected: 1,
263            actual: 2,
264        }
265        .at_entry(b"hello.txt".to_vec(), "verifying");
266
267        assert_eq!(
268            error.to_string(),
269            "while verifying entry 'hello.txt': checksum mismatch: expected 0x00000001, got 0x00000002"
270        );
271        assert_eq!(
272            std::error::Error::source(&error).map(ToString::to_string),
273            Some("checksum mismatch: expected 0x00000001, got 0x00000002".to_string())
274        );
275    }
276
277    #[test]
278    fn io_error_preserves_error_kind() {
279        let error = Error::from(std::io::Error::new(
280            std::io::ErrorKind::PermissionDenied,
281            "locked",
282        ));
283
284        assert!(matches!(
285            error,
286            Error::Io(ref source) if source.kind == std::io::ErrorKind::PermissionDenied
287        ));
288        assert_eq!(error.to_string(), "I/O error: locked");
289        assert_eq!(
290            std::error::Error::source(&error).map(ToString::to_string),
291            Some("locked".to_string())
292        );
293    }
294
295    #[test]
296    fn io_error_partial_eq_compares_kind_and_message_ignoring_source_identity() {
297        let permission = Error::from(std::io::Error::new(
298            std::io::ErrorKind::PermissionDenied,
299            "locked",
300        ));
301        let permission_again = Error::from(std::io::Error::new(
302            std::io::ErrorKind::PermissionDenied,
303            "locked",
304        ));
305        let different_kind =
306            Error::from(std::io::Error::new(std::io::ErrorKind::NotFound, "locked"));
307        let different_message = Error::from(std::io::Error::new(
308            std::io::ErrorKind::PermissionDenied,
309            "elsewhere",
310        ));
311
312        // Same kind + same message → equal even though Arc<io::Error> differs.
313        assert_eq!(permission, permission_again);
314        // Differing kind or message → not equal.
315        assert_ne!(permission, different_kind);
316        assert_ne!(permission, different_message);
317    }
318
319    #[test]
320    fn unsupported_codec_errors_are_not_reported_as_invalid_headers() {
321        assert_eq!(
322            Error::UnsupportedCompression {
323                family: "RAR 1.5-4.x",
324                unpack_version: 14,
325                method: 0x33,
326            }
327            .to_string(),
328            "RAR 1.5-4.x compression is not supported: unpack version 14, method 0x33"
329        );
330        assert_eq!(
331            Error::UnsupportedEncryption {
332                family: "RAR 1.5-4.x",
333                unpack_version: 14,
334            }
335            .to_string(),
336            "RAR 1.5-4.x encryption is not supported: unpack version 14"
337        );
338    }
339
340    #[test]
341    fn codec_errors_remain_inspectable_without_changing_display_text() {
342        let error = Error::from(rars_codec::Error::NeedMoreInput);
343
344        assert!(matches!(
345            error,
346            Error::Codec(rars_codec::Error::NeedMoreInput)
347        ));
348        assert_eq!(error.to_string(), "codec input is truncated");
349        assert_eq!(
350            std::error::Error::source(&error).map(ToString::to_string),
351            Some("codec input is truncated".to_string())
352        );
353    }
354
355    #[test]
356    fn recovery_errors_remain_inspectable_without_changing_display_text() {
357        let error = Error::from(rars_recovery::rar5::Error::TooManyDamagedShards);
358
359        assert!(matches!(
360            error,
361            Error::Rar5Recovery(rars_recovery::rar5::Error::TooManyDamagedShards)
362        ));
363        assert_eq!(
364            error.to_string(),
365            "RAR 5 recovery data cannot repair this many damaged shards"
366        );
367        assert_eq!(
368            std::error::Error::source(&error).map(ToString::to_string),
369            Some("RAR 5 recovery data cannot repair this many damaged shards".to_string())
370        );
371    }
372
373    #[test]
374    fn rar3_recovery_errors_remain_in_source_chain() {
375        let error = Error::from(rars_recovery::rar3::Error::DecodeFailed);
376
377        assert_eq!(error.to_string(), "RAR 3 recovery decode failed");
378        assert_eq!(
379            std::error::Error::source(&error).map(ToString::to_string),
380            Some("RAR 3 recovery decode failed".to_string())
381        );
382    }
383
384    #[test]
385    fn rar30_crypto_errors_remain_in_source_chain() {
386        let error = Error::from(rars_crypto::rar30::Error::UnalignedInput);
387
388        assert!(matches!(
389            error,
390            Error::Rar30Crypto(rars_crypto::rar30::Error::UnalignedInput)
391        ));
392        assert_eq!(error.to_string(), "RAR 3.x AES input is not block aligned");
393        assert_eq!(
394            std::error::Error::source(&error).map(ToString::to_string),
395            Some("RAR 3.x AES input is not block aligned".to_string())
396        );
397    }
398
399    #[test]
400    fn rar20_crypto_errors_remain_in_source_chain() {
401        let error = Error::from(rars_crypto::rar20::Error::UnalignedInput);
402
403        assert!(matches!(
404            error,
405            Error::Rar20Crypto(rars_crypto::rar20::Error::UnalignedInput)
406        ));
407        assert_eq!(
408            error.to_string(),
409            "RAR 2.0 cipher input is not block aligned"
410        );
411        assert_eq!(
412            std::error::Error::source(&error).map(ToString::to_string),
413            Some("RAR 2.0 cipher input is not block aligned".to_string())
414        );
415    }
416
417    #[test]
418    fn rar50_crypto_errors_remain_in_source_chain() {
419        let error = Error::from(rars_crypto::rar50::Error::UnalignedInput);
420
421        assert_eq!(error.to_string(), "RAR 5 AES input is not block aligned");
422        assert_eq!(
423            std::error::Error::source(&error).map(ToString::to_string),
424            Some("RAR 5 AES input is not block aligned".to_string())
425        );
426    }
427
428    #[test]
429    fn simple_display_variants_render_their_messages() {
430        assert_eq!(Error::TooShort.to_string(), "input is too short");
431        assert_eq!(
432            Error::UnsupportedSignature.to_string(),
433            "unsupported archive signature"
434        );
435        assert_eq!(Error::NeedPassword.to_string(), "a password is required");
436        assert_eq!(
437            Error::WrongPasswordOrCorruptData.to_string(),
438            "wrong password or corrupt encrypted data"
439        );
440        assert_eq!(
441            Error::HashMismatch { hash_type: 7 }.to_string(),
442            "hash mismatch for hash type 7"
443        );
444        assert_eq!(
445            Error::CrcMismatch {
446                expected: 0xabcd,
447                actual: 0x1234
448            }
449            .to_string(),
450            "checksum mismatch: expected 0xabcd, got 0x1234"
451        );
452        assert_eq!(
453            Error::UnsupportedVersion(ArchiveVersion::Rar50).to_string(),
454            "unsupported version: Rar50"
455        );
456        assert_eq!(
457            Error::UnsupportedFeature {
458                version: ArchiveVersion::Rar50,
459                feature: "quantum compression",
460            }
461            .to_string(),
462            "feature quantum compression is not supported by Rar50"
463        );
464        assert_eq!(
465            Error::Rar50BufferedDecodeLimitExceeded {
466                limit: 536_870_912,
467                required: 734_003_200,
468            }
469            .to_string(),
470            "RAR 5 filtered member requires buffered decoding of 734003200 bytes, above the configured limit of 536870912 bytes"
471        );
472    }
473
474    #[test]
475    fn rar3_recovery_errors_render_messages_through_from_conversion() {
476        assert_eq!(
477            Error::from(rars_recovery::rar3::Error::InvalidParitySize).to_string(),
478            "RAR 3 recovery parity size is invalid"
479        );
480        assert_eq!(
481            Error::from(rars_recovery::rar3::Error::InvalidCodewordSize).to_string(),
482            "RAR 3 recovery codeword size is invalid"
483        );
484        assert_eq!(
485            Error::from(rars_recovery::rar3::Error::TooManyErasures).to_string(),
486            "RAR 3 recovery data cannot repair this many erasures"
487        );
488        assert_eq!(
489            Error::from(rars_recovery::rar3::Error::DecodeFailed).to_string(),
490            "RAR 3 recovery decode failed"
491        );
492    }
493
494    #[test]
495    fn rar5_recovery_errors_render_messages_for_every_named_variant() {
496        let cases = [
497            (
498                rars_recovery::rar5::Error::BadRecoveryChunk,
499                "RAR 5 recovery chunk is invalid",
500            ),
501            (
502                rars_recovery::rar5::Error::OddShardSize,
503                "RAR 5 recovery shard size is odd",
504            ),
505            (
506                rars_recovery::rar5::Error::PlanOverflow,
507                "RAR 5 recovery plan overflows",
508            ),
509            (
510                rars_recovery::rar5::Error::PrefixExceedsPlan,
511                "RAR 5 recovery prefix exceeds planned shard capacity",
512            ),
513            (
514                rars_recovery::rar5::Error::ShardSizeMismatch,
515                "RAR 5 recovery shard sizes differ",
516            ),
517            (
518                rars_recovery::rar5::Error::TooManyShards,
519                "RAR 5 recovery shard count is invalid",
520            ),
521            (
522                rars_recovery::rar5::Error::SingularElement,
523                "RAR 5 recovery matrix is singular",
524            ),
525        ];
526        for (variant, expected) in cases {
527            assert_eq!(Error::from(variant).to_string(), expected);
528        }
529    }
530
531    #[test]
532    fn rar50_crypto_errors_render_messages_through_from_conversion() {
533        assert_eq!(
534            Error::from(rars_crypto::rar50::Error::KdfCountTooLarge).to_string(),
535            "RAR 5 KDF count is too large"
536        );
537        assert_eq!(
538            Error::from(rars_crypto::rar50::Error::BadPassword).to_string(),
539            "wrong password or corrupt encrypted data"
540        );
541    }
542
543    #[test]
544    fn codec_invalid_data_display_uses_inner_message() {
545        assert_eq!(
546            Error::from(rars_codec::Error::InvalidData("bad symbol")).to_string(),
547            "bad symbol"
548        );
549    }
550
551    #[test]
552    fn unsupported_family_feature_display_renders_family_and_feature() {
553        assert_eq!(
554            Error::UnsupportedFamilyFeature {
555                family: ArchiveFamily::Rar13,
556                feature: "recovery repair for RAR 1.3/1.4 archives",
557            }
558            .to_string(),
559            "feature recovery repair for RAR 1.3/1.4 archives is not supported by Rar13",
560        );
561    }
562
563    #[test]
564    fn rar50_crypto_unaligned_input_display_uses_named_message() {
565        assert_eq!(
566            Error::from(rars_crypto::rar50::Error::UnalignedInput).to_string(),
567            "RAR 5 AES input is not block aligned"
568        );
569    }
570}