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 assert_eq!(permission, permission_again);
314 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}