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