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