Skip to main content

rars/
lib.rs

1//! High-level RAR archive API.
2//!
3//! This crate is the supported public Rust API for `rars`. It is the facade
4//! over the version-specific format modules, detects archive families, exposes
5//! common member metadata, and streams extraction or recovery output to
6//! caller-provided writers without requiring callers to buffer whole archives
7//! in memory. New Rust users should depend on this crate rather than the
8//! lower-level `rars-*` implementation crates, which ended at 0.3.x.
9
10/// Where the unit tests write their files. Shared with the integration tests
11/// by path rather than by API.
12#[cfg(test)]
13#[path = "../tests/support/scratch.rs"]
14mod scratch;
15
16pub mod builder;
17#[doc(hidden)]
18pub mod codec;
19pub mod crc32;
20#[doc(hidden)]
21pub mod crypto;
22pub mod detect;
23pub mod error;
24mod fast;
25pub mod features;
26pub mod filter;
27mod filter_search;
28mod io_util;
29mod parallel;
30pub mod rar13;
31pub mod rar15_40;
32pub mod rar50;
33#[doc(hidden)]
34pub mod recovery;
35mod source;
36mod streaming;
37pub mod version;
38mod volume_extract;
39pub mod write_plan;
40mod write_progress;
41mod write_stream;
42mod x86_filter_scan;
43
44pub use builder::{entry_relative_path, validate_entry_name, Builder};
45pub use detect::{detect_archive_family, find_archive_start, ArchiveSignature, SFX_SCAN_LIMIT};
46pub use error::{Error, Result};
47pub use features::{Feature, FeatureSet};
48pub use filter::{
49    formats_supporting_filter, FilterKind, FilterPolicy, FilterSpec, UnsupportedFilterKind,
50};
51use std::io::{Read, Write};
52use std::path::Path;
53pub use streaming::{EntryReader, EntrySource, WriterResources, DEFAULT_WRITER_MEMORY_LIMIT};
54pub use version::{ArchiveFamily, ArchiveVersion};
55pub use write_plan::{
56    formats_supporting, supported_features, supports, MemberCoding, PlanShape, WriterOption,
57};
58pub use write_progress::{WriteOperation, WriteProgress, WriteProgressEvent};
59
60#[derive(Debug, Clone, Copy, Default)]
61#[non_exhaustive]
62/// Options used while parsing or extracting archives.
63pub struct ArchiveReadOptions<'a> {
64    /// Password bytes used for encrypted headers or payloads.
65    pub password: Option<&'a [u8]>,
66    /// Optional RAR 5 whole-member buffered decode limit.
67    ///
68    /// Filtered RAR 5 members need whole-member transforms. Compressed members
69    /// above this limit use the streaming path and reject filtered streams
70    /// with an unsupported-feature error instead of buffering the full member.
71    pub rar50_buffered_decode_limit: Option<u64>,
72}
73
74impl<'a> ArchiveReadOptions<'a> {
75    /// Creates read options without a password.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Creates read options with a password.
81    pub fn with_password(password: &'a [u8]) -> Self {
82        Self {
83            password: Some(password),
84            ..Self::default()
85        }
86    }
87
88    /// Creates read options with an optional password.
89    pub fn with_optional_password(password: Option<&'a [u8]>) -> Self {
90        Self {
91            password,
92            ..Self::default()
93        }
94    }
95
96    /// Sets the RAR 5 whole-member buffered decode limit.
97    pub fn with_rar50_buffered_decode_limit(mut self, limit: u64) -> Self {
98        self.rar50_buffered_decode_limit = Some(limit);
99        self
100    }
101}
102
103#[derive(Debug, Clone)]
104#[non_exhaustive]
105/// A parsed RAR archive, preserving the concrete archive family.
106pub enum Archive {
107    /// RAR 1.3/1.4 archive.
108    Rar13(rar13::Archive),
109    /// RAR 1.5 through RAR 4.x archive.
110    Rar15To40(rar15_40::Archive),
111    /// RAR 5.0 or later archive, including RAR 7 archives.
112    Rar50Plus(rar50::Archive),
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116#[non_exhaustive]
117/// Metadata supplied to streaming extraction callbacks.
118pub struct ExtractedEntryMeta {
119    /// Raw entry name bytes as stored by the archive family.
120    pub name: Vec<u8>,
121    /// DOS/FAT timestamp when the archive family exposes one.
122    pub file_time: u32,
123    /// File attributes widened to a common integer type.
124    pub file_attr: u64,
125    /// Whether the entry is a directory.
126    pub is_directory: bool,
127}
128
129impl ExtractedEntryMeta {
130    /// Creates common metadata for extraction callbacks.
131    pub fn new(name: Vec<u8>, file_time: u32, file_attr: u64, is_directory: bool) -> Self {
132        Self {
133            name,
134            file_time,
135            file_attr,
136            is_directory,
137        }
138    }
139
140    /// Raw entry name bytes as stored by the archive family.
141    pub fn name_bytes(&self) -> &[u8] {
142        &self.name
143    }
144
145    /// Returns the entry name with invalid UTF-8 replaced for display only.
146    ///
147    /// Use [`Self::name_bytes`] when exact archive bytes matter.
148    pub fn name_lossy(&self) -> String {
149        String::from_utf8_lossy(&self.name).into_owned()
150    }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154#[non_exhaustive]
155/// Common member view plus family-specific detail.
156pub struct ArchiveMember {
157    /// Metadata shared across archive families.
158    pub meta: ArchiveMemberMeta,
159    /// Extra metadata that is meaningful only for one archive family.
160    pub detail: ArchiveMemberDetail,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164#[non_exhaustive]
165/// Family-independent metadata for a file-like archive member.
166pub struct ArchiveMemberMeta {
167    /// Archive family that produced this member.
168    pub family: ArchiveFamily,
169    /// Raw entry name bytes as stored by the archive.
170    pub name: Vec<u8>,
171    /// Packed payload size in bytes.
172    pub packed_size: u64,
173    /// Unpacked file size in bytes.
174    pub unpacked_size: u64,
175    /// DOS/FAT timestamp when present.
176    pub file_time: Option<u32>,
177    /// File attributes widened to a common integer type.
178    pub file_attr: u64,
179    /// Host OS discriminator when present in the archive format.
180    pub host_os: Option<u64>,
181    /// Whether the member is a directory.
182    pub is_directory: bool,
183    /// Whether the member payload is encrypted.
184    pub is_encrypted: bool,
185    /// Whether the member payload is stored without compression.
186    pub is_stored: bool,
187    /// Whether the member continues from a previous volume.
188    pub is_split_before: bool,
189    /// Whether the member continues into the next volume.
190    pub is_split_after: bool,
191}
192
193impl ArchiveMemberMeta {
194    /// Raw member name bytes as stored by the archive family.
195    pub fn name_bytes(&self) -> &[u8] {
196        &self.name
197    }
198
199    /// Returns the member name with invalid UTF-8 replaced for display only.
200    ///
201    /// Use [`Self::name_bytes`] when exact archive bytes matter.
202    pub fn name_lossy(&self) -> String {
203        String::from_utf8_lossy(&self.name).into_owned()
204    }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208#[non_exhaustive]
209/// Family-specific member metadata.
210pub enum ArchiveMemberDetail {
211    /// RAR 1.3/1.4 member fields.
212    #[non_exhaustive]
213    Rar13 {
214        /// Compression method byte from the file header.
215        method: u8,
216        /// Minimum unpacker version byte from the file header.
217        unpack_version: u8,
218        /// Legacy 16-bit file checksum.
219        file_checksum: u16,
220        /// Whether the member carries a file-comment extension.
221        has_file_comment: bool,
222    },
223    /// RAR 1.5 through RAR 4.x member fields.
224    #[non_exhaustive]
225    Rar15To40 {
226        /// Compression method byte from the file header.
227        method: u8,
228        /// Minimum unpacker version byte from the file header.
229        unpack_version: u8,
230        /// Stored CRC-32 of the unpacked data.
231        crc32: u32,
232        /// Whether this member participates in a solid stream.
233        solid: bool,
234        /// Per-file salt when file encryption is used.
235        salt: Option<[u8; 8]>,
236        /// Whether the member carries a file-comment extension.
237        has_file_comment: bool,
238    },
239    /// RAR 5.0 and later member fields.
240    #[non_exhaustive]
241    Rar50Plus {
242        /// Raw compression-info field from the RAR5 file header.
243        compression_info: u64,
244        /// Stored CRC-32 when present.
245        crc32: Option<u32>,
246        /// Strong file hash when present.
247        hash: Option<ArchiveMemberHash>,
248    },
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252#[non_exhaustive]
253/// Strong hash metadata attached to an archive member.
254pub enum ArchiveMemberHash {
255    /// RAR5 BLAKE2sp file hash.
256    Blake2sp([u8; 32]),
257    /// Unknown hash record retained for inspection.
258    Other { hash_type: u64, data: Vec<u8> },
259}
260
261#[derive(Debug, Clone)]
262#[non_exhaustive]
263/// Lazy iterator returned by [`Archive::members`].
264pub struct ArchiveMembers<'a> {
265    inner: ArchiveMembersInner<'a>,
266    index: usize,
267}
268
269#[derive(Debug, Clone)]
270enum ArchiveMembersInner<'a> {
271    Rar13(&'a [rar13::Entry]),
272    Rar15To40(&'a [rar15_40::Block]),
273    Rar50Plus(&'a [rar50::Block]),
274}
275
276impl Iterator for ArchiveMembers<'_> {
277    type Item = ArchiveMember;
278
279    fn next(&mut self) -> Option<Self::Item> {
280        match self.inner {
281            ArchiveMembersInner::Rar13(entries) => {
282                let entry = entries.get(self.index)?;
283                self.index += 1;
284                Some(rar13_member(entry))
285            }
286            ArchiveMembersInner::Rar15To40(blocks) => {
287                while let Some(block) = blocks.get(self.index) {
288                    self.index += 1;
289                    if let rar15_40::Block::File(file) = block {
290                        return Some(rar15_40_member(file));
291                    }
292                }
293                None
294            }
295            ArchiveMembersInner::Rar50Plus(blocks) => {
296                while let Some(block) = blocks.get(self.index) {
297                    self.index += 1;
298                    if let rar50::Block::File(file) = block {
299                        return Some(rar50_member(file));
300                    }
301                }
302                None
303            }
304        }
305    }
306}
307
308/// A `Write` that appends into a buffer someone else holds, for the extraction
309/// callbacks that hand their writer away and need the bytes back.
310struct SharedBuffer(std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>);
311
312impl SharedBuffer {
313    fn lock(&self) -> std::sync::MutexGuard<'_, Option<Vec<u8>>> {
314        self.0
315            .lock()
316            .unwrap_or_else(|poisoned| poisoned.into_inner())
317    }
318}
319
320impl Write for SharedBuffer {
321    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
322        self.lock()
323            .get_or_insert_with(Vec::new)
324            .extend_from_slice(buf);
325        Ok(buf.len())
326    }
327
328    fn flush(&mut self) -> std::io::Result<()> {
329        Ok(())
330    }
331}
332
333impl Archive {
334    /// Returns the detected archive family.
335    pub fn family(&self) -> ArchiveFamily {
336        match self {
337            Self::Rar13(_) => ArchiveFamily::Rar13,
338            Self::Rar15To40(_) => ArchiveFamily::Rar15To40,
339            Self::Rar50Plus(_) => ArchiveFamily::Rar50Plus,
340        }
341    }
342
343    /// Returns the byte offset where the RAR archive begins after any SFX stub.
344    pub fn sfx_offset(&self) -> usize {
345        match self {
346            Self::Rar13(archive) => archive.sfx_offset,
347            Self::Rar15To40(archive) => archive.sfx_offset,
348            Self::Rar50Plus(archive) => archive.sfx_offset,
349        }
350    }
351
352    /// Iterates over file-like members using a common cross-version metadata view.
353    pub fn members(&self) -> ArchiveMembers<'_> {
354        match self {
355            Self::Rar13(archive) => ArchiveMembers {
356                inner: ArchiveMembersInner::Rar13(&archive.entries),
357                index: 0,
358            },
359            Self::Rar15To40(archive) => ArchiveMembers {
360                inner: ArchiveMembersInner::Rar15To40(&archive.blocks),
361                index: 0,
362            },
363            Self::Rar50Plus(archive) => ArchiveMembers {
364                inner: ArchiveMembersInner::Rar50Plus(&archive.blocks),
365                index: 0,
366            },
367        }
368    }
369
370    /// Streams extracted entries to caller-provided writers.
371    pub fn extract_to<F>(&self, password: Option<&[u8]>, open: F) -> Result<()>
372    where
373        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
374    {
375        self.extract_to_with_options(read_options(password), open)
376    }
377
378    /// Streams extracted entries to caller-provided writers with read options.
379    pub fn extract_to_with_options<F>(
380        &self,
381        options: ArchiveReadOptions<'_>,
382        mut open: F,
383    ) -> Result<()>
384    where
385        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
386    {
387        match self {
388            Self::Rar13(archive) => {
389                archive.extract_to(options.password, |meta| open(&rar13_meta(meta)))
390            }
391            Self::Rar15To40(archive) => {
392                archive.extract_to(options, |meta| open(&rar15_40_meta(meta)))
393            }
394            Self::Rar50Plus(archive) => archive.extract_to(options, |meta| open(&rar50_meta(meta))),
395        }
396    }
397
398    /// Extracts independent non-solid members in parallel, buffering decoded
399    /// file bytes before replaying writes in archive order.
400    ///
401    /// Solid archives, split members, multivolume sets, and RAR 1.3/1.4
402    /// archives use the regular streaming extractor.
403    pub fn extract_to_parallel_buffered<F>(&self, password: Option<&[u8]>, open: F) -> Result<()>
404    where
405        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
406    {
407        self.extract_to_parallel_buffered_with_options(read_options(password), open)
408    }
409
410    /// Extracts independent non-solid members in parallel with read options.
411    pub fn extract_to_parallel_buffered_with_options<F>(
412        &self,
413        options: ArchiveReadOptions<'_>,
414        mut open: F,
415    ) -> Result<()>
416    where
417        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
418    {
419        match self {
420            Self::Rar13(archive) => {
421                archive.extract_to(options.password, |meta| open(&rar13_meta(meta)))
422            }
423            Self::Rar15To40(archive) => {
424                archive.extract_to_parallel_buffered(options, |meta| open(&rar15_40_meta(meta)))
425            }
426            Self::Rar50Plus(archive) => {
427                archive.extract_to_parallel_buffered(options, |meta| open(&rar50_meta(meta)))
428            }
429        }
430    }
431
432    /// Returns one member's decoded bytes, or `None` when the archive has no
433    /// file of that name.
434    ///
435    /// Extraction runs over the whole archive either way, because a solid
436    /// member is only decodable after the ones before it. Reading several
437    /// members from a solid archive one call at a time therefore costs as many
438    /// full passes; use [`extract_to`](Self::extract_to) to take them all in
439    /// one.
440    pub fn read_member(&self, name: &[u8], password: Option<&[u8]>) -> Result<Option<Vec<u8>>> {
441        // The closure gives the writer away rather than returning bytes, so the
442        // member arrives through a buffer shared with it.
443        let collected = std::sync::Arc::new(std::sync::Mutex::new(None::<Vec<u8>>));
444        self.extract_to_parallel_buffered(password, |meta| {
445            if meta.name != name || meta.is_directory {
446                return Ok(Box::new(std::io::sink()) as Box<dyn Write>);
447            }
448            let sink = SharedBuffer(std::sync::Arc::clone(&collected));
449            *sink.lock() = Some(Vec::new());
450            Ok(Box::new(sink) as Box<dyn Write>)
451        })?;
452        let taken = collected
453            .lock()
454            .unwrap_or_else(|poisoned| poisoned.into_inner())
455            .take();
456        Ok(taken)
457    }
458
459    /// Decodes every member and discards the bytes, so a bad checksum or a
460    /// wrong password is reported and nothing is written.
461    pub fn test(&self, password: Option<&[u8]>) -> Result<()> {
462        self.extract_to_parallel_buffered(password, |_| {
463            Ok(Box::new(std::io::sink()) as Box<dyn Write>)
464        })
465    }
466
467    /// The archive comment, decrypting it when the archive is RAR 5 or later
468    /// and its headers are encrypted.
469    pub fn comment(&self, password: Option<&[u8]>) -> Result<Option<Vec<u8>>> {
470        match self {
471            Self::Rar13(archive) => archive.archive_comment(),
472            Self::Rar15To40(archive) => archive.archive_comment(),
473            Self::Rar50Plus(archive) => archive.archive_comment_with_password(password),
474        }
475    }
476
477    /// Returns full repaired archive bytes using the archive's embedded
478    /// recovery records.
479    pub fn repair_recovery(&self) -> Result<Vec<u8>> {
480        let mut repaired = Vec::new();
481        self.repair_recovery_to(&mut repaired)?;
482        Ok(repaired)
483    }
484
485    /// Streams full repaired archive bytes to `writer` using embedded recovery
486    /// records.
487    pub fn repair_recovery_to(&self, writer: &mut dyn Write) -> Result<()> {
488        match self {
489            Self::Rar15To40(archive) => {
490                writer.write_all(&archive.repair_protect_head()?)?;
491                Ok(())
492            }
493            Self::Rar50Plus(archive) => archive.repair_recovery_to(writer),
494            Self::Rar13(_) => Err(Error::UnsupportedFamilyFeature {
495                family: ArchiveFamily::Rar13,
496                feature: "recovery repair for RAR 1.3/1.4 archives",
497            }),
498        }
499    }
500
501    /// Returns the concrete RAR 1.3/1.4 archive when this archive has that family.
502    pub fn as_rar13(&self) -> Option<&rar13::Archive> {
503        match self {
504            Self::Rar13(archive) => Some(archive),
505            Self::Rar15To40(_) => None,
506            Self::Rar50Plus(_) => None,
507        }
508    }
509
510    /// Returns the concrete RAR 1.5 through RAR 4.x archive when applicable.
511    pub fn as_rar15_40(&self) -> Option<&rar15_40::Archive> {
512        match self {
513            Self::Rar13(_) => None,
514            Self::Rar15To40(archive) => Some(archive),
515            Self::Rar50Plus(_) => None,
516        }
517    }
518
519    /// Returns the concrete RAR 5.0 or later archive when applicable.
520    pub fn as_rar50(&self) -> Option<&rar50::Archive> {
521        match self {
522            Self::Rar13(_) | Self::Rar15To40(_) => None,
523            Self::Rar50Plus(archive) => Some(archive),
524        }
525    }
526}
527
528fn rar13_member(entry: &rar13::Entry) -> ArchiveMember {
529    ArchiveMember {
530        meta: ArchiveMemberMeta {
531            family: ArchiveFamily::Rar13,
532            name: entry.name.clone(),
533            packed_size: u64::from(entry.header.pack_size),
534            unpacked_size: u64::from(entry.header.unp_size),
535            file_time: Some(entry.header.file_time),
536            file_attr: u64::from(entry.header.file_attr),
537            host_os: None,
538            is_directory: entry.is_directory(),
539            is_encrypted: entry.is_encrypted(),
540            is_stored: entry.is_stored(),
541            is_split_before: entry.is_split_before(),
542            is_split_after: entry.is_split_after(),
543        },
544        detail: ArchiveMemberDetail::Rar13 {
545            method: entry.header.method,
546            unpack_version: entry.header.unp_ver,
547            file_checksum: entry.header.file_crc,
548            has_file_comment: entry.has_file_comment(),
549        },
550    }
551}
552
553fn rar15_40_member(file: &rar15_40::FileHeader) -> ArchiveMember {
554    ArchiveMember {
555        meta: ArchiveMemberMeta {
556            family: ArchiveFamily::Rar15To40,
557            name: file.name.clone(),
558            packed_size: file.pack_size,
559            unpacked_size: file.unp_size,
560            file_time: Some(file.file_time),
561            file_attr: u64::from(file.attr),
562            host_os: Some(u64::from(file.host_os)),
563            is_directory: file.is_directory(),
564            is_encrypted: file.is_encrypted(),
565            is_stored: file.is_stored(),
566            is_split_before: file.is_split_before(),
567            is_split_after: file.is_split_after(),
568        },
569        detail: ArchiveMemberDetail::Rar15To40 {
570            method: file.method,
571            unpack_version: file.unp_ver,
572            crc32: file.file_crc,
573            solid: file.is_solid(),
574            salt: file.salt,
575            has_file_comment: file.has_file_comment(),
576        },
577    }
578}
579
580fn rar50_member(file: &rar50::FileHeader) -> ArchiveMember {
581    ArchiveMember {
582        meta: ArchiveMemberMeta {
583            family: ArchiveFamily::Rar50Plus,
584            name: file.name.clone(),
585            packed_size: file.packed_size(),
586            unpacked_size: file.unpacked_size,
587            file_time: file.mtime,
588            file_attr: file.attributes,
589            host_os: Some(file.host_os),
590            is_directory: file.is_directory(),
591            is_encrypted: file.encrypted,
592            is_stored: file.is_stored(),
593            is_split_before: file.is_split_before(),
594            is_split_after: file.is_split_after(),
595        },
596        detail: ArchiveMemberDetail::Rar50Plus {
597            compression_info: file.compression_info,
598            crc32: file.data_crc32,
599            hash: file.hash.as_ref().map(rar50_member_hash),
600        },
601    }
602}
603
604fn rar50_member_hash(hash: &rar50::FileHash) -> ArchiveMemberHash {
605    match hash.hash_type {
606        0 if hash.data.len() == 32 => {
607            let mut data = [0; 32];
608            data.copy_from_slice(&hash.data);
609            ArchiveMemberHash::Blake2sp(data)
610        }
611        _ => ArchiveMemberHash::Other {
612            hash_type: hash.hash_type,
613            data: hash.data.clone(),
614        },
615    }
616}
617
618#[derive(Debug, Clone, Copy, Default)]
619#[non_exhaustive]
620/// Archive reader facade with signature-based dispatch.
621pub struct ArchiveReader;
622
623impl ArchiveReader {
624    /// Detects the archive signature in a byte slice.
625    pub fn detect(input: &[u8]) -> Result<ArchiveSignature> {
626        detect_archive_family(input).ok_or(Error::UnsupportedSignature)
627    }
628
629    /// Parses an archive from memory with default read options.
630    pub fn read(input: &[u8]) -> Result<Archive> {
631        Self::read_with_options(input, ArchiveReadOptions::default())
632    }
633
634    /// Parses an archive from an owned memory buffer with default read options.
635    pub fn read_owned(input: Vec<u8>) -> Result<Archive> {
636        Self::read_owned_with_options(input, ArchiveReadOptions::default())
637    }
638
639    /// Parses an archive from memory using explicit read options.
640    pub fn read_with_options(input: &[u8], options: ArchiveReadOptions<'_>) -> Result<Archive> {
641        let signature =
642            find_archive_start(input, SFX_SCAN_LIMIT).ok_or(Error::UnsupportedSignature)?;
643        match signature.family {
644            ArchiveFamily::Rar13 => Ok(Archive::Rar13(rar13::Archive::parse(input)?)),
645            ArchiveFamily::Rar15To40 => Ok(Archive::Rar15To40(
646                rar15_40::Archive::parse_with_options(input, options)?,
647            )),
648            ArchiveFamily::Rar50Plus => Ok(Archive::Rar50Plus(rar50::Archive::parse_with_options(
649                input, options,
650            )?)),
651        }
652    }
653
654    /// Parses an archive from an owned memory buffer using explicit read options.
655    pub fn read_owned_with_options(
656        input: Vec<u8>,
657        options: ArchiveReadOptions<'_>,
658    ) -> Result<Archive> {
659        let signature =
660            find_archive_start(&input, SFX_SCAN_LIMIT).ok_or(Error::UnsupportedSignature)?;
661        match signature.family {
662            ArchiveFamily::Rar13 => Ok(Archive::Rar13(rar13::Archive::parse_owned(input)?)),
663            ArchiveFamily::Rar15To40 => Ok(Archive::Rar15To40(
664                rar15_40::Archive::parse_owned_with_options(input, options)?,
665            )),
666            ArchiveFamily::Rar50Plus => Ok(Archive::Rar50Plus(
667                rar50::Archive::parse_owned_with_options(input, options)?,
668            )),
669        }
670    }
671
672    /// Parses an archive from a path with default read options.
673    pub fn read_path(path: impl AsRef<Path>) -> Result<Archive> {
674        Self::read_path_with_options(path, ArchiveReadOptions::default())
675    }
676
677    /// Parses an archive from a path using explicit read options.
678    pub fn read_path_with_options(
679        path: impl AsRef<Path>,
680        options: ArchiveReadOptions<'_>,
681    ) -> Result<Archive> {
682        let path = path.as_ref();
683        let mut file = std::fs::File::open(path)?;
684        let len = file.metadata()?.len();
685        let mut scan = vec![0; len.min(SFX_SCAN_LIMIT as u64) as usize];
686        file.read_exact(&mut scan)?;
687        let signature =
688            find_archive_start(&scan, SFX_SCAN_LIMIT).ok_or(Error::UnsupportedSignature)?;
689        match signature.family {
690            ArchiveFamily::Rar13 => Ok(Archive::Rar13(rar13::Archive::parse_path_with_signature(
691                path, signature,
692            )?)),
693            ArchiveFamily::Rar15To40 => Ok(Archive::Rar15To40(
694                rar15_40::Archive::parse_path_with_signature(path, signature, options)?,
695            )),
696            ArchiveFamily::Rar50Plus => Ok(Archive::Rar50Plus(
697                rar50::Archive::parse_path_with_signature(path, signature, options)?,
698            )),
699        }
700    }
701}
702
703fn read_options(password: Option<&[u8]>) -> ArchiveReadOptions<'_> {
704    match password {
705        Some(password) => ArchiveReadOptions::with_password(password),
706        None => ArchiveReadOptions::new(),
707    }
708}
709
710/// Streams a multivolume archive set to caller-provided writers.
711pub fn extract_volumes_to<F>(archives: &[Archive], password: Option<&[u8]>, open: F) -> Result<()>
712where
713    F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
714{
715    extract_volumes_to_with_options(archives, read_options(password), open)
716}
717
718/// Streams a multivolume archive set to caller-provided writers with read options.
719pub fn extract_volumes_to_with_options<F>(
720    archives: &[Archive],
721    options: ArchiveReadOptions<'_>,
722    mut open: F,
723) -> Result<()>
724where
725    F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
726{
727    let Some(first) = archives.first() else {
728        return Err(Error::InvalidHeader("volume set is empty"));
729    };
730
731    match first.family() {
732        ArchiveFamily::Rar13 => {
733            let typed = rar13_volumes(archives)?;
734            rar13::extract_volumes_to(&typed, options.password, |meta| open(&rar13_meta(meta)))
735        }
736        ArchiveFamily::Rar15To40 => {
737            let typed = rar15_40_volumes(archives)?;
738            rar15_40::extract_volumes_to(&typed, options, |meta| open(&rar15_40_meta(meta)))
739        }
740        ArchiveFamily::Rar50Plus => {
741            let typed = rar50_volumes(archives)?;
742            rar50::extract_volumes_to(&typed, options, |meta| open(&rar50_meta(meta)))
743        }
744    }
745}
746
747fn rar13_meta(meta: &rar13::ExtractedEntryMeta) -> ExtractedEntryMeta {
748    ExtractedEntryMeta {
749        name: meta.name.clone(),
750        file_time: meta.file_time,
751        file_attr: u64::from(meta.file_attr),
752        is_directory: meta.is_directory,
753    }
754}
755
756fn rar15_40_meta(meta: &rar15_40::ExtractedEntryMeta) -> ExtractedEntryMeta {
757    ExtractedEntryMeta {
758        name: meta.name.clone(),
759        file_time: meta.file_time,
760        file_attr: u64::from(meta.attr),
761        is_directory: meta.is_directory,
762    }
763}
764
765fn rar50_meta(meta: &rar50::ExtractedEntryMeta) -> ExtractedEntryMeta {
766    ExtractedEntryMeta {
767        name: meta.name.clone(),
768        file_time: meta.file_time,
769        file_attr: meta.attr,
770        is_directory: meta.is_directory,
771    }
772}
773
774fn rar13_volumes(archives: &[Archive]) -> Result<Vec<rar13::Archive>> {
775    archives
776        .iter()
777        .map(|archive| match archive {
778            Archive::Rar13(archive) => Ok(archive.clone()),
779            Archive::Rar15To40(_) | Archive::Rar50Plus(_) => {
780                Err(Error::InvalidHeader("mixed archive families in volume set"))
781            }
782        })
783        .collect()
784}
785
786fn rar15_40_volumes(archives: &[Archive]) -> Result<Vec<rar15_40::Archive>> {
787    archives
788        .iter()
789        .map(|archive| match archive {
790            Archive::Rar15To40(archive) => Ok(archive.clone()),
791            Archive::Rar13(_) | Archive::Rar50Plus(_) => {
792                Err(Error::InvalidHeader("mixed archive families in volume set"))
793            }
794        })
795        .collect()
796}
797
798fn rar50_volumes(archives: &[Archive]) -> Result<Vec<rar50::Archive>> {
799    archives
800        .iter()
801        .map(|archive| match archive {
802            Archive::Rar50Plus(archive) => Ok(archive.clone()),
803            Archive::Rar13(_) | Archive::Rar15To40(_) => {
804                Err(Error::InvalidHeader("mixed archive families in volume set"))
805            }
806        })
807        .collect()
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use std::cell::RefCell;
814    use std::path::{Path, PathBuf};
815    use std::rc::Rc;
816
817    struct CollectWriter {
818        data: Rc<RefCell<Vec<u8>>>,
819    }
820
821    #[derive(Debug, Clone, PartialEq, Eq)]
822    struct CollectedEntry {
823        name: Vec<u8>,
824        data: Vec<u8>,
825        file_time: u32,
826        file_attr: u64,
827        is_directory: bool,
828    }
829
830    fn deterministic_noise(len: usize) -> Vec<u8> {
831        let mut state = 0x1234_5678u32;
832        (0..len)
833            .map(|_| {
834                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
835                (state >> 24) as u8
836            })
837            .collect()
838    }
839
840    fn rar15_40_fixture(name: &str) -> PathBuf {
841        Path::new(env!("CARGO_MANIFEST_DIR"))
842            .join("tests/fixtures/rar15_40")
843            .join(name)
844    }
845
846    #[test]
847    fn extracted_entry_meta_exposes_raw_and_lossy_names() {
848        let meta = ExtractedEntryMeta {
849            name: vec![0xff, b'.', b't', b'x', b't'],
850            file_time: 0,
851            file_attr: 0,
852            is_directory: false,
853        };
854
855        assert_eq!(meta.name_bytes(), [0xff, b'.', b't', b'x', b't']);
856        assert_eq!(meta.name_lossy(), "\u{fffd}.txt");
857    }
858
859    impl Write for CollectWriter {
860        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
861            self.data.borrow_mut().extend_from_slice(buf);
862            Ok(buf.len())
863        }
864
865        fn flush(&mut self) -> std::io::Result<()> {
866            Ok(())
867        }
868    }
869
870    fn collect_extract(archive: &Archive, password: Option<&[u8]>) -> Result<Vec<CollectedEntry>> {
871        let entries = RefCell::new(Vec::new());
872        archive.extract_to(password, |meta| {
873            let data = Rc::new(RefCell::new(Vec::new()));
874            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
875            Ok(Box::new(CollectWriter { data }))
876        })?;
877        Ok(entries
878            .into_inner()
879            .into_iter()
880            .map(|(meta, data)| CollectedEntry {
881                name: meta.name,
882                data: data.borrow().clone(),
883                file_time: meta.file_time,
884                file_attr: meta.file_attr,
885                is_directory: meta.is_directory,
886            })
887            .collect())
888    }
889
890    fn collect_rar15_40(archive: &rar15_40::Archive) -> Result<Vec<CollectedEntry>> {
891        let entries = RefCell::new(Vec::new());
892        archive.extract_to(ArchiveReadOptions::default(), |meta| {
893            let data = Rc::new(RefCell::new(Vec::new()));
894            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
895            Ok(Box::new(CollectWriter { data }))
896        })?;
897        Ok(entries
898            .into_inner()
899            .into_iter()
900            .map(|(meta, data)| CollectedEntry {
901                name: meta.name,
902                data: data.borrow().clone(),
903                file_time: meta.file_time,
904                file_attr: u64::from(meta.attr),
905                is_directory: meta.is_directory,
906            })
907            .collect())
908    }
909
910    fn collect_rar15_40_volumes(
911        archives: &[rar15_40::Archive],
912        password: Option<&[u8]>,
913    ) -> Result<Vec<CollectedEntry>> {
914        let entries = RefCell::new(Vec::new());
915        rar15_40::extract_volumes_to(archives, read_options(password), |meta| {
916            let data = Rc::new(RefCell::new(Vec::new()));
917            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
918            Ok(Box::new(CollectWriter { data }))
919        })?;
920        Ok(entries
921            .into_inner()
922            .into_iter()
923            .map(|(meta, data)| CollectedEntry {
924                name: meta.name,
925                data: data.borrow().clone(),
926                file_time: meta.file_time,
927                file_attr: u64::from(meta.attr),
928                is_directory: meta.is_directory,
929            })
930            .collect())
931    }
932
933    fn collect_rar50_volumes(
934        archives: &[rar50::Archive],
935        password: Option<&[u8]>,
936    ) -> Result<Vec<CollectedEntry>> {
937        let entries = RefCell::new(Vec::new());
938        rar50::extract_volumes_to(archives, read_options(password), |meta| {
939            let data = Rc::new(RefCell::new(Vec::new()));
940            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
941            Ok(Box::new(CollectWriter { data }))
942        })?;
943        Ok(entries
944            .into_inner()
945            .into_iter()
946            .map(|(meta, data)| CollectedEntry {
947                name: meta.name,
948                data: data.borrow().clone(),
949                file_time: meta.file_time,
950                file_attr: meta.attr,
951                is_directory: meta.is_directory,
952            })
953            .collect())
954    }
955
956    fn collect_rar50_file(
957        archive: &rar50::Archive,
958        file: &rar50::FileHeader,
959    ) -> Result<CollectedEntry> {
960        let meta = file.metadata();
961        let data = Rc::new(RefCell::new(Vec::new()));
962        file.write_to(
963            archive,
964            None,
965            &mut CollectWriter {
966                data: Rc::clone(&data),
967            },
968        )?;
969        let data = data.borrow().clone();
970        Ok(CollectedEntry {
971            name: meta.name,
972            data,
973            file_time: meta.file_time,
974            file_attr: meta.attr,
975            is_directory: meta.is_directory,
976        })
977    }
978
979    fn rar13_options(target: ArchiveVersion) -> rar13::WriterOptions {
980        rar13::WriterOptions::new(target, FeatureSet::store_only())
981    }
982
983    fn rar15_options(target: ArchiveVersion) -> rar15_40::WriterOptions {
984        rar15_options_with_features(target, FeatureSet::store_only())
985    }
986
987    fn rar15_options_with_features(
988        target: ArchiveVersion,
989        features: FeatureSet,
990    ) -> rar15_40::WriterOptions {
991        rar15_40::WriterOptions::new(target, features)
992    }
993
994    fn rar50_options(target: ArchiveVersion) -> rar50::WriterOptions {
995        rar50_options_with_features(target, FeatureSet::store_only())
996    }
997
998    fn rar50_options_with_features(
999        target: ArchiveVersion,
1000        features: FeatureSet,
1001    ) -> rar50::WriterOptions {
1002        rar50::WriterOptions::new(target, features)
1003    }
1004
1005    /// Builds a member from bytes the test already holds.
1006    fn rar50_entry(name: &[u8], data: &[u8]) -> rar50::ArchiveEntry {
1007        rar50::ArchiveEntry::new(
1008            name.to_vec(),
1009            EntrySource::from_bytes(std::sync::Arc::<[u8]>::from(data.to_vec())),
1010        )
1011    }
1012
1013    fn write_rar50_volume_set(
1014        entries: &[rar50::ArchiveEntry],
1015        options: rar50::WriterOptions,
1016        max_payload_per_volume: u64,
1017        recovery_percent: Option<u64>,
1018    ) -> Vec<Vec<u8>> {
1019        let mut sink = rar50::CollectedVolumes::new();
1020        rar50::write_streaming_volumes_to(
1021            entries,
1022            options,
1023            rar50::ArchiveExtras::default().with_recovery_percent(recovery_percent),
1024            max_payload_per_volume,
1025            &mut sink,
1026            &WriterResources::default(),
1027        )
1028        .unwrap();
1029        sink.take()
1030    }
1031
1032    fn write_rar29_filter(
1033        options: rar15_40::WriterOptions,
1034        entries: &[rar15_40::FileEntry<'_>],
1035        kind: rar15_40::FilterKind,
1036    ) -> Result<Vec<u8>> {
1037        rar15_40::write_rar29_compressed_archive_with_filter_policy(
1038            entries,
1039            options,
1040            rar15_40::FilterPolicy::Explicit(rar15_40::FilterSpec::whole(kind)),
1041        )
1042    }
1043
1044    fn write_rar29_filter_range(
1045        options: rar15_40::WriterOptions,
1046        entries: &[rar15_40::FileEntry<'_>],
1047        kind: rar15_40::FilterKind,
1048        range: std::ops::Range<usize>,
1049    ) -> Result<Vec<u8>> {
1050        rar15_40::write_rar29_compressed_archive_with_filter_policy(
1051            entries,
1052            options,
1053            rar15_40::FilterPolicy::Explicit(rar15_40::FilterSpec::range(kind, range)),
1054        )
1055    }
1056
1057    fn assert_rar50_volume_recovery_records(archives: &[rar50::Archive], percent: u64) {
1058        assert!(archives.iter().all(|archive| archive.main.is_volume()));
1059        assert!(archives
1060            .iter()
1061            .all(|archive| archive.main.has_recovery_record()));
1062        for archive in archives {
1063            let service = archive.services().next().unwrap();
1064            assert_eq!(service.name, b"RR");
1065            assert_eq!(service.recovery_record().unwrap().unwrap().percent, percent);
1066            let data = collect_rar50_file(archive, service).unwrap().data;
1067            assert!(data.starts_with(b"{RB}"));
1068            assert_eq!(
1069                u32::from_le_bytes(data[0x0c..0x10].try_into().unwrap()) as usize,
1070                data.len()
1071            );
1072        }
1073    }
1074
1075    #[test]
1076    fn direct_writer_creates_rar15_stored_archive() {
1077        let bytes = rar15_40::write_stored_archive(
1078            &[rar15_40::StoredEntry {
1079                name: b"hello.txt",
1080                data: b"hello via facade\n",
1081                file_time: 0,
1082                file_attr: 0x20,
1083                host_os: 3,
1084                password: None,
1085                file_comment: None,
1086            }],
1087            rar15_options(ArchiveVersion::Rar15),
1088        )
1089        .unwrap();
1090
1091        let archive = ArchiveReader::read(&bytes).unwrap();
1092        assert_eq!(archive.family(), ArchiveFamily::Rar15To40);
1093        let extracted = collect_extract(&archive, None).unwrap();
1094        assert_eq!(extracted.len(), 1);
1095        assert_eq!(extracted[0].data, b"hello via facade\n");
1096    }
1097
1098    #[test]
1099    fn archive_reader_accepts_owned_buffers_without_changing_dispatch() {
1100        let rar13_bytes = rar13::write_stored_archive(
1101            &[rar13::StoredEntry {
1102                name: b"old.txt",
1103                data: b"owned rar13\n",
1104                file_time: 0,
1105                file_attr: 0x20,
1106                password: None,
1107                file_comment: None,
1108            }],
1109            rar13_options(ArchiveVersion::Rar14),
1110        )
1111        .unwrap();
1112        let rar13_archive = ArchiveReader::read_owned(rar13_bytes).unwrap();
1113        assert_eq!(rar13_archive.family(), ArchiveFamily::Rar13);
1114        assert_eq!(
1115            collect_extract(&rar13_archive, None).unwrap()[0].data,
1116            b"owned rar13\n"
1117        );
1118
1119        let rar15_bytes = rar15_40::write_stored_archive(
1120            &[rar15_40::StoredEntry {
1121                name: b"mid.txt",
1122                data: b"owned rar15\n",
1123                file_time: 0,
1124                file_attr: 0x20,
1125                host_os: 3,
1126                password: None,
1127                file_comment: None,
1128            }],
1129            rar15_options(ArchiveVersion::Rar15),
1130        )
1131        .unwrap();
1132        let rar15_archive = ArchiveReader::read_owned(rar15_bytes).unwrap();
1133        assert_eq!(rar15_archive.family(), ArchiveFamily::Rar15To40);
1134        assert_eq!(
1135            collect_extract(&rar15_archive, None).unwrap()[0].data,
1136            b"owned rar15\n"
1137        );
1138
1139        let rar50_bytes =
1140            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
1141                .entries(
1142                    [rar50_entry(b"new.txt", b"owned rar50\n")
1143                        .with_attributes(0x20)
1144                        .with_host_os(3)]
1145                    .to_vec(),
1146                )
1147                .finish()
1148                .unwrap();
1149        let rar50_archive = ArchiveReader::read_owned(rar50_bytes).unwrap();
1150        assert_eq!(rar50_archive.family(), ArchiveFamily::Rar50Plus);
1151        assert_eq!(
1152            collect_extract(&rar50_archive, None).unwrap()[0].data,
1153            b"owned rar50\n"
1154        );
1155    }
1156
1157    #[test]
1158    fn direct_writer_keeps_rar13_methods_version_typed() {
1159        let err =
1160            rar13::write_stored_archive(&[], rar13_options(ArchiveVersion::Rar15)).unwrap_err();
1161
1162        assert!(matches!(
1163            err,
1164            Error::UnsupportedVersion(ArchiveVersion::Rar15)
1165        ));
1166    }
1167
1168    #[test]
1169    fn archive_members_exposes_rar13_common_metadata_and_typed_detail() {
1170        let bytes = rar13::write_stored_archive(
1171            &[rar13::StoredEntry {
1172                name: b"old.txt",
1173                data: b"old rar member",
1174                file_time: 0x1234_5678,
1175                file_attr: 0x20,
1176                password: None,
1177                file_comment: Some(b"note"),
1178            }],
1179            rar13_options(ArchiveVersion::Rar14),
1180        )
1181        .unwrap();
1182
1183        let archive = ArchiveReader::read(&bytes).unwrap();
1184        let members: Vec<_> = archive.members().collect();
1185
1186        assert_eq!(members.len(), 1);
1187        assert_eq!(members[0].meta.family, ArchiveFamily::Rar13);
1188        assert_eq!(members[0].meta.name, b"old.txt");
1189        assert_eq!(members[0].meta.name_bytes(), b"old.txt");
1190        assert_eq!(members[0].meta.name_lossy(), "old.txt");
1191        assert_eq!(members[0].meta.packed_size, b"old rar member".len() as u64);
1192        assert_eq!(
1193            members[0].meta.unpacked_size,
1194            b"old rar member".len() as u64
1195        );
1196        assert_eq!(members[0].meta.file_time, Some(0x1234_5678));
1197        assert_eq!(members[0].meta.file_attr, 0x20);
1198        assert_eq!(members[0].meta.host_os, None);
1199        assert!(members[0].meta.is_stored);
1200        assert!(!members[0].meta.is_encrypted);
1201        assert!(!members[0].meta.is_split_before);
1202        assert!(!members[0].meta.is_split_after);
1203        assert!(matches!(
1204            members[0].detail,
1205            ArchiveMemberDetail::Rar13 {
1206                method: 0,
1207                unpack_version: _,
1208                file_checksum: _,
1209                has_file_comment: true,
1210            }
1211        ));
1212    }
1213
1214    #[test]
1215    fn archive_members_exposes_rar15_40_common_metadata_and_typed_detail() {
1216        let features = FeatureSet::store_only();
1217        let payload = b"rar 2.9 member metadata ".repeat(32);
1218        let bytes = rar15_40::write_compressed_archive(
1219            &[rar15_40::FileEntry {
1220                name: b"newer.txt",
1221                data: &payload,
1222                file_time: 0x0102_0304,
1223                file_attr: 0x20,
1224                host_os: 2,
1225                password: None,
1226                file_comment: Some(b"rar29 note"),
1227            }],
1228            rar15_options_with_features(ArchiveVersion::Rar29, features),
1229        )
1230        .unwrap();
1231
1232        let archive = ArchiveReader::read(&bytes).unwrap();
1233        let members: Vec<_> = archive.members().collect();
1234
1235        assert_eq!(members.len(), 1);
1236        assert_eq!(members[0].meta.family, ArchiveFamily::Rar15To40);
1237        assert_eq!(members[0].meta.name, b"newer.txt");
1238        assert_eq!(members[0].meta.unpacked_size, payload.len() as u64);
1239        assert_eq!(members[0].meta.file_time, Some(0x0102_0304));
1240        assert_eq!(members[0].meta.file_attr, 0x20);
1241        assert_eq!(members[0].meta.host_os, Some(2));
1242        assert!(!members[0].meta.is_stored);
1243        assert!(!members[0].meta.is_encrypted);
1244        assert!(matches!(
1245            members[0].detail,
1246            ArchiveMemberDetail::Rar15To40 {
1247                method: 0x33 | 0x35,
1248                unpack_version: 29,
1249                crc32: _,
1250                solid: false,
1251                salt: None,
1252                has_file_comment: true,
1253            }
1254        ));
1255    }
1256
1257    #[test]
1258    fn archive_members_exposes_rar50_common_metadata_and_typed_detail() {
1259        let bytes =
1260            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
1261                .entries(
1262                    [rar50_entry(b"five.txt", b"rar 5 member metadata")
1263                        .with_mtime(Some(0x1111_2222))
1264                        .with_attributes(0x1_0000_0020)
1265                        .with_host_os(3)]
1266                    .to_vec(),
1267                )
1268                .finish()
1269                .unwrap();
1270
1271        let archive = ArchiveReader::read(&bytes).unwrap();
1272        let members: Vec<_> = archive.members().collect();
1273
1274        assert_eq!(members.len(), 1);
1275        assert_eq!(members[0].meta.family, ArchiveFamily::Rar50Plus);
1276        assert_eq!(members[0].meta.name, b"five.txt");
1277        assert_eq!(
1278            members[0].meta.packed_size,
1279            b"rar 5 member metadata".len() as u64
1280        );
1281        assert_eq!(
1282            members[0].meta.unpacked_size,
1283            b"rar 5 member metadata".len() as u64
1284        );
1285        assert_eq!(members[0].meta.file_time, Some(0x1111_2222));
1286        assert_eq!(members[0].meta.file_attr, 0x1_0000_0020);
1287        assert_eq!(members[0].meta.host_os, Some(3));
1288        assert!(members[0].meta.is_stored);
1289        assert!(!members[0].meta.is_encrypted);
1290        assert!(matches!(
1291            members[0].detail,
1292            ArchiveMemberDetail::Rar50Plus {
1293                compression_info: _,
1294                crc32: _,
1295                hash: _,
1296            }
1297        ));
1298    }
1299
1300    #[test]
1301    fn extraction_metadata_preserves_rar50_u64_file_attributes() {
1302        let bytes =
1303            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
1304                .entries(
1305                    [rar50_entry(b"wide-attrs.txt", b"wide RAR5 file attributes")
1306                        .with_mtime(Some(0))
1307                        .with_attributes(0x1_0000_0020)
1308                        .with_host_os(3)]
1309                    .to_vec(),
1310                )
1311                .finish()
1312                .unwrap();
1313
1314        let archive = ArchiveReader::read(&bytes).unwrap();
1315        let extracted = collect_extract(&archive, None).unwrap();
1316
1317        assert_eq!(extracted.len(), 1);
1318        assert_eq!(extracted[0].name, b"wide-attrs.txt");
1319        assert_eq!(extracted[0].file_attr, 0x1_0000_0020);
1320    }
1321
1322    #[test]
1323    fn direct_writer_creates_rar15_compressed_archive() {
1324        let bytes = rar15_40::write_compressed_archive(
1325            &[rar15_40::FileEntry {
1326                name: b"text.txt",
1327                data: b"facade compressed facade compressed facade compressed\n",
1328                file_time: 0,
1329                file_attr: 0x20,
1330                host_os: 3,
1331                password: None,
1332                file_comment: None,
1333            }],
1334            rar15_options(ArchiveVersion::Rar15),
1335        )
1336        .unwrap();
1337
1338        let archive = ArchiveReader::read(&bytes).unwrap();
1339        assert_eq!(archive.family(), ArchiveFamily::Rar15To40);
1340        let extracted = collect_extract(&archive, None).unwrap();
1341        assert_eq!(
1342            extracted[0].data,
1343            b"facade compressed facade compressed facade compressed\n"
1344        );
1345    }
1346
1347    #[test]
1348    fn direct_writer_creates_rar29_compressed_archive_with_default_auto_policy() {
1349        let payload =
1350            b"facade rar29 default auto text alpha beta gamma alpha beta gamma\n".repeat(256);
1351        let bytes = rar15_40::write_compressed_archive(
1352            &[rar15_40::FileEntry {
1353                name: b"rar29-default-auto.txt",
1354                data: &payload,
1355                file_time: 0,
1356                file_attr: 0x20,
1357                host_os: 3,
1358                password: None,
1359                file_comment: None,
1360            }],
1361            rar15_options(ArchiveVersion::Rar29),
1362        )
1363        .unwrap();
1364
1365        let archive = ArchiveReader::read(&bytes).unwrap();
1366        let raw = archive.as_rar15_40().unwrap();
1367        // The byte is the level that was asked for, and nothing here asked for
1368        // one, so it is the default 0x33. Which engine answered is signalled in
1369        // the stream, which is why the round trip below is what proves PPMd
1370        // read back. WinRAR stamps 0x34 on the PPMd archive it writes at -m4.
1371        assert_eq!(raw.files().next().unwrap().method, 0x33);
1372        let extracted = collect_extract(&archive, None).unwrap();
1373        assert_eq!(extracted[0].data, payload);
1374    }
1375
1376    #[test]
1377    fn direct_writer_creates_rar29_e8_filtered_compressed_archive() {
1378        let payload = b"\xe8\0\0\0\0facade rar29 e8 filter payload\n".repeat(12);
1379        let bytes = write_rar29_filter(
1380            rar15_options(ArchiveVersion::Rar29),
1381            &[rar15_40::FileEntry {
1382                name: b"rar29-e8.bin",
1383                data: &payload,
1384                file_time: 0,
1385                file_attr: 0x20,
1386                host_os: 3,
1387                password: None,
1388                file_comment: None,
1389            }],
1390            rar15_40::FilterKind::E8,
1391        )
1392        .unwrap();
1393
1394        let archive = ArchiveReader::read(&bytes).unwrap();
1395        let extracted = collect_extract(&archive, None).unwrap();
1396        assert_eq!(extracted[0].data, payload);
1397    }
1398
1399    #[test]
1400    fn direct_writer_creates_rar29_auto_filtered_compressed_archive() {
1401        let payload = b"\xe8\0\0\0\0facade rar29 auto filter payload\n".repeat(12);
1402        let bytes = rar15_40::write_rar29_compressed_archive_with_filter_policy(
1403            &[rar15_40::FileEntry {
1404                name: b"rar29-auto.bin",
1405                data: &payload,
1406                file_time: 0,
1407                file_attr: 0x20,
1408                host_os: 3,
1409                password: None,
1410                file_comment: None,
1411            }],
1412            rar15_options(ArchiveVersion::Rar29),
1413            rar15_40::FilterPolicy::Auto,
1414        )
1415        .unwrap();
1416
1417        let archive = ArchiveReader::read(&bytes).unwrap();
1418        let extracted = collect_extract(&archive, None).unwrap();
1419        assert_eq!(extracted[0].data, payload);
1420    }
1421
1422    #[test]
1423    fn direct_writer_creates_rar29_ppmd_compressed_archive() {
1424        let payload = b"facade rar29 ppmd text payload alpha beta gamma\n".repeat(64);
1425        let bytes = rar15_40::write_rar29_compressed_archive_with_filter_policy(
1426            &[rar15_40::FileEntry {
1427                name: b"rar29-ppmd.txt",
1428                data: &payload,
1429                file_time: 0,
1430                file_attr: 0x20,
1431                host_os: 3,
1432                password: None,
1433                file_comment: None,
1434            }],
1435            rar15_options(ArchiveVersion::Rar29).with_method(rar15_40::Rar29Method::Ppmd),
1436            rar15_40::FilterPolicy::None,
1437        )
1438        .unwrap();
1439
1440        let archive = ArchiveReader::read(&bytes).unwrap();
1441        let raw = archive.as_rar15_40().unwrap();
1442        let file = raw.files().next().unwrap();
1443        // Forcing PPMd does not change the level, so this is still the default
1444        // 0x33; extracting it is what shows PPMd was used.
1445        assert_eq!(file.method, 0x33);
1446        assert_eq!(collect_extract(&archive, None).unwrap()[0].data, payload);
1447    }
1448
1449    #[test]
1450    fn direct_writer_creates_rar29_segmented_e8_filtered_compressed_archive() {
1451        let mut payload = b"facade unfiltered prefix before x86 segment ".to_vec();
1452        let filter_start = payload.len();
1453        payload.extend_from_slice(b"\xe8\0\0\0\0facade segmented e8 filter payload\n");
1454        let filter_end = payload.len();
1455        payload.extend_from_slice(b"facade unfiltered suffix after x86 segment\n");
1456        let bytes = write_rar29_filter_range(
1457            rar15_options(ArchiveVersion::Rar29),
1458            &[rar15_40::FileEntry {
1459                name: b"rar29-segmented-e8.bin",
1460                data: &payload,
1461                file_time: 0,
1462                file_attr: 0x20,
1463                host_os: 3,
1464                password: None,
1465                file_comment: None,
1466            }],
1467            rar15_40::FilterKind::E8,
1468            filter_start..filter_end,
1469        )
1470        .unwrap();
1471
1472        let archive = ArchiveReader::read(&bytes).unwrap();
1473        let extracted = collect_extract(&archive, None).unwrap();
1474        assert_eq!(extracted[0].data, payload);
1475    }
1476
1477    #[test]
1478    fn direct_writer_creates_rar29_solid_e8_filtered_compressed_archive() {
1479        let first = b"\xe8\0\0\0\0facade rar29 solid e8 first payload\n".repeat(12);
1480        let second = b"\xe8\0\0\0\0facade rar29 solid e8 second payload\n".repeat(12);
1481        let mut features = FeatureSet::store_only();
1482        features.solid = true;
1483        let bytes = write_rar29_filter(
1484            rar15_options_with_features(ArchiveVersion::Rar29, features),
1485            &[
1486                rar15_40::FileEntry {
1487                    name: b"rar29-solid-e8-first.bin",
1488                    data: &first,
1489                    file_time: 0,
1490                    file_attr: 0x20,
1491                    host_os: 3,
1492                    password: None,
1493                    file_comment: None,
1494                },
1495                rar15_40::FileEntry {
1496                    name: b"rar29-solid-e8-second.bin",
1497                    data: &second,
1498                    file_time: 0,
1499                    file_attr: 0x20,
1500                    host_os: 3,
1501                    password: None,
1502                    file_comment: None,
1503                },
1504            ],
1505            rar15_40::FilterKind::E8,
1506        )
1507        .unwrap();
1508
1509        let archive = ArchiveReader::read(&bytes).unwrap();
1510        let raw = archive.as_rar15_40().unwrap();
1511        let files: Vec<_> = raw.files().collect();
1512        assert!(raw.main.is_solid());
1513        assert!(!files[0].is_solid());
1514        assert!(files[1].is_solid());
1515        let extracted = collect_extract(&archive, None).unwrap();
1516        assert_eq!(extracted[0].data, first);
1517        assert_eq!(extracted[1].data, second);
1518    }
1519
1520    #[test]
1521    fn direct_writer_creates_rar29_encrypted_e8_filtered_compressed_archive() {
1522        let payload = b"\xe8\0\0\0\0facade rar29 encrypted e8 payload\n".repeat(12);
1523        let features = FeatureSet::store_only();
1524        let bytes = write_rar29_filter(
1525            rar15_options_with_features(ArchiveVersion::Rar29, features),
1526            &[rar15_40::FileEntry {
1527                name: b"rar29-encrypted-e8.bin",
1528                data: &payload,
1529                file_time: 0,
1530                file_attr: 0x20,
1531                host_os: 3,
1532                password: Some(b"password"),
1533                file_comment: None,
1534            }],
1535            rar15_40::FilterKind::E8,
1536        )
1537        .unwrap();
1538
1539        let archive = ArchiveReader::read(&bytes).unwrap();
1540        let raw = archive.as_rar15_40().unwrap();
1541        let file = raw.files().next().unwrap();
1542        assert!(file.is_encrypted());
1543        assert!(file.salt.is_some());
1544        assert!(matches!(
1545            collect_extract(&archive, Some(b"wrong")),
1546            Err(Error::WrongPasswordOrCorruptData)
1547        ));
1548        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
1549        assert_eq!(extracted[0].data, payload);
1550    }
1551
1552    #[test]
1553    fn direct_writer_creates_rar30_header_encrypted_e8_filtered_compressed_archive() {
1554        let payload = b"\xe8\0\0\0\0facade rar30 header encrypted e8 payload\n".repeat(12);
1555        let mut features = FeatureSet::store_only();
1556        features.header_encryption = true;
1557        let bytes = write_rar29_filter(
1558            rar15_options_with_features(ArchiveVersion::Rar30, features),
1559            &[rar15_40::FileEntry {
1560                name: b"rar30-header-encrypted-e8.bin",
1561                data: &payload,
1562                file_time: 0,
1563                file_attr: 0x20,
1564                host_os: 3,
1565                password: Some(b"password"),
1566                file_comment: None,
1567            }],
1568            rar15_40::FilterKind::E8,
1569        )
1570        .unwrap();
1571
1572        assert!(matches!(
1573            ArchiveReader::read(&bytes),
1574            Err(Error::NeedPassword)
1575        ));
1576        let archive = ArchiveReader::read_with_options(
1577            &bytes,
1578            ArchiveReadOptions::with_password(b"password"),
1579        )
1580        .unwrap();
1581        let raw = archive.as_rar15_40().unwrap();
1582        assert!(raw.main.has_encrypted_headers());
1583        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
1584        assert_eq!(extracted[0].data, payload);
1585    }
1586
1587    #[test]
1588    fn direct_writer_creates_rar29_e8e9_filtered_compressed_archive() {
1589        let payload = b"\xe9\0\0\0\0facade rar29 e8e9 filter payload\n".repeat(12);
1590        let bytes = write_rar29_filter(
1591            rar15_options(ArchiveVersion::Rar29),
1592            &[rar15_40::FileEntry {
1593                name: b"rar29-e8e9.bin",
1594                data: &payload,
1595                file_time: 0,
1596                file_attr: 0x20,
1597                host_os: 3,
1598                password: None,
1599                file_comment: None,
1600            }],
1601            rar15_40::FilterKind::E8E9,
1602        )
1603        .unwrap();
1604
1605        let archive = ArchiveReader::read(&bytes).unwrap();
1606        let extracted = collect_extract(&archive, None).unwrap();
1607        assert_eq!(extracted[0].data, payload);
1608    }
1609
1610    #[test]
1611    fn direct_writer_creates_rar29_delta_filtered_compressed_archive() {
1612        let payload: Vec<u8> = (0..384).map(|index| (index * 19 + 5) as u8).collect();
1613        let bytes = write_rar29_filter(
1614            rar15_options(ArchiveVersion::Rar29),
1615            &[rar15_40::FileEntry {
1616                name: b"rar29-delta.bin",
1617                data: &payload,
1618                file_time: 0,
1619                file_attr: 0x20,
1620                host_os: 3,
1621                password: None,
1622                file_comment: None,
1623            }],
1624            rar15_40::FilterKind::Delta { channels: 3 },
1625        )
1626        .unwrap();
1627
1628        let archive = ArchiveReader::read(&bytes).unwrap();
1629        let extracted = collect_extract(&archive, None).unwrap();
1630        assert_eq!(extracted[0].data, payload);
1631    }
1632
1633    #[test]
1634    fn direct_writer_creates_rar29_segmented_delta_filtered_compressed_archive() {
1635        let mut payload = b"facade unfiltered prefix before delta segment ".to_vec();
1636        let filter_start = payload.len();
1637        payload.extend((0..384).map(|index| (index * 19 + 5) as u8));
1638        let filter_end = payload.len();
1639        payload.extend_from_slice(b"facade unfiltered suffix after delta segment\n");
1640        let bytes = write_rar29_filter_range(
1641            rar15_options(ArchiveVersion::Rar29),
1642            &[rar15_40::FileEntry {
1643                name: b"rar29-segmented-delta.bin",
1644                data: &payload,
1645                file_time: 0,
1646                file_attr: 0x20,
1647                host_os: 3,
1648                password: None,
1649                file_comment: None,
1650            }],
1651            rar15_40::FilterKind::Delta { channels: 3 },
1652            filter_start..filter_end,
1653        )
1654        .unwrap();
1655
1656        let archive = ArchiveReader::read(&bytes).unwrap();
1657        let extracted = collect_extract(&archive, None).unwrap();
1658        assert_eq!(extracted[0].data, payload);
1659    }
1660
1661    #[test]
1662    fn direct_writer_creates_rar29_itanium_filtered_compressed_archive() {
1663        let mut payload = vec![0u8; 48];
1664        payload[16] = 22;
1665        payload[21] = 20;
1666        payload.extend_from_slice(b"facade rar29 itanium filter payload\n");
1667        let bytes = write_rar29_filter(
1668            rar15_options(ArchiveVersion::Rar29),
1669            &[rar15_40::FileEntry {
1670                name: b"rar29-itanium.bin",
1671                data: &payload,
1672                file_time: 0,
1673                file_attr: 0x20,
1674                host_os: 3,
1675                password: None,
1676                file_comment: None,
1677            }],
1678            rar15_40::FilterKind::Itanium,
1679        )
1680        .unwrap();
1681
1682        let archive = ArchiveReader::read(&bytes).unwrap();
1683        let extracted = collect_extract(&archive, None).unwrap();
1684        assert_eq!(extracted[0].data, payload);
1685    }
1686
1687    #[test]
1688    fn direct_writer_creates_rar29_segmented_itanium_filtered_compressed_archive() {
1689        let mut payload = b"facade unfiltered prefix before itanium segment ".to_vec();
1690        let filter_start = payload.len();
1691        payload.extend_from_slice(&[0; 48]);
1692        payload[filter_start + 16] = 22;
1693        payload[filter_start + 21] = 20;
1694        payload.extend_from_slice(b"facade segmented itanium filter payload\n");
1695        let filter_end = payload.len();
1696        payload.extend_from_slice(b"facade unfiltered suffix after itanium segment\n");
1697        let bytes = write_rar29_filter_range(
1698            rar15_options(ArchiveVersion::Rar29),
1699            &[rar15_40::FileEntry {
1700                name: b"rar29-segmented-itanium.bin",
1701                data: &payload,
1702                file_time: 0,
1703                file_attr: 0x20,
1704                host_os: 3,
1705                password: None,
1706                file_comment: None,
1707            }],
1708            rar15_40::FilterKind::Itanium,
1709            filter_start..filter_end,
1710        )
1711        .unwrap();
1712
1713        let archive = ArchiveReader::read(&bytes).unwrap();
1714        let extracted = collect_extract(&archive, None).unwrap();
1715        assert_eq!(extracted[0].data, payload);
1716    }
1717
1718    #[test]
1719    fn direct_writer_creates_rar29_rgb_filtered_compressed_archive() {
1720        let width = 12;
1721        let payload: Vec<u8> = (0..96).map(|index| (index * 37 + 17) as u8).collect();
1722        let bytes = write_rar29_filter(
1723            rar15_options(ArchiveVersion::Rar29),
1724            &[rar15_40::FileEntry {
1725                name: b"rar29-rgb.bin",
1726                data: &payload,
1727                file_time: 0,
1728                file_attr: 0x20,
1729                host_os: 3,
1730                password: None,
1731                file_comment: None,
1732            }],
1733            rar15_40::FilterKind::Rgb { width, pos_r: 0 },
1734        )
1735        .unwrap();
1736
1737        let archive = ArchiveReader::read(&bytes).unwrap();
1738        let extracted = collect_extract(&archive, None).unwrap();
1739        assert_eq!(extracted[0].data, payload);
1740    }
1741
1742    #[test]
1743    fn direct_writer_creates_rar29_segmented_rgb_filtered_compressed_archive() {
1744        let width = 12;
1745        let mut payload = b"facade unfiltered prefix before rgb segment ".to_vec();
1746        let filter_start = payload.len();
1747        payload.extend((0..96).map(|index| (index * 37 + 17) as u8));
1748        let filter_end = payload.len();
1749        payload.extend_from_slice(b"facade unfiltered suffix after rgb segment\n");
1750        let bytes = write_rar29_filter_range(
1751            rar15_options(ArchiveVersion::Rar29),
1752            &[rar15_40::FileEntry {
1753                name: b"rar29-segmented-rgb.bin",
1754                data: &payload,
1755                file_time: 0,
1756                file_attr: 0x20,
1757                host_os: 3,
1758                password: None,
1759                file_comment: None,
1760            }],
1761            rar15_40::FilterKind::Rgb { width, pos_r: 0 },
1762            filter_start..filter_end,
1763        )
1764        .unwrap();
1765
1766        let archive = ArchiveReader::read(&bytes).unwrap();
1767        let extracted = collect_extract(&archive, None).unwrap();
1768        assert_eq!(extracted[0].data, payload);
1769    }
1770
1771    #[test]
1772    fn direct_writer_creates_rar29_audio_filtered_compressed_archive() {
1773        let payload: Vec<u8> = (0..160)
1774            .map(|index| (index * 11 + index / 7) as u8)
1775            .collect();
1776        let bytes = write_rar29_filter(
1777            rar15_options(ArchiveVersion::Rar29),
1778            &[rar15_40::FileEntry {
1779                name: b"rar29-audio.bin",
1780                data: &payload,
1781                file_time: 0,
1782                file_attr: 0x20,
1783                host_os: 3,
1784                password: None,
1785                file_comment: None,
1786            }],
1787            rar15_40::FilterKind::Audio { channels: 2 },
1788        )
1789        .unwrap();
1790
1791        let archive = ArchiveReader::read(&bytes).unwrap();
1792        let extracted = collect_extract(&archive, None).unwrap();
1793        assert_eq!(extracted[0].data, payload);
1794    }
1795
1796    #[test]
1797    fn direct_writer_creates_rar29_segmented_audio_filtered_compressed_archive() {
1798        let mut payload = b"facade unfiltered prefix before audio segment ".to_vec();
1799        let filter_start = payload.len();
1800        payload.extend((0..160).map(|index| (index * 11 + index / 7) as u8));
1801        let filter_end = payload.len();
1802        payload.extend_from_slice(b"facade unfiltered suffix after audio segment\n");
1803        let bytes = write_rar29_filter_range(
1804            rar15_options(ArchiveVersion::Rar29),
1805            &[rar15_40::FileEntry {
1806                name: b"rar29-segmented-audio.bin",
1807                data: &payload,
1808                file_time: 0,
1809                file_attr: 0x20,
1810                host_os: 3,
1811                password: None,
1812                file_comment: None,
1813            }],
1814            rar15_40::FilterKind::Audio { channels: 2 },
1815            filter_start..filter_end,
1816        )
1817        .unwrap();
1818
1819        let archive = ArchiveReader::read(&bytes).unwrap();
1820        let extracted = collect_extract(&archive, None).unwrap();
1821        assert_eq!(extracted[0].data, payload);
1822    }
1823
1824    #[test]
1825    fn direct_writer_creates_rar20_compressed_archive() {
1826        let payload = b"facade rar20 literal compressed payload\n".repeat(32);
1827        let bytes = rar15_40::write_compressed_archive(
1828            &[rar15_40::FileEntry {
1829                name: b"rar20.txt",
1830                data: &payload,
1831                file_time: 0,
1832                file_attr: 0x20,
1833                host_os: 3,
1834                password: None,
1835                file_comment: None,
1836            }],
1837            rar15_options(ArchiveVersion::Rar20),
1838        )
1839        .unwrap();
1840
1841        let archive = ArchiveReader::read(&bytes).unwrap();
1842        assert_eq!(archive.family(), ArchiveFamily::Rar15To40);
1843        let raw = archive.as_rar15_40().unwrap();
1844        let file = raw.files().next().unwrap();
1845        assert_eq!(file.unp_ver, 20);
1846        assert_eq!(file.method, 0x33);
1847        assert_eq!(collect_extract(&archive, None).unwrap()[0].data, payload);
1848    }
1849
1850    #[test]
1851    fn direct_writer_creates_rar29_compressed_archive() {
1852        let payload = b"facade rar29 literal compressed payload\n".repeat(32);
1853        let bytes = rar15_40::write_compressed_archive(
1854            &[rar15_40::FileEntry {
1855                name: b"rar29.txt",
1856                data: &payload,
1857                file_time: 0,
1858                file_attr: 0x20,
1859                host_os: 3,
1860                password: None,
1861                file_comment: None,
1862            }],
1863            rar15_options(ArchiveVersion::Rar29),
1864        )
1865        .unwrap();
1866
1867        let archive = ArchiveReader::read(&bytes).unwrap();
1868        assert_eq!(archive.family(), ArchiveFamily::Rar15To40);
1869        let raw = archive.as_rar15_40().unwrap();
1870        let file = raw.files().next().unwrap();
1871        assert_eq!(file.unp_ver, 29);
1872        assert!(matches!(file.method, 0x33 | 0x35));
1873        assert_eq!(collect_extract(&archive, None).unwrap()[0].data, payload);
1874    }
1875
1876    #[test]
1877    fn direct_writer_creates_rar29_solid_compressed_archive() {
1878        let mut features = FeatureSet::store_only();
1879        features.solid = true;
1880        let bytes = rar15_40::write_compressed_archive(
1881            &[
1882                rar15_40::FileEntry {
1883                    name: b"one.txt",
1884                    data: b"facade rar29 solid one alpha beta\n",
1885                    file_time: 0,
1886                    file_attr: 0x20,
1887                    host_os: 3,
1888                    password: None,
1889                    file_comment: None,
1890                },
1891                rar15_40::FileEntry {
1892                    name: b"two.txt",
1893                    data: b"facade rar29 solid two alpha beta\n",
1894                    file_time: 0,
1895                    file_attr: 0x20,
1896                    host_os: 3,
1897                    password: None,
1898                    file_comment: None,
1899                },
1900            ],
1901            rar15_options_with_features(ArchiveVersion::Rar29, features),
1902        )
1903        .unwrap();
1904
1905        let archive = ArchiveReader::read(&bytes).unwrap();
1906        let raw = archive.as_rar15_40().unwrap();
1907        assert!(raw.main.is_solid());
1908        let files: Vec<_> = raw.files().collect();
1909        assert!(!files[0].is_solid());
1910        assert!(files[1].is_solid());
1911        let extracted = collect_extract(&archive, None).unwrap();
1912        assert_eq!(extracted[0].data, b"facade rar29 solid one alpha beta\n");
1913        assert_eq!(extracted[1].data, b"facade rar29 solid two alpha beta\n");
1914    }
1915
1916    #[test]
1917    fn direct_writer_creates_rar20_solid_compressed_archive() {
1918        let mut features = FeatureSet::store_only();
1919        features.solid = true;
1920        let first = b"facade rar20 solid shared line alpha beta gamma\n".repeat(48);
1921        let second = b"facade rar20 solid shared line alpha beta gamma\nsecond\n".repeat(24);
1922        let bytes = rar15_40::write_compressed_archive(
1923            &[
1924                rar15_40::FileEntry {
1925                    name: b"one.txt",
1926                    data: &first,
1927                    file_time: 0,
1928                    file_attr: 0x20,
1929                    host_os: 3,
1930                    password: None,
1931                    file_comment: None,
1932                },
1933                rar15_40::FileEntry {
1934                    name: b"two.txt",
1935                    data: &second,
1936                    file_time: 0,
1937                    file_attr: 0x20,
1938                    host_os: 3,
1939                    password: None,
1940                    file_comment: None,
1941                },
1942            ],
1943            rar15_options_with_features(ArchiveVersion::Rar20, features),
1944        )
1945        .unwrap();
1946
1947        let archive = ArchiveReader::read(&bytes).unwrap();
1948        let raw = archive.as_rar15_40().unwrap();
1949        assert!(raw.main.is_solid());
1950        let files: Vec<_> = raw.files().collect();
1951        assert_eq!(files[0].unp_ver, 20);
1952        assert_eq!(files[1].unp_ver, 20);
1953        assert!(!files[0].is_solid());
1954        assert!(files[1].is_solid());
1955        let extracted = collect_extract(&archive, None).unwrap();
1956        assert_eq!(extracted[0].data, first);
1957        assert_eq!(extracted[1].data, second);
1958    }
1959
1960    #[test]
1961    fn direct_writer_creates_rar15_archive_comment() {
1962        let features = FeatureSet::store_only();
1963        let bytes = rar15_40::write_compressed_archive_with_comment(
1964            &[rar15_40::FileEntry {
1965                name: b"commented.txt",
1966                data: b"facade commented payload\n",
1967                file_time: 0,
1968                file_attr: 0x20,
1969                host_os: 3,
1970                password: None,
1971                file_comment: None,
1972            }],
1973            rar15_options_with_features(ArchiveVersion::Rar15, features),
1974            Some(b"facade note\n"),
1975        )
1976        .unwrap();
1977
1978        let archive = ArchiveReader::read(&bytes).unwrap();
1979        let archive = archive.as_rar15_40().unwrap();
1980        assert_eq!(
1981            archive.archive_comment().unwrap().as_deref(),
1982            Some(&b"facade note\n"[..])
1983        );
1984        assert_eq!(
1985            collect_rar15_40(archive).unwrap()[0].data,
1986            b"facade commented payload\n"
1987        );
1988    }
1989
1990    #[test]
1991    fn direct_writer_creates_rar15_file_comment() {
1992        let features = FeatureSet::store_only();
1993        let bytes = rar15_40::write_stored_archive(
1994            &[rar15_40::StoredEntry {
1995                name: b"file-comment.txt",
1996                data: b"facade file comment payload\n",
1997                file_time: 0,
1998                file_attr: 0x20,
1999                host_os: 3,
2000                password: None,
2001                file_comment: Some(b"facade file note"),
2002            }],
2003            rar15_options_with_features(ArchiveVersion::Rar15, features),
2004        )
2005        .unwrap();
2006
2007        let archive = ArchiveReader::read(&bytes).unwrap();
2008        let archive = archive.as_rar15_40().unwrap();
2009        let file = archive.files().next().unwrap();
2010        assert_eq!(
2011            file.file_comment().unwrap().as_deref(),
2012            Some(&b"facade file note"[..])
2013        );
2014        assert_eq!(
2015            collect_rar15_40(archive).unwrap()[0].data,
2016            b"facade file comment payload\n"
2017        );
2018    }
2019
2020    #[test]
2021    fn direct_writer_creates_rar20_old_style_comments() {
2022        let archive_features = FeatureSet::store_only();
2023        let bytes = rar15_40::write_compressed_archive_with_comment(
2024            &[rar15_40::FileEntry {
2025                name: b"rar20-commented.txt",
2026                data: b"facade rar20 archive comment payload\n",
2027                file_time: 0,
2028                file_attr: 0x20,
2029                host_os: 3,
2030                password: None,
2031                file_comment: None,
2032            }],
2033            rar15_options_with_features(ArchiveVersion::Rar20, archive_features),
2034            Some(b"facade rar20 archive note"),
2035        )
2036        .unwrap();
2037        let archive = ArchiveReader::read(&bytes).unwrap();
2038        let raw = archive.as_rar15_40().unwrap();
2039        assert!(raw.main.has_archive_comment());
2040        assert_eq!(
2041            raw.archive_comment().unwrap().as_deref(),
2042            Some(b"facade rar20 archive note".as_slice())
2043        );
2044
2045        let file_features = FeatureSet::store_only();
2046        let bytes = rar15_40::write_compressed_archive(
2047            &[rar15_40::FileEntry {
2048                name: b"rar20-file-commented.txt",
2049                data: b"facade rar20 file comment payload payload\n",
2050                file_time: 0,
2051                file_attr: 0x20,
2052                host_os: 3,
2053                password: None,
2054                file_comment: Some(b"facade rar20 file note"),
2055            }],
2056            rar15_options_with_features(ArchiveVersion::Rar20, file_features),
2057        )
2058        .unwrap();
2059        let archive = ArchiveReader::read(&bytes).unwrap();
2060        let raw = archive.as_rar15_40().unwrap();
2061        let file = raw.files().next().unwrap();
2062        assert_eq!(file.unp_ver, 20);
2063        assert_eq!(
2064            file.file_comment().unwrap().as_deref(),
2065            Some(b"facade rar20 file note".as_slice())
2066        );
2067    }
2068
2069    #[test]
2070    fn direct_writer_creates_rar29_old_style_comments() {
2071        let archive_features = FeatureSet::store_only();
2072        let bytes = rar15_40::write_compressed_archive_with_comment(
2073            &[rar15_40::FileEntry {
2074                name: b"rar29-commented.txt",
2075                data: b"facade rar29 archive comment payload\n",
2076                file_time: 0,
2077                file_attr: 0x20,
2078                host_os: 3,
2079                password: None,
2080                file_comment: None,
2081            }],
2082            rar15_options_with_features(ArchiveVersion::Rar29, archive_features),
2083            Some(b"facade rar29 archive note"),
2084        )
2085        .unwrap();
2086        let archive = ArchiveReader::read(&bytes).unwrap();
2087        let raw = archive.as_rar15_40().unwrap();
2088        assert!(raw.main.has_archive_comment());
2089        assert_eq!(
2090            raw.archive_comment().unwrap().as_deref(),
2091            Some(b"facade rar29 archive note".as_slice())
2092        );
2093
2094        let file_features = FeatureSet::store_only();
2095        let bytes = rar15_40::write_compressed_archive(
2096            &[rar15_40::FileEntry {
2097                name: b"rar29-file-commented.txt",
2098                data: b"facade rar29 file comment payload payload\n",
2099                file_time: 0,
2100                file_attr: 0x20,
2101                host_os: 3,
2102                password: None,
2103                file_comment: Some(b"facade rar29 file note"),
2104            }],
2105            rar15_options_with_features(ArchiveVersion::Rar29, file_features),
2106        )
2107        .unwrap();
2108        let archive = ArchiveReader::read(&bytes).unwrap();
2109        let raw = archive.as_rar15_40().unwrap();
2110        let file = raw.files().next().unwrap();
2111        assert_eq!(file.unp_ver, 29);
2112        assert_eq!(
2113            file.file_comment().unwrap().as_deref(),
2114            Some(b"facade rar29 file note".as_slice())
2115        );
2116    }
2117
2118    #[test]
2119    fn direct_writer_creates_rar30_newsub_archive_comment() {
2120        let features = FeatureSet::store_only();
2121        let bytes = rar15_40::write_compressed_archive_with_comment(
2122            &[rar15_40::FileEntry {
2123                name: b"rar30-commented.txt",
2124                data: b"facade rar30 NEWSUB archive comment payload\n",
2125                file_time: 0,
2126                file_attr: 0x20,
2127                host_os: 3,
2128                password: None,
2129                file_comment: None,
2130            }],
2131            rar15_options_with_features(ArchiveVersion::Rar30, features),
2132            Some(b"facade rar30 NEWSUB note"),
2133        )
2134        .unwrap();
2135
2136        let archive = ArchiveReader::read(&bytes).unwrap();
2137        let raw = archive.as_rar15_40().unwrap();
2138        assert!(!raw.main.has_archive_comment());
2139        let subblock = raw.new_subs().next().unwrap();
2140        assert_eq!(subblock.kind, rar15_40::NewSubKind::ArchiveComment);
2141        assert_eq!(subblock.file.name, b"CMT");
2142        assert_eq!(
2143            raw.archive_comment().unwrap().as_deref(),
2144            Some(b"facade rar30 NEWSUB note".as_slice())
2145        );
2146    }
2147
2148    #[test]
2149    fn direct_writer_creates_rar15_solid_compressed_archive() {
2150        let mut features = FeatureSet::store_only();
2151        features.solid = true;
2152        let bytes = rar15_40::write_compressed_archive(
2153            &[
2154                rar15_40::FileEntry {
2155                    name: b"one.txt",
2156                    data: b"shared facade prefix one\n",
2157                    file_time: 0,
2158                    file_attr: 0x20,
2159                    host_os: 3,
2160                    password: None,
2161                    file_comment: None,
2162                },
2163                rar15_40::FileEntry {
2164                    name: b"two.txt",
2165                    data: b"shared facade prefix two\n",
2166                    file_time: 0,
2167                    file_attr: 0x20,
2168                    host_os: 3,
2169                    password: None,
2170                    file_comment: None,
2171                },
2172            ],
2173            rar15_options_with_features(ArchiveVersion::Rar15, features),
2174        )
2175        .unwrap();
2176
2177        let archive = ArchiveReader::read(&bytes).unwrap();
2178        let extracted = collect_extract(&archive, None).unwrap();
2179        assert_eq!(extracted[0].data, b"shared facade prefix one\n");
2180        assert_eq!(extracted[1].data, b"shared facade prefix two\n");
2181    }
2182
2183    #[test]
2184    fn direct_writer_creates_rar15_encrypted_compressed_archive() {
2185        let features = FeatureSet::store_only();
2186        let bytes = rar15_40::write_compressed_archive(
2187            &[rar15_40::FileEntry {
2188                name: b"secret.txt",
2189                data: b"facade encrypted payload\n",
2190                file_time: 0,
2191                file_attr: 0x20,
2192                host_os: 3,
2193                password: Some(b"password"),
2194                file_comment: None,
2195            }],
2196            rar15_options_with_features(ArchiveVersion::Rar15, features),
2197        )
2198        .unwrap();
2199
2200        let archive = ArchiveReader::read(&bytes).unwrap();
2201        assert!(matches!(
2202            collect_extract(&archive, None),
2203            Err(Error::NeedPassword)
2204        ));
2205        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2206        assert_eq!(extracted[0].data, b"facade encrypted payload\n");
2207    }
2208
2209    #[test]
2210    fn direct_writer_creates_rar15_stored_volumes() {
2211        let parts = rar15_40::write_stored_volumes(
2212            rar15_40::StoredEntry {
2213                name: b"split.bin",
2214                data: b"abcdefghijklmnopqrstuvwxyz0123456789",
2215                file_time: 0,
2216                file_attr: 0x20,
2217                host_os: 3,
2218                password: None,
2219                file_comment: None,
2220            },
2221            rar15_options(ArchiveVersion::Rar15),
2222            10,
2223        )
2224        .unwrap();
2225        let archives: Vec<_> = parts
2226            .iter()
2227            .map(|part| rar15_40::Archive::parse(part).unwrap())
2228            .collect();
2229        let extracted = collect_rar15_40_volumes(&archives, None).unwrap();
2230
2231        assert_eq!(extracted[0].name, b"split.bin");
2232        assert_eq!(extracted[0].data, b"abcdefghijklmnopqrstuvwxyz0123456789");
2233    }
2234
2235    #[test]
2236    fn direct_writer_creates_rar20_compressed_volumes() {
2237        let data = b"facade rar20 split phrase alpha beta gamma\n".repeat(32);
2238        let parts = rar15_40::write_compressed_volumes(
2239            rar15_40::FileEntry {
2240                name: b"split-rar20.txt",
2241                data: &data,
2242                file_time: 0,
2243                file_attr: 0x20,
2244                host_os: 3,
2245                password: None,
2246                file_comment: None,
2247            },
2248            rar15_options(ArchiveVersion::Rar20),
2249            8,
2250        )
2251        .unwrap();
2252        let archives: Vec<_> = parts
2253            .iter()
2254            .map(|part| rar15_40::Archive::parse(part).unwrap())
2255            .collect();
2256        let first_file = archives[0].files().next().unwrap();
2257        assert_eq!(first_file.unp_ver, 20);
2258        assert!(first_file.is_split_after());
2259
2260        let extracted = collect_rar15_40_volumes(&archives, None).unwrap();
2261        assert_eq!(extracted[0].name, b"split-rar20.txt");
2262        assert_eq!(extracted[0].data, data);
2263    }
2264
2265    #[test]
2266    fn direct_writer_creates_rar29_compressed_volumes() {
2267        let data = b"facade rar29 split phrase alpha beta gamma\n".repeat(32);
2268        let parts = rar15_40::write_compressed_volumes(
2269            rar15_40::FileEntry {
2270                name: b"split-rar29.txt",
2271                data: &data,
2272                file_time: 0,
2273                file_attr: 0x20,
2274                host_os: 3,
2275                password: None,
2276                file_comment: None,
2277            },
2278            rar15_options(ArchiveVersion::Rar29),
2279            8,
2280        )
2281        .unwrap();
2282        let archives: Vec<_> = parts
2283            .iter()
2284            .map(|part| rar15_40::Archive::parse(part).unwrap())
2285            .collect();
2286        let first_file = archives[0].files().next().unwrap();
2287        assert_eq!(first_file.unp_ver, 29);
2288        assert!(first_file.is_split_after());
2289
2290        let extracted = collect_rar15_40_volumes(&archives, None).unwrap();
2291        assert_eq!(extracted[0].name, b"split-rar29.txt");
2292        assert_eq!(extracted[0].data, data);
2293    }
2294
2295    #[test]
2296    fn direct_writer_creates_rar29_encrypted_compressed_volumes() {
2297        let features = FeatureSet::store_only();
2298        let parts = rar15_40::write_compressed_volumes(
2299            rar15_40::FileEntry {
2300                name: b"split-rar29-secret.txt",
2301                data: b"facade rar29 encrypted split facade rar29 encrypted split\n",
2302                file_time: 0,
2303                file_attr: 0x20,
2304                host_os: 3,
2305                password: Some(b"password"),
2306                file_comment: None,
2307            },
2308            rar15_options_with_features(ArchiveVersion::Rar29, features),
2309            8,
2310        )
2311        .unwrap();
2312        let archives: Vec<_> = parts
2313            .iter()
2314            .map(|part| rar15_40::Archive::parse(part).unwrap())
2315            .collect();
2316
2317        assert!(matches!(
2318            collect_rar15_40_volumes(&archives, None),
2319            Err(Error::NeedPassword)
2320        ));
2321        let extracted = collect_rar15_40_volumes(&archives, Some(b"password")).unwrap();
2322        assert_eq!(extracted[0].name, b"split-rar29-secret.txt");
2323        assert_eq!(
2324            extracted[0].data,
2325            b"facade rar29 encrypted split facade rar29 encrypted split\n"
2326        );
2327    }
2328
2329    #[test]
2330    fn direct_writer_creates_rar15_encrypted_compressed_volumes() {
2331        let features = FeatureSet::store_only();
2332        let parts = rar15_40::write_compressed_volumes(
2333            rar15_40::FileEntry {
2334                name: b"split-secret.txt",
2335                data: b"facade encrypted split facade encrypted split\n",
2336                file_time: 0,
2337                file_attr: 0x20,
2338                host_os: 3,
2339                password: Some(b"password"),
2340                file_comment: None,
2341            },
2342            rar15_options_with_features(ArchiveVersion::Rar15, features),
2343            8,
2344        )
2345        .unwrap();
2346        let archives: Vec<_> = parts
2347            .iter()
2348            .map(|part| rar15_40::Archive::parse(part).unwrap())
2349            .collect();
2350
2351        assert!(matches!(
2352            collect_rar15_40_volumes(&archives, None),
2353            Err(Error::NeedPassword)
2354        ));
2355        let extracted = collect_rar15_40_volumes(&archives, Some(b"password")).unwrap();
2356        assert_eq!(extracted[0].name, b"split-secret.txt");
2357        assert_eq!(
2358            extracted[0].data,
2359            b"facade encrypted split facade encrypted split\n"
2360        );
2361    }
2362
2363    #[test]
2364    fn direct_writer_creates_rar30_encrypted_compressed_volumes() {
2365        let features = FeatureSet::store_only();
2366        let parts = rar15_40::write_compressed_volumes(
2367            rar15_40::FileEntry {
2368                name: b"split-rar30-secret.txt",
2369                data: b"facade rar30 encrypted split facade rar30 encrypted split\n",
2370                file_time: 0,
2371                file_attr: 0x20,
2372                host_os: 3,
2373                password: Some(b"password"),
2374                file_comment: None,
2375            },
2376            rar15_options_with_features(ArchiveVersion::Rar30, features),
2377            8,
2378        )
2379        .unwrap();
2380        let archives: Vec<_> = parts
2381            .iter()
2382            .map(|part| rar15_40::Archive::parse(part).unwrap())
2383            .collect();
2384
2385        assert!(matches!(
2386            collect_rar15_40_volumes(&archives, None),
2387            Err(Error::NeedPassword)
2388        ));
2389        let extracted = collect_rar15_40_volumes(&archives, Some(b"password")).unwrap();
2390        assert_eq!(extracted[0].name, b"split-rar30-secret.txt");
2391        assert_eq!(
2392            extracted[0].data,
2393            b"facade rar30 encrypted split facade rar30 encrypted split\n"
2394        );
2395    }
2396
2397    #[test]
2398    fn direct_writer_creates_rar30_header_encrypted_compressed_volumes() {
2399        let mut features = FeatureSet::store_only();
2400        features.header_encryption = true;
2401        let parts = rar15_40::write_compressed_volumes(
2402            rar15_40::FileEntry {
2403                name: b"split-rar30-header-secret.txt",
2404                data: b"facade rar30 header encrypted split facade rar30 header encrypted split\n",
2405                file_time: 0,
2406                file_attr: 0x20,
2407                host_os: 3,
2408                password: Some(b"password"),
2409                file_comment: None,
2410            },
2411            rar15_options_with_features(ArchiveVersion::Rar30, features),
2412            8,
2413        )
2414        .unwrap();
2415        assert!(matches!(
2416            rar15_40::Archive::parse(&parts[0]),
2417            Err(Error::NeedPassword)
2418        ));
2419        let archives: Vec<_> = parts
2420            .iter()
2421            .map(|part| rar15_40::Archive::parse_with_password(part, Some(b"password")).unwrap())
2422            .collect();
2423
2424        let extracted = collect_rar15_40_volumes(&archives, Some(b"password")).unwrap();
2425        assert_eq!(extracted[0].name, b"split-rar30-header-secret.txt");
2426        assert_eq!(
2427            extracted[0].data,
2428            b"facade rar30 header encrypted split facade rar30 header encrypted split\n"
2429        );
2430    }
2431
2432    #[test]
2433    fn direct_writer_creates_rar30_aes_encrypted_compressed_archive() {
2434        let features = FeatureSet::store_only();
2435        let bytes = rar15_40::write_compressed_archive(
2436            &[rar15_40::FileEntry {
2437                name: b"rar30-secret.txt",
2438                data: b"facade rar30 aes encrypted payload\n",
2439                file_time: 0,
2440                file_attr: 0x20,
2441                host_os: 3,
2442                password: Some(b"password"),
2443                file_comment: None,
2444            }],
2445            rar15_options_with_features(ArchiveVersion::Rar30, features),
2446        )
2447        .unwrap();
2448
2449        let archive = ArchiveReader::read(&bytes).unwrap();
2450        assert!(matches!(
2451            collect_extract(&archive, None),
2452            Err(Error::NeedPassword)
2453        ));
2454        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2455        assert_eq!(extracted[0].data, b"facade rar30 aes encrypted payload\n");
2456    }
2457
2458    #[test]
2459    fn direct_writer_creates_rar29_aes_encrypted_compressed_archive() {
2460        let features = FeatureSet::store_only();
2461        let bytes = rar15_40::write_compressed_archive(
2462            &[rar15_40::FileEntry {
2463                name: b"rar29-secret.txt",
2464                data: b"facade rar29 aes encrypted payload\n",
2465                file_time: 0,
2466                file_attr: 0x20,
2467                host_os: 3,
2468                password: Some(b"password"),
2469                file_comment: None,
2470            }],
2471            rar15_options_with_features(ArchiveVersion::Rar29, features),
2472        )
2473        .unwrap();
2474
2475        let archive = ArchiveReader::read(&bytes).unwrap();
2476        let raw = archive.as_rar15_40().unwrap();
2477        let file = raw.files().next().unwrap();
2478        assert_eq!(file.unp_ver, 29);
2479        assert!(file.is_encrypted());
2480        assert!(file.salt.is_some());
2481        assert!(matches!(
2482            collect_extract(&archive, None),
2483            Err(Error::NeedPassword)
2484        ));
2485        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2486        assert_eq!(extracted[0].data, b"facade rar29 aes encrypted payload\n");
2487    }
2488
2489    #[test]
2490    fn direct_writer_creates_rar20_encrypted_compressed_archive() {
2491        let features = FeatureSet::store_only();
2492        let bytes = rar15_40::write_compressed_archive(
2493            &[rar15_40::FileEntry {
2494                name: b"rar20-secret.txt",
2495                data: b"facade rar20 encrypted payload payload\n",
2496                file_time: 0,
2497                file_attr: 0x20,
2498                host_os: 3,
2499                password: Some(b"password"),
2500                file_comment: None,
2501            }],
2502            rar15_options_with_features(ArchiveVersion::Rar20, features),
2503        )
2504        .unwrap();
2505
2506        let archive = ArchiveReader::read(&bytes).unwrap();
2507        let raw = archive.as_rar15_40().unwrap();
2508        let file = raw.files().next().unwrap();
2509        assert_eq!(file.unp_ver, 20);
2510        assert!(file.is_encrypted());
2511        assert!(matches!(
2512            collect_extract(&archive, None),
2513            Err(Error::NeedPassword)
2514        ));
2515        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2516        assert_eq!(
2517            extracted[0].data,
2518            b"facade rar20 encrypted payload payload\n"
2519        );
2520    }
2521
2522    #[test]
2523    fn direct_writer_creates_rar30_header_encrypted_compressed_archive() {
2524        let mut features = FeatureSet::store_only();
2525        features.header_encryption = true;
2526        let bytes = rar15_40::write_compressed_archive(
2527            &[rar15_40::FileEntry {
2528                name: b"rar30-header-secret.txt",
2529                data: b"facade rar30 header encrypted payload\n",
2530                file_time: 0,
2531                file_attr: 0x20,
2532                host_os: 3,
2533                password: Some(b"password"),
2534                file_comment: None,
2535            }],
2536            rar15_options_with_features(ArchiveVersion::Rar30, features),
2537        )
2538        .unwrap();
2539
2540        assert!(matches!(
2541            ArchiveReader::read(&bytes),
2542            Err(Error::NeedPassword)
2543        ));
2544        let archive = ArchiveReader::read_with_options(
2545            &bytes,
2546            ArchiveReadOptions::with_password(b"password"),
2547        )
2548        .unwrap();
2549        let raw = archive.as_rar15_40().unwrap();
2550        assert!(raw.main.has_encrypted_headers());
2551        assert_eq!(raw.files().next().unwrap().name, b"rar30-header-secret.txt");
2552        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2553        assert_eq!(
2554            extracted[0].data,
2555            b"facade rar30 header encrypted payload\n"
2556        );
2557    }
2558
2559    #[test]
2560    fn direct_writer_creates_rar30_solid_header_encrypted_compressed_archive() {
2561        let mut features = FeatureSet::store_only();
2562        features.header_encryption = true;
2563        features.solid = true;
2564        let bytes = rar15_40::write_compressed_archive(
2565            &[
2566                rar15_40::FileEntry {
2567                    name: b"solid-header-one.txt",
2568                    data: b"facade solid header encrypted one one one\n",
2569                    file_time: 0,
2570                    file_attr: 0x20,
2571                    host_os: 3,
2572                    password: Some(b"password"),
2573                    file_comment: None,
2574                },
2575                rar15_40::FileEntry {
2576                    name: b"solid-header-two.txt",
2577                    data: b"facade solid header encrypted two two two\n",
2578                    file_time: 0,
2579                    file_attr: 0x20,
2580                    host_os: 3,
2581                    password: Some(b"password"),
2582                    file_comment: None,
2583                },
2584            ],
2585            rar15_options_with_features(ArchiveVersion::Rar30, features),
2586        )
2587        .unwrap();
2588
2589        assert!(matches!(
2590            ArchiveReader::read(&bytes),
2591            Err(Error::NeedPassword)
2592        ));
2593        let archive = ArchiveReader::read_with_options(
2594            &bytes,
2595            ArchiveReadOptions::with_password(b"password"),
2596        )
2597        .unwrap();
2598        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2599        assert_eq!(
2600            extracted[0].data,
2601            b"facade solid header encrypted one one one\n"
2602        );
2603        assert_eq!(
2604            extracted[1].data,
2605            b"facade solid header encrypted two two two\n"
2606        );
2607    }
2608
2609    #[test]
2610    fn direct_writer_creates_rar50_stored_archive() {
2611        let bytes =
2612            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
2613                .entries(
2614                    [
2615                        rar50_entry(b"rar5-store.txt", b"facade rar5 stored payload\n")
2616                            .with_mtime(Some(0))
2617                            .with_attributes(0x20)
2618                            .with_host_os(3),
2619                    ]
2620                    .to_vec(),
2621                )
2622                .finish()
2623                .unwrap();
2624
2625        let archive = ArchiveReader::read(&bytes).unwrap();
2626        assert_eq!(archive.family(), ArchiveFamily::Rar50Plus);
2627        let extracted = collect_extract(&archive, None).unwrap();
2628        assert_eq!(extracted[0].data, b"facade rar5 stored payload\n");
2629    }
2630
2631    #[test]
2632    fn direct_writer_creates_rar50_compressed_archive() {
2633        let bytes = rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50))
2634            .entries(
2635                [rar50_entry(
2636                    b"rar5-compressed.txt",
2637                    b"facade rar5 compressed payload\nfacade rar5 compressed payload\n",
2638                )
2639                .with_mtime(Some(0))
2640                .with_attributes(0x20)
2641                .with_host_os(3)]
2642                .to_vec(),
2643            )
2644            .finish()
2645            .unwrap();
2646
2647        let archive = ArchiveReader::read(&bytes).unwrap();
2648        let raw = archive.as_rar50().unwrap();
2649        let file = raw.files().next().unwrap();
2650        assert_eq!(file.decoded_compression_info().unwrap().method, 3);
2651        let extracted = collect_extract(&archive, None).unwrap();
2652        assert_eq!(
2653            extracted[0].data,
2654            b"facade rar5 compressed payload\nfacade rar5 compressed payload\n"
2655        );
2656    }
2657
2658    #[test]
2659    fn direct_writer_creates_rar50_solid_compressed_archive() {
2660        let mut features = FeatureSet::store_only();
2661        features.solid = true;
2662        let first = b"facade rar50 solid shared phrase alpha beta gamma\n".repeat(16);
2663        let second = b"facade rar50 solid shared phrase alpha beta gamma\nsecond\n".repeat(8);
2664        let bytes =
2665            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
2666                .entries(
2667                    [
2668                        rar50_entry(b"rar5-solid-one.txt", &first)
2669                            .with_mtime(Some(0))
2670                            .with_attributes(0x20)
2671                            .with_host_os(3),
2672                        rar50_entry(b"rar5-solid-two.txt", &second)
2673                            .with_mtime(Some(0))
2674                            .with_attributes(0x20)
2675                            .with_host_os(3),
2676                    ]
2677                    .to_vec(),
2678                )
2679                .finish()
2680                .unwrap();
2681
2682        let archive = ArchiveReader::read(&bytes).unwrap();
2683        let raw = archive.as_rar50().unwrap();
2684        let files: Vec<_> = raw.files().collect();
2685        assert!(raw.main.is_solid());
2686        assert!(!files[0].decoded_compression_info().unwrap().solid);
2687        assert!(files[1].decoded_compression_info().unwrap().solid);
2688        let extracted = collect_extract(&archive, None).unwrap();
2689        assert_eq!(extracted[0].data, first);
2690        assert_eq!(extracted[1].data, second);
2691    }
2692
2693    #[test]
2694    fn direct_writer_creates_rar50_delta_filtered_compressed_archive() {
2695        let payload: Vec<u8> = (0..180)
2696            .map(|index| (index * 11 + index / 5) as u8)
2697            .collect();
2698        let bytes = rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50))
2699            .entries(
2700                [rar50_entry(b"rar5-delta-filtered.bin", &payload)
2701                    .with_mtime(Some(0))
2702                    .with_attributes(0x20)
2703                    .with_host_os(3)]
2704                .to_vec(),
2705            )
2706            .filter_policy(rar50::FilterPolicy::explicit(rar50::FilterKind::Delta {
2707                channels: 3,
2708            }))
2709            .finish()
2710            .unwrap();
2711
2712        let archive = ArchiveReader::read(&bytes).unwrap();
2713        let extracted = collect_extract(&archive, None).unwrap();
2714        assert_eq!(extracted[0].data, payload);
2715    }
2716
2717    #[test]
2718    fn direct_writer_creates_rar50_e8_filtered_compressed_archive() {
2719        let payload = b"\xe8\0\0\0\0facade rar5 e8 filter payload".to_vec();
2720        let bytes = rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50))
2721            .entries(
2722                [rar50_entry(b"rar5-e8-filtered.bin", &payload)
2723                    .with_mtime(Some(0))
2724                    .with_attributes(0x20)
2725                    .with_host_os(3)]
2726                .to_vec(),
2727            )
2728            .filter_policy(rar50::FilterPolicy::explicit(rar50::FilterKind::E8))
2729            .finish()
2730            .unwrap();
2731
2732        let archive = ArchiveReader::read(&bytes).unwrap();
2733        let extracted = collect_extract(&archive, None).unwrap();
2734        assert_eq!(extracted[0].data, payload);
2735    }
2736
2737    #[test]
2738    fn direct_writer_creates_rar50_arm_filtered_compressed_archive() {
2739        let payload = [0x04, 0x00, 0x00, 0xeb, b'A', b'R', b'M', b'!'];
2740        let bytes = rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50))
2741            .entries(
2742                [rar50_entry(b"rar5-arm-filtered.bin", &payload)
2743                    .with_mtime(Some(0))
2744                    .with_attributes(0x20)
2745                    .with_host_os(3)]
2746                .to_vec(),
2747            )
2748            .filter_policy(rar50::FilterPolicy::explicit(rar50::FilterKind::Arm))
2749            .finish()
2750            .unwrap();
2751
2752        let archive = ArchiveReader::read(&bytes).unwrap();
2753        let extracted = collect_extract(&archive, None).unwrap();
2754        assert_eq!(extracted[0].data, payload);
2755    }
2756
2757    #[test]
2758    fn direct_writer_creates_rar50_auto_filtered_compressed_archive() {
2759        let payload = b"\xe8\0\0\0\0facade rar5 auto filter payload\n".repeat(16);
2760        let bytes = rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50))
2761            .entries(
2762                [rar50_entry(b"rar5-auto-filtered.bin", &payload)
2763                    .with_mtime(Some(0))
2764                    .with_attributes(0x20)
2765                    .with_host_os(3)]
2766                .to_vec(),
2767            )
2768            .filter_policy(rar50::FilterPolicy::Auto)
2769            .finish()
2770            .unwrap();
2771
2772        let archive = ArchiveReader::read(&bytes).unwrap();
2773        let extracted = collect_extract(&archive, None).unwrap();
2774        assert_eq!(extracted[0].data, payload);
2775    }
2776
2777    #[test]
2778    fn direct_writer_creates_rar50_stored_archive_with_comment_service() {
2779        let features = FeatureSet::store_only();
2780        let bytes = rar50::Rar50Writer::new(
2781            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
2782        )
2783        .entries(
2784            [
2785                rar50_entry(b"rar5-commented.txt", b"facade rar5 comment payload\n")
2786                    .with_attributes(0x20)
2787                    .with_host_os(3),
2788            ]
2789            .to_vec(),
2790        )
2791        .archive_comment(Some(b"facade rar5 comment\n"))
2792        .finish()
2793        .unwrap();
2794
2795        let archive = ArchiveReader::read(&bytes).unwrap();
2796        let raw = archive.as_rar50().unwrap();
2797        let services: Vec<_> = raw.services().collect();
2798        assert_eq!(services.len(), 1);
2799        assert_eq!(services[0].name, b"CMT");
2800        assert_eq!(
2801            collect_rar50_file(raw, services[0]).unwrap().data,
2802            b"facade rar5 comment\n"
2803        );
2804        let extracted = collect_extract(&archive, None).unwrap();
2805        assert_eq!(extracted[0].data, b"facade rar5 comment payload\n");
2806    }
2807
2808    #[test]
2809    fn direct_writer_creates_rar50_stored_file_comment_service() {
2810        let bytes =
2811            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
2812                .entry(
2813                    rar50_entry(
2814                        b"rar5-file-commented.txt",
2815                        b"facade rar5 file comment payload\n",
2816                    )
2817                    .with_attributes(0x20)
2818                    .with_host_os(3)
2819                    .with_service(rar50::ServiceEntry::new(
2820                        b"CMT".to_vec(),
2821                        b"facade rar5 file comment\n".to_vec(),
2822                    )),
2823                )
2824                .finish()
2825                .unwrap();
2826
2827        let archive = ArchiveReader::read(&bytes).unwrap();
2828        let raw = archive.as_rar50().unwrap();
2829        let services: Vec<_> = raw.services().collect();
2830        assert_eq!(services.len(), 1);
2831        assert_eq!(services[0].name, b"CMT");
2832        assert_eq!(
2833            collect_rar50_file(raw, services[0]).unwrap().data,
2834            b"facade rar5 file comment\n"
2835        );
2836        let extracted = collect_extract(&archive, None).unwrap();
2837        assert_eq!(extracted[0].data, b"facade rar5 file comment payload\n");
2838    }
2839
2840    #[test]
2841    fn direct_writer_creates_rar50_encrypted_stored_file_comment_service() {
2842        let features = FeatureSet::store_only();
2843        let bytes = rar50::Rar50Writer::new(
2844            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
2845        )
2846        .entry(
2847            rar50_entry(
2848                b"rar5-encrypted-file-commented.txt",
2849                b"facade encrypted rar5 file comment payload\n",
2850            )
2851            .with_attributes(0x20)
2852            .with_host_os(3)
2853            .with_password(b"password".to_vec())
2854            .with_service(
2855                rar50::ServiceEntry::new(
2856                    b"CMT".to_vec(),
2857                    b"facade encrypted rar5 file comment\n".to_vec(),
2858                )
2859                .with_password(b"password".to_vec()),
2860            ),
2861        )
2862        .finish()
2863        .unwrap();
2864
2865        let archive = ArchiveReader::read_with_options(
2866            &bytes,
2867            ArchiveReadOptions::with_password(b"password"),
2868        )
2869        .unwrap();
2870        let raw = archive.as_rar50().unwrap();
2871        let service = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
2872        assert_eq!(service.data, b"facade encrypted rar5 file comment\n");
2873        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2874        assert_eq!(
2875            extracted[0].data,
2876            b"facade encrypted rar5 file comment payload\n"
2877        );
2878    }
2879
2880    #[test]
2881    fn direct_writer_creates_rar50_header_encrypted_stored_file_comment_service() {
2882        let mut features = FeatureSet::store_only();
2883        features.header_encryption = true;
2884        let bytes = rar50::Rar50Writer::new(
2885            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
2886        )
2887        .entry(
2888            rar50_entry(
2889                b"rar5-header-file-commented.txt",
2890                b"facade header encrypted rar5 file comment payload\n",
2891            )
2892            .with_attributes(0x20)
2893            .with_host_os(3)
2894            .with_password(b"password".to_vec())
2895            .with_service(
2896                rar50::ServiceEntry::new(
2897                    b"CMT".to_vec(),
2898                    b"facade header encrypted rar5 file comment\n".to_vec(),
2899                )
2900                .with_password(b"password".to_vec()),
2901            ),
2902        )
2903        .finish()
2904        .unwrap();
2905
2906        assert!(matches!(
2907            ArchiveReader::read(&bytes),
2908            Err(Error::NeedPassword)
2909        ));
2910        let archive = ArchiveReader::read_with_options(
2911            &bytes,
2912            ArchiveReadOptions::with_password(b"password"),
2913        )
2914        .unwrap();
2915        let raw = archive.as_rar50().unwrap();
2916        let service = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
2917        assert_eq!(service.data, b"facade header encrypted rar5 file comment\n");
2918        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
2919        assert_eq!(
2920            extracted[0].data,
2921            b"facade header encrypted rar5 file comment payload\n"
2922        );
2923    }
2924
2925    #[test]
2926    fn direct_writer_creates_rar50_stored_archive_with_quick_open_service() {
2927        let mut features = FeatureSet::store_only();
2928        features.quick_open = true;
2929        let bytes = rar50::Rar50Writer::new(
2930            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
2931        )
2932        .entries(
2933            [
2934                rar50_entry(b"rar5-qo.txt", b"facade rar5 quick-open payload\n")
2935                    .with_attributes(0x20)
2936                    .with_host_os(3),
2937            ]
2938            .to_vec(),
2939        )
2940        .finish()
2941        .unwrap();
2942
2943        let archive = ArchiveReader::read(&bytes).unwrap();
2944        let raw = archive.as_rar50().unwrap();
2945        assert!(raw.main.locator().unwrap().quick_open_offset.unwrap() > 0);
2946        let services: Vec<_> = raw.services().collect();
2947        assert_eq!(services.len(), 1);
2948        assert_eq!(services[0].name, b"QO");
2949        assert!(!collect_rar50_file(raw, services[0])
2950            .unwrap()
2951            .data
2952            .is_empty());
2953        let extracted = collect_extract(&archive, None).unwrap();
2954        assert_eq!(extracted[0].data, b"facade rar5 quick-open payload\n");
2955    }
2956
2957    #[test]
2958    fn direct_writer_creates_rar50_stored_archive_with_file_services() {
2959        let bytes =
2960            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
2961                .entry(
2962                    rar50_entry(b"rar5-services.txt", b"facade rar5 service payload\n")
2963                        .with_attributes(0x20)
2964                        .with_host_os(3)
2965                        .with_service(rar50::ServiceEntry::new(
2966                            b"ACL".to_vec(),
2967                            b"facade acl".to_vec(),
2968                        ))
2969                        .with_service(rar50::ServiceEntry::new(
2970                            b"STM".to_vec(),
2971                            b"facade stream".to_vec(),
2972                        )),
2973                )
2974                .finish()
2975                .unwrap();
2976
2977        let archive = ArchiveReader::read(&bytes).unwrap();
2978        let raw = archive.as_rar50().unwrap();
2979        let services: Vec<_> = raw.services().collect();
2980        assert_eq!(services.len(), 2);
2981        assert_eq!(services[0].name, b"ACL");
2982        assert_eq!(services[1].name, b"STM");
2983        assert_eq!(
2984            collect_rar50_file(raw, services[0]).unwrap().data,
2985            b"facade acl"
2986        );
2987        assert_eq!(
2988            collect_rar50_file(raw, services[1]).unwrap().data,
2989            b"facade stream"
2990        );
2991        let extracted = collect_extract(&archive, None).unwrap();
2992        assert_eq!(extracted[0].data, b"facade rar5 service payload\n");
2993    }
2994
2995    #[test]
2996    fn direct_writer_creates_rar50_stored_archive_with_recovery_service() {
2997        let features = FeatureSet::store_only();
2998        let bytes = rar50::Rar50Writer::new(
2999            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3000        )
3001        .entries(
3002            [
3003                rar50_entry(b"rar5-recovery.txt", b"facade rar5 recovery payload\n")
3004                    .with_attributes(0x20)
3005                    .with_host_os(3),
3006            ]
3007            .to_vec(),
3008        )
3009        .recovery_percent(Some(9))
3010        .finish()
3011        .unwrap();
3012
3013        let archive = ArchiveReader::read(&bytes).unwrap();
3014        let raw = archive.as_rar50().unwrap();
3015        assert!(raw.main.has_recovery_record());
3016        let service = raw.services().next().unwrap();
3017        assert_eq!(service.name, b"RR");
3018        let recovery = service.recovery_record().unwrap().unwrap();
3019        assert_eq!(recovery.percent, 9);
3020        let extracted = collect_extract(&archive, None).unwrap();
3021        assert_eq!(extracted[0].data, b"facade rar5 recovery payload\n");
3022    }
3023
3024    #[test]
3025    fn archive_facade_repairs_rar50_inline_recovery_damage() {
3026        let features = FeatureSet::store_only();
3027        let payload = b"facade rar5 repair payload\n".repeat(64);
3028        let bytes = rar50::Rar50Writer::new(
3029            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3030        )
3031        .entries(
3032            [rar50_entry(b"rar5-repair.txt", &payload)
3033                .with_attributes(0x20)
3034                .with_host_os(3)]
3035            .to_vec(),
3036        )
3037        .recovery_percent(Some(20))
3038        .finish()
3039        .unwrap();
3040        let archive = ArchiveReader::read(&bytes).unwrap();
3041        let data_range = archive
3042            .as_rar50()
3043            .unwrap()
3044            .files()
3045            .next()
3046            .unwrap()
3047            .block
3048            .data_range
3049            .clone();
3050        let mut damaged = bytes.clone();
3051        damaged[data_range.start + 4..data_range.start + 80].fill(0xa5);
3052        let damaged_archive = ArchiveReader::read(&damaged).unwrap();
3053        assert!(collect_extract(&damaged_archive, None).is_err());
3054
3055        let mut repaired = Vec::new();
3056        damaged_archive.repair_recovery_to(&mut repaired).unwrap();
3057
3058        assert_eq!(repaired, bytes);
3059        let repaired_archive = ArchiveReader::read(&repaired).unwrap();
3060        assert_eq!(
3061            collect_extract(&repaired_archive, None).unwrap()[0].data,
3062            payload
3063        );
3064    }
3065
3066    #[test]
3067    fn archive_facade_reports_rar13_family_for_unsupported_recovery_repair() {
3068        let bytes = rar13::write_stored_archive(
3069            &[rar13::StoredEntry {
3070                name: b"old.txt",
3071                data: b"old rar payload",
3072                file_time: 0,
3073                file_attr: 0x20,
3074                password: None,
3075                file_comment: None,
3076            }],
3077            rar13_options(ArchiveVersion::Rar13),
3078        )
3079        .unwrap();
3080        let archive = ArchiveReader::read(&bytes).unwrap();
3081        let mut repaired = Vec::new();
3082        let err = archive.repair_recovery_to(&mut repaired).unwrap_err();
3083
3084        assert_eq!(
3085            err,
3086            Error::UnsupportedFamilyFeature {
3087                family: ArchiveFamily::Rar13,
3088                feature: "recovery repair for RAR 1.3/1.4 archives"
3089            }
3090        );
3091    }
3092
3093    #[test]
3094    fn archive_facade_repairs_rar15_40_recovery_as_full_archive_bytes() {
3095        let bytes = std::fs::read(rar15_40_fixture("rar250_protect_head_rr5.rar")).unwrap();
3096        let mut damaged = bytes.clone();
3097        damaged[512 + 16..512 + 80].fill(0xa5);
3098        let damaged_archive = ArchiveReader::read(&damaged).unwrap();
3099        assert!(collect_extract(&damaged_archive, None).is_err());
3100
3101        let mut repaired = Vec::new();
3102        damaged_archive.repair_recovery_to(&mut repaired).unwrap();
3103
3104        assert_eq!(repaired, bytes);
3105        let repaired_archive = ArchiveReader::read(&repaired).unwrap();
3106        assert_eq!(
3107            collect_extract(&repaired_archive, None).unwrap()[0].name,
3108            b"BIG.BIN"
3109        );
3110    }
3111
3112    #[test]
3113    fn archive_facade_repairs_rar3_newsub_recovery_as_full_archive_bytes() {
3114        let bytes = std::fs::read(rar15_40_fixture("rar300/with_recovery_rar300.rar")).unwrap();
3115        let mut damaged = bytes.clone();
3116        damaged[512 + 16..512 + 80].fill(0xa5);
3117        let damaged_archive = ArchiveReader::read(&damaged).unwrap();
3118        assert!(collect_extract(&damaged_archive, None).is_err());
3119
3120        let mut repaired = Vec::new();
3121        damaged_archive.repair_recovery_to(&mut repaired).unwrap();
3122
3123        assert_eq!(repaired, bytes);
3124        let repaired_archive = ArchiveReader::read(&repaired).unwrap();
3125        assert_eq!(
3126            collect_extract(&repaired_archive, None).unwrap()[0].name,
3127            b"bigtext_64k.bin"
3128        );
3129    }
3130
3131    #[test]
3132    fn direct_writer_creates_rar50_compressed_archive_with_recovery_service() {
3133        let features = FeatureSet::store_only();
3134        let payload = b"facade rar5 compressed recovery payload repeated repeated\n".repeat(8);
3135        let bytes =
3136            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3137                .entries(
3138                    [rar50_entry(b"rar5-compressed-recovery.txt", &payload)
3139                        .with_attributes(0x20)
3140                        .with_host_os(3)]
3141                    .to_vec(),
3142                )
3143                .recovery_percent(Some(9))
3144                .finish()
3145                .unwrap();
3146
3147        let archive = ArchiveReader::read(&bytes).unwrap();
3148        let raw = archive.as_rar50().unwrap();
3149        assert!(raw.main.has_recovery_record());
3150        let service = raw.services().next().unwrap();
3151        assert_eq!(service.name, b"RR");
3152        assert_eq!(service.recovery_record().unwrap().unwrap().percent, 9);
3153        let extracted = collect_extract(&archive, None).unwrap();
3154        assert_eq!(extracted[0].data, payload);
3155    }
3156
3157    #[test]
3158    fn direct_writer_creates_rar70_stored_archive_with_metadata() {
3159        let bytes =
3160            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar70).with_compression_level(0))
3161                .entries(
3162                    [
3163                        rar50_entry(b"rar7-metadata.txt", b"facade rar7 metadata payload\n")
3164                            .with_attributes(0x20)
3165                            .with_host_os(3),
3166                    ]
3167                    .to_vec(),
3168                )
3169                .archive_metadata(Some(rar50::ArchiveMetadataEntry {
3170                    name: Some(b"facade-metadata.rar"),
3171                    creation_time: Some(0x01dcd60e_662d7a32),
3172                }))
3173                .finish()
3174                .unwrap();
3175
3176        let archive = ArchiveReader::read(&bytes).unwrap();
3177        let metadata = archive.as_rar50().unwrap().main.archive_metadata().unwrap();
3178        assert_eq!(
3179            metadata.name.as_deref(),
3180            Some(b"facade-metadata.rar".as_slice())
3181        );
3182        let extracted = collect_extract(&archive, None).unwrap();
3183        assert_eq!(extracted[0].data, b"facade rar7 metadata payload\n");
3184    }
3185
3186    #[test]
3187    fn direct_writer_creates_rar70_compressed_archive_with_metadata() {
3188        let payload = b"facade rar7 compressed metadata payload repeated\n".repeat(8);
3189        let bytes = rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar70))
3190            .entries(
3191                [rar50_entry(b"rar7-compressed-metadata.txt", &payload)
3192                    .with_attributes(0x20)
3193                    .with_host_os(3)]
3194                .to_vec(),
3195            )
3196            .archive_metadata(Some(rar50::ArchiveMetadataEntry {
3197                name: Some(b"facade-compressed-metadata.rar"),
3198                creation_time: Some(0x01dcd60e_662d7a32),
3199            }))
3200            .finish()
3201            .unwrap();
3202
3203        let archive = ArchiveReader::read(&bytes).unwrap();
3204        let metadata = archive.as_rar50().unwrap().main.archive_metadata().unwrap();
3205        assert_eq!(
3206            metadata.name.as_deref(),
3207            Some(b"facade-compressed-metadata.rar".as_slice())
3208        );
3209        let extracted = collect_extract(&archive, None).unwrap();
3210        assert_eq!(extracted[0].data, payload);
3211    }
3212
3213    #[test]
3214    fn direct_writer_creates_rar50_compressed_archive_with_comment() {
3215        let payload = b"facade rar5 compressed archive comment payload repeated\n".repeat(8);
3216        let features = FeatureSet::store_only();
3217        let bytes =
3218            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3219                .entries(
3220                    [rar50_entry(b"rar5-compressed-comment.txt", &payload)
3221                        .with_attributes(0x20)
3222                        .with_host_os(3)]
3223                    .to_vec(),
3224                )
3225                .archive_comment(Some(b"facade compressed comment\n"))
3226                .finish()
3227                .unwrap();
3228
3229        let archive = ArchiveReader::read(&bytes).unwrap();
3230        let raw = archive.as_rar50().unwrap();
3231        let comment = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
3232        assert_eq!(comment.data, b"facade compressed comment\n");
3233        let extracted = collect_extract(&archive, None).unwrap();
3234        assert_eq!(extracted[0].data, payload);
3235    }
3236
3237    #[test]
3238    fn direct_writer_creates_rar70_encrypted_stored_archive_with_metadata() {
3239        let features = FeatureSet::store_only();
3240        let bytes = rar50::Rar50Writer::new(
3241            rar50_options_with_features(ArchiveVersion::Rar70, features).with_compression_level(0),
3242        )
3243        .entries(
3244            [rar50_entry(
3245                b"rar7-encrypted-metadata.txt",
3246                b"facade rar7 encrypted metadata payload\n",
3247            )
3248            .with_attributes(0x20)
3249            .with_host_os(3)
3250            .with_password(b"password".to_vec())]
3251            .to_vec(),
3252        )
3253        .archive_metadata(Some(rar50::ArchiveMetadataEntry {
3254            name: Some(b"facade-encrypted-metadata.rar"),
3255            creation_time: Some(0x01dcd60e_662d7a32),
3256        }))
3257        .finish()
3258        .unwrap();
3259
3260        let archive = ArchiveReader::read(&bytes).unwrap();
3261        let metadata = archive.as_rar50().unwrap().main.archive_metadata().unwrap();
3262        assert_eq!(
3263            metadata.name.as_deref(),
3264            Some(b"facade-encrypted-metadata.rar".as_slice())
3265        );
3266        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3267        assert_eq!(
3268            extracted[0].data,
3269            b"facade rar7 encrypted metadata payload\n"
3270        );
3271    }
3272
3273    #[test]
3274    fn direct_writer_creates_rar70_encrypted_compressed_archive_with_metadata() {
3275        let features = FeatureSet::store_only();
3276        let payload = b"facade rar7 encrypted compressed metadata payload repeated\n".repeat(8);
3277        let bytes =
3278            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar70, features))
3279                .entries(
3280                    [
3281                        rar50_entry(b"rar7-encrypted-compressed-metadata.txt", &payload)
3282                            .with_attributes(0x20)
3283                            .with_host_os(3)
3284                            .with_password(b"password".to_vec()),
3285                    ]
3286                    .to_vec(),
3287                )
3288                .archive_metadata(Some(rar50::ArchiveMetadataEntry {
3289                    name: Some(b"facade-encrypted-compressed-metadata.rar"),
3290                    creation_time: Some(0x01dcd60e_662d7a32),
3291                }))
3292                .finish()
3293                .unwrap();
3294
3295        let archive = ArchiveReader::read(&bytes).unwrap();
3296        let metadata = archive.as_rar50().unwrap().main.archive_metadata().unwrap();
3297        assert_eq!(
3298            metadata.name.as_deref(),
3299            Some(b"facade-encrypted-compressed-metadata.rar".as_slice())
3300        );
3301        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3302        assert_eq!(extracted[0].data, payload);
3303    }
3304
3305    #[test]
3306    fn direct_writer_creates_rar70_header_encrypted_stored_archive_with_metadata() {
3307        let mut features = FeatureSet::store_only();
3308        features.header_encryption = true;
3309        let bytes = rar50::Rar50Writer::new(
3310            rar50_options_with_features(ArchiveVersion::Rar70, features).with_compression_level(0),
3311        )
3312        .entries(
3313            [rar50_entry(
3314                b"rar7-header-metadata.txt",
3315                b"facade rar7 header encrypted metadata payload\n",
3316            )
3317            .with_attributes(0x20)
3318            .with_host_os(3)
3319            .with_password(b"password".to_vec())]
3320            .to_vec(),
3321        )
3322        .archive_metadata(Some(rar50::ArchiveMetadataEntry {
3323            name: Some(b"facade-header-metadata.rar"),
3324            creation_time: Some(0x01dcd60e_662d7a32),
3325        }))
3326        .finish()
3327        .unwrap();
3328
3329        assert!(matches!(
3330            ArchiveReader::read(&bytes),
3331            Err(Error::NeedPassword)
3332        ));
3333        let archive = ArchiveReader::read_with_options(
3334            &bytes,
3335            ArchiveReadOptions::with_password(b"password"),
3336        )
3337        .unwrap();
3338        let metadata = archive.as_rar50().unwrap().main.archive_metadata().unwrap();
3339        assert_eq!(
3340            metadata.name.as_deref(),
3341            Some(b"facade-header-metadata.rar".as_slice())
3342        );
3343        let extracted = collect_extract(&archive, None).unwrap();
3344        assert_eq!(
3345            extracted[0].data,
3346            b"facade rar7 header encrypted metadata payload\n"
3347        );
3348    }
3349
3350    #[test]
3351    fn direct_writer_creates_rar70_header_encrypted_compressed_archive_with_metadata() {
3352        let mut features = FeatureSet::store_only();
3353        features.header_encryption = true;
3354        let payload =
3355            b"facade rar7 header encrypted compressed metadata payload repeated\n".repeat(8);
3356        let bytes =
3357            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar70, features))
3358                .entries(
3359                    [
3360                        rar50_entry(b"rar7-header-compressed-metadata.txt", &payload)
3361                            .with_attributes(0x20)
3362                            .with_host_os(3)
3363                            .with_password(b"password".to_vec()),
3364                    ]
3365                    .to_vec(),
3366                )
3367                .archive_metadata(Some(rar50::ArchiveMetadataEntry {
3368                    name: Some(b"facade-header-compressed-metadata.rar"),
3369                    creation_time: Some(0x01dcd60e_662d7a32),
3370                }))
3371                .finish()
3372                .unwrap();
3373
3374        assert!(matches!(
3375            ArchiveReader::read(&bytes),
3376            Err(Error::NeedPassword)
3377        ));
3378        let archive = ArchiveReader::read_with_options(
3379            &bytes,
3380            ArchiveReadOptions::with_password(b"password"),
3381        )
3382        .unwrap();
3383        let metadata = archive.as_rar50().unwrap().main.archive_metadata().unwrap();
3384        assert_eq!(
3385            metadata.name.as_deref(),
3386            Some(b"facade-header-compressed-metadata.rar".as_slice())
3387        );
3388        let extracted = collect_extract(&archive, None).unwrap();
3389        assert_eq!(extracted[0].data, payload);
3390    }
3391
3392    #[test]
3393    fn direct_writer_creates_rar50_encrypted_stored_archive() {
3394        let features = FeatureSet::store_only();
3395        let bytes = rar50::Rar50Writer::new(
3396            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3397        )
3398        .entries(
3399            [rar50_entry(
3400                b"rar5-secret.txt",
3401                b"facade rar5 encrypted stored payload\n",
3402            )
3403            .with_mtime(Some(0))
3404            .with_attributes(0x20)
3405            .with_host_os(3)
3406            .with_password(b"password".to_vec())]
3407            .to_vec(),
3408        )
3409        .finish()
3410        .unwrap();
3411
3412        let archive = ArchiveReader::read(&bytes).unwrap();
3413        assert!(matches!(
3414            collect_extract(&archive, None),
3415            Err(Error::AtEntry { source, .. }) if matches!(*source, Error::NeedPassword)
3416        ));
3417        assert!(matches!(
3418            collect_extract(&archive, Some(b"wrong")),
3419            Err(Error::AtEntry { source, .. })
3420                if matches!(*source, Error::WrongPasswordOrCorruptData)
3421        ));
3422        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3423        assert_eq!(extracted[0].data, b"facade rar5 encrypted stored payload\n");
3424    }
3425
3426    #[test]
3427    fn direct_writer_creates_rar50_encrypted_compressed_archive() {
3428        let features = FeatureSet::store_only();
3429        let payload = b"facade rar5 encrypted compressed\n".repeat(16);
3430        let bytes =
3431            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3432                .entries(
3433                    [rar50_entry(b"rar5-secret-compressed.txt", &payload)
3434                        .with_attributes(0x20)
3435                        .with_host_os(3)
3436                        .with_password(b"password".to_vec())]
3437                    .to_vec(),
3438                )
3439                .finish()
3440                .unwrap();
3441
3442        let archive = ArchiveReader::read(&bytes).unwrap();
3443        let raw = archive.as_rar50().unwrap();
3444        let file = raw.files().next().unwrap();
3445        assert!(file.encrypted);
3446        assert_eq!(file.decoded_compression_info().unwrap().method, 3);
3447        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3448        assert_eq!(extracted[0].data, payload);
3449    }
3450
3451    #[test]
3452    fn direct_writer_creates_rar50_encrypted_solid_compressed_archive() {
3453        let mut features = FeatureSet::store_only();
3454        features.solid = true;
3455        let first = b"facade rar50 encrypted solid shared phrase alpha beta gamma\n".repeat(12);
3456        let second =
3457            b"facade rar50 encrypted solid shared phrase alpha beta gamma\nsecond\n".repeat(6);
3458        let bytes =
3459            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3460                .entries(
3461                    [
3462                        rar50_entry(b"rar5-encrypted-solid-one.txt", &first)
3463                            .with_attributes(0x20)
3464                            .with_host_os(3)
3465                            .with_password(b"password".to_vec()),
3466                        rar50_entry(b"rar5-encrypted-solid-two.txt", &second)
3467                            .with_attributes(0x20)
3468                            .with_host_os(3)
3469                            .with_password(b"password".to_vec()),
3470                    ]
3471                    .to_vec(),
3472                )
3473                .finish()
3474                .unwrap();
3475
3476        let archive = ArchiveReader::read(&bytes).unwrap();
3477        let raw = archive.as_rar50().unwrap();
3478        let files: Vec<_> = raw.files().collect();
3479        assert!(raw.main.is_solid());
3480        assert!(files.iter().all(|file| file.encrypted));
3481        assert!(!files[0].decoded_compression_info().unwrap().solid);
3482        assert!(files[1].decoded_compression_info().unwrap().solid);
3483        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3484        assert_eq!(extracted[0].data, first);
3485        assert_eq!(extracted[1].data, second);
3486    }
3487
3488    #[test]
3489    fn direct_writer_creates_rar50_header_encrypted_compressed_archive() {
3490        let mut features = FeatureSet::store_only();
3491        features.header_encryption = true;
3492        let bytes = rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3493            .entries([rar50_entry(b"rar5-header-secret-compressed.txt", b"facade rar5 header encrypted compressed\nfacade rar5 header encrypted compressed\n").with_attributes(0x20).with_host_os(3).with_password(b"password".to_vec())].to_vec())
3494            .finish()
3495            .unwrap();
3496
3497        assert!(matches!(
3498            ArchiveReader::read(&bytes),
3499            Err(Error::NeedPassword)
3500        ));
3501        let archive = ArchiveReader::read_with_options(
3502            &bytes,
3503            ArchiveReadOptions::with_password(b"password"),
3504        )
3505        .unwrap();
3506        let raw = archive.as_rar50().unwrap();
3507        let file = raw.files().next().unwrap();
3508        assert!(file.encrypted);
3509        assert_eq!(file.decoded_compression_info().unwrap().method, 3);
3510        let extracted = collect_extract(&archive, None).unwrap();
3511        assert_eq!(
3512            extracted[0].data,
3513            b"facade rar5 header encrypted compressed\nfacade rar5 header encrypted compressed\n"
3514        );
3515    }
3516
3517    #[test]
3518    fn direct_writer_creates_rar50_header_encrypted_solid_compressed_archive() {
3519        let mut features = FeatureSet::store_only();
3520        features.header_encryption = true;
3521        features.solid = true;
3522        let first =
3523            b"facade rar50 header encrypted solid shared phrase alpha beta gamma\n".repeat(12);
3524        let second =
3525            b"facade rar50 header encrypted solid shared phrase alpha beta gamma\nsecond\n"
3526                .repeat(6);
3527        let bytes =
3528            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3529                .entries(
3530                    [
3531                        rar50_entry(b"rar5-header-solid-one.txt", &first)
3532                            .with_attributes(0x20)
3533                            .with_host_os(3)
3534                            .with_password(b"password".to_vec()),
3535                        rar50_entry(b"rar5-header-solid-two.txt", &second)
3536                            .with_attributes(0x20)
3537                            .with_host_os(3)
3538                            .with_password(b"password".to_vec()),
3539                    ]
3540                    .to_vec(),
3541                )
3542                .finish()
3543                .unwrap();
3544
3545        assert!(matches!(
3546            ArchiveReader::read(&bytes),
3547            Err(Error::NeedPassword)
3548        ));
3549        let archive = ArchiveReader::read_with_options(
3550            &bytes,
3551            ArchiveReadOptions::with_password(b"password"),
3552        )
3553        .unwrap();
3554        let raw = archive.as_rar50().unwrap();
3555        let files: Vec<_> = raw.files().collect();
3556        assert!(raw.main.is_solid());
3557        assert!(files.iter().all(|file| file.encrypted));
3558        assert!(!files[0].decoded_compression_info().unwrap().solid);
3559        assert!(files[1].decoded_compression_info().unwrap().solid);
3560        let extracted = collect_extract(&archive, None).unwrap();
3561        assert_eq!(extracted[0].data, first);
3562        assert_eq!(extracted[1].data, second);
3563    }
3564
3565    #[test]
3566    fn direct_writer_creates_rar50_encrypted_stored_archive_with_comment() {
3567        let features = FeatureSet::store_only();
3568        let bytes = rar50::Rar50Writer::new(
3569            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3570        )
3571        .entries(
3572            [rar50_entry(
3573                b"rar5-secret.txt",
3574                b"facade rar5 encrypted stored payload\n",
3575            )
3576            .with_mtime(Some(0))
3577            .with_attributes(0x20)
3578            .with_host_os(3)
3579            .with_password(b"password".to_vec())]
3580            .to_vec(),
3581        )
3582        .encrypted_archive_comment(b"facade encrypted comment\n", b"password")
3583        .finish()
3584        .unwrap();
3585
3586        let archive = ArchiveReader::read_with_options(
3587            &bytes,
3588            ArchiveReadOptions::with_password(b"password"),
3589        )
3590        .unwrap();
3591        let raw = archive.as_rar50().unwrap();
3592        let comment = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
3593        assert_eq!(comment.data, b"facade encrypted comment\n");
3594        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3595        assert_eq!(extracted[0].data, b"facade rar5 encrypted stored payload\n");
3596    }
3597
3598    #[test]
3599    fn direct_writer_creates_rar50_encrypted_compressed_archive_with_comment() {
3600        let payload = b"facade rar5 encrypted compressed comment payload\n".repeat(8);
3601        let features = FeatureSet::store_only();
3602        let bytes =
3603            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3604                .entries(
3605                    [
3606                        rar50_entry(b"rar5-encrypted-compressed-comment.txt", &payload)
3607                            .with_mtime(Some(0))
3608                            .with_attributes(0x20)
3609                            .with_host_os(3)
3610                            .with_password(b"password".to_vec()),
3611                    ]
3612                    .to_vec(),
3613                )
3614                .encrypted_archive_comment(b"facade encrypted compressed comment\n", b"password")
3615                .finish()
3616                .unwrap();
3617
3618        let archive = ArchiveReader::read_with_options(
3619            &bytes,
3620            ArchiveReadOptions::with_password(b"password"),
3621        )
3622        .unwrap();
3623        let raw = archive.as_rar50().unwrap();
3624        let comment = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
3625        assert_eq!(comment.data, b"facade encrypted compressed comment\n");
3626        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3627        assert_eq!(extracted[0].data, payload);
3628    }
3629
3630    #[test]
3631    fn direct_writer_creates_rar50_header_encrypted_stored_archive() {
3632        let mut features = FeatureSet::store_only();
3633        features.header_encryption = true;
3634        let bytes = rar50::Rar50Writer::new(
3635            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3636        )
3637        .entries(
3638            [rar50_entry(
3639                b"rar5-header-secret.txt",
3640                b"facade rar5 header encrypted stored payload\n",
3641            )
3642            .with_mtime(Some(0))
3643            .with_attributes(0x20)
3644            .with_host_os(3)
3645            .with_password(b"password".to_vec())]
3646            .to_vec(),
3647        )
3648        .finish()
3649        .unwrap();
3650
3651        assert!(matches!(
3652            ArchiveReader::read(&bytes),
3653            Err(Error::NeedPassword)
3654        ));
3655        assert!(matches!(
3656            ArchiveReader::read_with_options(&bytes, ArchiveReadOptions::with_password(b"wrong")),
3657            Err(Error::WrongPasswordOrCorruptData)
3658        ));
3659        let archive = ArchiveReader::read_with_options(
3660            &bytes,
3661            ArchiveReadOptions::with_password(b"password"),
3662        )
3663        .unwrap();
3664        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3665        assert_eq!(
3666            extracted[0].data,
3667            b"facade rar5 header encrypted stored payload\n"
3668        );
3669    }
3670
3671    #[test]
3672    fn direct_writer_creates_rar50_header_encrypted_stored_archive_with_comment() {
3673        let mut features = FeatureSet::store_only();
3674        features.header_encryption = true;
3675        let bytes = rar50::Rar50Writer::new(
3676            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3677        )
3678        .entries(
3679            [rar50_entry(
3680                b"rar5-header-comment-secret.txt",
3681                b"facade rar5 header encrypted comment payload\n",
3682            )
3683            .with_mtime(Some(0))
3684            .with_attributes(0x20)
3685            .with_host_os(3)
3686            .with_password(b"password".to_vec())]
3687            .to_vec(),
3688        )
3689        .encrypted_archive_comment(b"facade header encrypted comment\n", b"password")
3690        .finish()
3691        .unwrap();
3692
3693        let archive = ArchiveReader::read_with_options(
3694            &bytes,
3695            ArchiveReadOptions::with_password(b"password"),
3696        )
3697        .unwrap();
3698        let raw = archive.as_rar50().unwrap();
3699        let comment = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
3700        assert_eq!(comment.data, b"facade header encrypted comment\n");
3701        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3702        assert_eq!(
3703            extracted[0].data,
3704            b"facade rar5 header encrypted comment payload\n"
3705        );
3706    }
3707
3708    #[test]
3709    fn direct_writer_creates_rar50_header_encrypted_compressed_archive_with_comment() {
3710        let payload = b"facade rar5 header encrypted compressed comment payload\n".repeat(8);
3711        let mut features = FeatureSet::store_only();
3712        features.header_encryption = true;
3713        let bytes =
3714            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3715                .entries(
3716                    [
3717                        rar50_entry(b"rar5-header-compressed-comment-secret.txt", &payload)
3718                            .with_mtime(Some(0))
3719                            .with_attributes(0x20)
3720                            .with_host_os(3)
3721                            .with_password(b"password".to_vec()),
3722                    ]
3723                    .to_vec(),
3724                )
3725                .encrypted_archive_comment(
3726                    b"facade header encrypted compressed comment\n",
3727                    b"password",
3728                )
3729                .finish()
3730                .unwrap();
3731
3732        let archive = ArchiveReader::read_with_options(
3733            &bytes,
3734            ArchiveReadOptions::with_password(b"password"),
3735        )
3736        .unwrap();
3737        let raw = archive.as_rar50().unwrap();
3738        let comment = collect_rar50_file(raw, raw.services().next().unwrap()).unwrap();
3739        assert_eq!(
3740            comment.data,
3741            b"facade header encrypted compressed comment\n"
3742        );
3743        let extracted = collect_extract(&archive, Some(b"password")).unwrap();
3744        assert_eq!(extracted[0].data, payload);
3745    }
3746
3747    #[test]
3748    fn direct_writer_creates_rar50_encrypted_stored_archive_with_recovery() {
3749        let features = FeatureSet::store_only();
3750        let bytes = rar50::Rar50Writer::new(
3751            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3752        )
3753        .entries(
3754            [rar50_entry(
3755                b"rar5-encrypted-recovery.txt",
3756                b"facade rar5 encrypted recovery payload\n",
3757            )
3758            .with_attributes(0x20)
3759            .with_host_os(3)
3760            .with_password(b"password".to_vec())]
3761            .to_vec(),
3762        )
3763        .recovery_percent(Some(6))
3764        .finish()
3765        .unwrap();
3766
3767        let archive = ArchiveReader::read_with_options(
3768            &bytes,
3769            ArchiveReadOptions::with_password(b"password"),
3770        )
3771        .unwrap();
3772        let raw = archive.as_rar50().unwrap();
3773        assert!(raw.main.has_recovery_record());
3774        let service = raw.services().next().unwrap();
3775        assert_eq!(service.name, b"RR");
3776        assert_eq!(service.recovery_record().unwrap().unwrap().percent, 6);
3777        let recovery_data = collect_rar50_file(raw, service).unwrap().data;
3778        assert!(recovery_data.starts_with(b"{RB}"));
3779        assert_eq!(
3780            u32::from_le_bytes(recovery_data[0x0c..0x10].try_into().unwrap()) as usize,
3781            recovery_data.len()
3782        );
3783        let extracted = collect_extract(&archive, None).unwrap();
3784        assert_eq!(
3785            extracted[0].data,
3786            b"facade rar5 encrypted recovery payload\n"
3787        );
3788    }
3789
3790    #[test]
3791    fn direct_writer_creates_rar50_encrypted_compressed_archive_with_recovery() {
3792        let features = FeatureSet::store_only();
3793        let payload = b"facade rar5 encrypted compressed recovery payload repeated\n".repeat(8);
3794        let bytes =
3795            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3796                .entries(
3797                    [
3798                        rar50_entry(b"rar5-encrypted-compressed-recovery.txt", &payload)
3799                            .with_attributes(0x20)
3800                            .with_host_os(3)
3801                            .with_password(b"password".to_vec()),
3802                    ]
3803                    .to_vec(),
3804                )
3805                .recovery_percent(Some(6))
3806                .finish()
3807                .unwrap();
3808
3809        let archive = ArchiveReader::read_with_options(
3810            &bytes,
3811            ArchiveReadOptions::with_password(b"password"),
3812        )
3813        .unwrap();
3814        let raw = archive.as_rar50().unwrap();
3815        assert!(raw.main.has_recovery_record());
3816        let service = raw.services().next().unwrap();
3817        assert_eq!(service.name, b"RR");
3818        assert_eq!(service.recovery_record().unwrap().unwrap().percent, 6);
3819        let extracted = collect_extract(&archive, None).unwrap();
3820        assert_eq!(extracted[0].data, payload);
3821    }
3822
3823    #[test]
3824    fn direct_writer_creates_rar50_header_encrypted_stored_archive_with_recovery() {
3825        let mut features = FeatureSet::store_only();
3826        features.header_encryption = true;
3827        let bytes = rar50::Rar50Writer::new(
3828            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3829        )
3830        .entries(
3831            [rar50_entry(
3832                b"rar5-header-recovery.txt",
3833                b"facade rar5 header encrypted recovery payload\n",
3834            )
3835            .with_attributes(0x20)
3836            .with_host_os(3)
3837            .with_password(b"password".to_vec())]
3838            .to_vec(),
3839        )
3840        .recovery_percent(Some(4))
3841        .finish()
3842        .unwrap();
3843
3844        assert!(matches!(
3845            ArchiveReader::read(&bytes),
3846            Err(Error::NeedPassword)
3847        ));
3848        let archive = ArchiveReader::read_with_options(
3849            &bytes,
3850            ArchiveReadOptions::with_password(b"password"),
3851        )
3852        .unwrap();
3853        let raw = archive.as_rar50().unwrap();
3854        assert!(raw.main.has_recovery_record());
3855        let service = raw.services().next().unwrap();
3856        assert_eq!(service.name, b"RR");
3857        assert!(!service.encrypted);
3858        assert_eq!(service.recovery_record().unwrap().unwrap().percent, 4);
3859        let recovery_data = collect_rar50_file(raw, service).unwrap().data;
3860        assert!(recovery_data.starts_with(b"{RB}"));
3861        assert_eq!(
3862            u32::from_le_bytes(recovery_data[0x0c..0x10].try_into().unwrap()) as usize,
3863            recovery_data.len()
3864        );
3865        let extracted = collect_extract(&archive, None).unwrap();
3866        assert_eq!(
3867            extracted[0].data,
3868            b"facade rar5 header encrypted recovery payload\n"
3869        );
3870    }
3871
3872    #[test]
3873    fn direct_writer_creates_rar50_header_encrypted_compressed_archive_with_recovery() {
3874        let mut features = FeatureSet::store_only();
3875        features.header_encryption = true;
3876        let payload =
3877            b"facade rar5 header encrypted compressed recovery payload repeated\n".repeat(8);
3878        let bytes =
3879            rar50::Rar50Writer::new(rar50_options_with_features(ArchiveVersion::Rar50, features))
3880                .entries(
3881                    [
3882                        rar50_entry(b"rar5-header-compressed-recovery.txt", &payload)
3883                            .with_attributes(0x20)
3884                            .with_host_os(3)
3885                            .with_password(b"password".to_vec()),
3886                    ]
3887                    .to_vec(),
3888                )
3889                .recovery_percent(Some(4))
3890                .finish()
3891                .unwrap();
3892
3893        assert!(matches!(
3894            ArchiveReader::read(&bytes),
3895            Err(Error::NeedPassword)
3896        ));
3897        let archive = ArchiveReader::read_with_options(
3898            &bytes,
3899            ArchiveReadOptions::with_password(b"password"),
3900        )
3901        .unwrap();
3902        let raw = archive.as_rar50().unwrap();
3903        assert!(raw.main.has_recovery_record());
3904        let service = raw.services().next().unwrap();
3905        assert_eq!(service.name, b"RR");
3906        assert!(!service.encrypted);
3907        assert_eq!(service.recovery_record().unwrap().unwrap().percent, 4);
3908        let extracted = collect_extract(&archive, None).unwrap();
3909        assert_eq!(extracted[0].data, payload);
3910    }
3911
3912    #[test]
3913    fn direct_writer_creates_rar50_encrypted_stored_volumes() {
3914        let features = FeatureSet::store_only();
3915        let payload = b"facade rar5 encrypted split payload\n".repeat(12);
3916        let parts = write_rar50_volume_set(
3917            &[rar50_entry(b"split-secret50.txt", &payload)
3918                .with_attributes(0x20)
3919                .with_host_os(3)
3920                .with_password(b"password".to_vec())],
3921            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3922            16,
3923            None,
3924        );
3925        let archives: Vec<_> = parts
3926            .iter()
3927            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
3928            .collect();
3929
3930        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
3931        assert_eq!(extracted[0].name, b"split-secret50.txt");
3932        assert_eq!(extracted[0].data, payload);
3933    }
3934
3935    #[test]
3936    fn direct_writer_creates_rar50_encrypted_stored_volumes_with_recovery() {
3937        let features = FeatureSet::store_only();
3938        let payload = b"facade rar5 encrypted recovery split payload\n".repeat(12);
3939        let parts = write_rar50_volume_set(
3940            &[rar50_entry(b"split-secret50-rr.txt", &payload)
3941                .with_attributes(0x20)
3942                .with_host_os(3)
3943                .with_password(b"password".to_vec())],
3944            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3945            16,
3946            Some(8),
3947        );
3948        let archives: Vec<_> = parts
3949            .iter()
3950            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
3951            .collect();
3952        assert_rar50_volume_recovery_records(&archives, 8);
3953
3954        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
3955        assert_eq!(extracted[0].name, b"split-secret50-rr.txt");
3956        assert_eq!(extracted[0].data, payload);
3957    }
3958
3959    #[test]
3960    fn direct_writer_creates_rar50_header_encrypted_stored_volumes_with_recovery() {
3961        let mut features = FeatureSet::store_only();
3962        features.header_encryption = true;
3963        let payload = b"facade rar5 header encrypted recovery split payload\n".repeat(4);
3964        let parts = write_rar50_volume_set(
3965            &[rar50_entry(b"split-header-secret50-rr.txt", &payload)
3966                .with_attributes(0x20)
3967                .with_host_os(3)
3968                .with_password(b"password".to_vec())],
3969            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3970            16,
3971            Some(8),
3972        );
3973        assert!(matches!(
3974            rar50::Archive::parse(&parts[0]),
3975            Err(Error::NeedPassword)
3976        ));
3977        let archives: Vec<_> = parts
3978            .iter()
3979            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
3980            .collect();
3981        assert_rar50_volume_recovery_records(&archives, 8);
3982
3983        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
3984        assert_eq!(extracted[0].name, b"split-header-secret50-rr.txt");
3985        assert_eq!(extracted[0].data, payload);
3986    }
3987
3988    #[test]
3989    fn direct_writer_creates_rar50_header_encrypted_stored_volumes() {
3990        let mut features = FeatureSet::store_only();
3991        features.header_encryption = true;
3992        let payload = b"facade rar5 header encrypted split payload\n".repeat(12);
3993        let parts = write_rar50_volume_set(
3994            &[rar50_entry(b"split-header-secret50.txt", &payload)
3995                .with_attributes(0x20)
3996                .with_host_os(3)
3997                .with_password(b"password".to_vec())],
3998            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
3999            16,
4000            None,
4001        );
4002        assert!(matches!(
4003            rar50::Archive::parse(&parts[0]),
4004            Err(Error::NeedPassword)
4005        ));
4006        let archives: Vec<_> = parts
4007            .iter()
4008            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
4009            .collect();
4010
4011        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
4012        assert_eq!(extracted[0].name, b"split-header-secret50.txt");
4013        assert_eq!(extracted[0].data, payload);
4014    }
4015
4016    #[test]
4017    fn direct_writer_creates_rar50_encrypted_compressed_volumes() {
4018        let features = FeatureSet::store_only();
4019        let payload = b"facade rar5 encrypted compressed split payload\n".repeat(12);
4020        let parts = write_rar50_volume_set(
4021            &[rar50_entry(b"split-secret-compressed50.txt", &payload)
4022                .with_attributes(0x20)
4023                .with_host_os(3)
4024                .with_password(b"password".to_vec())],
4025            rar50_options_with_features(ArchiveVersion::Rar50, features),
4026            32,
4027            None,
4028        );
4029        let archives: Vec<_> = parts
4030            .iter()
4031            .map(|part| rar50::Archive::parse(part).unwrap())
4032            .collect();
4033
4034        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
4035        assert_eq!(extracted[0].name, b"split-secret-compressed50.txt");
4036        assert_eq!(extracted[0].data, payload);
4037    }
4038
4039    #[test]
4040    fn direct_writer_creates_rar50_encrypted_compressed_volumes_with_recovery() {
4041        let features = FeatureSet::store_only();
4042        let payload = b"facade rar5 encrypted compressed recovery split payload\n".repeat(12);
4043        let entries = [rar50_entry(b"split-secret-compressed50-rr.txt", &payload)
4044            .with_attributes(0x20)
4045            .with_host_os(3)
4046            .with_password(b"password".to_vec())];
4047        let parts = write_rar50_volume_set(
4048            &entries,
4049            rar50_options_with_features(ArchiveVersion::Rar50, features),
4050            32,
4051            Some(8),
4052        );
4053        let archives: Vec<_> = parts
4054            .iter()
4055            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
4056            .collect();
4057        assert_rar50_volume_recovery_records(&archives, 8);
4058
4059        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
4060        assert_eq!(extracted[0].name, b"split-secret-compressed50-rr.txt");
4061        assert_eq!(extracted[0].data, payload);
4062    }
4063
4064    #[test]
4065    fn direct_writer_creates_rar50_header_encrypted_compressed_volumes_with_recovery() {
4066        let mut features = FeatureSet::store_only();
4067        features.header_encryption = true;
4068        let payload =
4069            b"facade rar5 header encrypted compressed recovery split payload\n".repeat(12);
4070        let entries = [
4071            rar50_entry(b"split-header-secret-compressed50-rr.txt", &payload)
4072                .with_attributes(0x20)
4073                .with_host_os(3)
4074                .with_password(b"password".to_vec()),
4075        ];
4076        let parts = write_rar50_volume_set(
4077            &entries,
4078            rar50_options_with_features(ArchiveVersion::Rar50, features),
4079            32,
4080            Some(8),
4081        );
4082        assert!(matches!(
4083            rar50::Archive::parse(&parts[0]),
4084            Err(Error::NeedPassword)
4085        ));
4086        let archives: Vec<_> = parts
4087            .iter()
4088            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
4089            .collect();
4090        assert_rar50_volume_recovery_records(&archives, 8);
4091
4092        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
4093        assert_eq!(
4094            extracted[0].name,
4095            b"split-header-secret-compressed50-rr.txt"
4096        );
4097        assert_eq!(extracted[0].data, payload);
4098    }
4099
4100    #[test]
4101    fn direct_writer_creates_rar50_encrypted_solid_compressed_volumes() {
4102        let mut features = FeatureSet::store_only();
4103        features.solid = true;
4104        let payload = b"facade rar5 encrypted solid compressed split payload\n".repeat(12);
4105        let parts = write_rar50_volume_set(
4106            &[
4107                rar50_entry(b"split-solid-secret-compressed50.txt", &payload)
4108                    .with_attributes(0x20)
4109                    .with_host_os(3)
4110                    .with_password(b"password".to_vec()),
4111            ],
4112            rar50_options_with_features(ArchiveVersion::Rar50, features),
4113            32,
4114            None,
4115        );
4116        let archives: Vec<_> = parts
4117            .iter()
4118            .map(|part| rar50::Archive::parse(part).unwrap())
4119            .collect();
4120        assert!(archives.iter().all(|archive| archive.main.is_solid()));
4121
4122        let extracted = collect_rar50_volumes(&archives, Some(b"password")).unwrap();
4123        assert_eq!(extracted[0].name, b"split-solid-secret-compressed50.txt");
4124        assert_eq!(extracted[0].data, payload);
4125    }
4126
4127    #[test]
4128    fn direct_writer_creates_rar50_header_encrypted_compressed_volumes() {
4129        let mut features = FeatureSet::store_only();
4130        features.header_encryption = true;
4131        let payload: Vec<u8> = (0..512).map(|index| (index * 37 + 11) as u8).collect();
4132        let parts = write_rar50_volume_set(
4133            &[
4134                rar50_entry(b"split-header-secret-compressed50.txt", &payload)
4135                    .with_attributes(0x20)
4136                    .with_host_os(3)
4137                    .with_password(b"password".to_vec()),
4138            ],
4139            rar50_options_with_features(ArchiveVersion::Rar50, features),
4140            64,
4141            None,
4142        );
4143        assert!(matches!(
4144            rar50::Archive::parse(&parts[0]),
4145            Err(Error::NeedPassword)
4146        ));
4147        let archives: Vec<_> = parts
4148            .iter()
4149            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
4150            .collect();
4151
4152        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4153        assert_eq!(extracted[0].name, b"split-header-secret-compressed50.txt");
4154        assert_eq!(extracted[0].data, payload);
4155    }
4156
4157    #[test]
4158    fn direct_writer_creates_rar50_header_encrypted_solid_compressed_volumes() {
4159        let mut features = FeatureSet::store_only();
4160        features.header_encryption = true;
4161        features.solid = true;
4162        let payload = b"facade rar5 header encrypted solid compressed split payload\n".repeat(12);
4163        let parts = write_rar50_volume_set(
4164            &[
4165                rar50_entry(b"split-header-solid-secret-compressed50.txt", &payload)
4166                    .with_attributes(0x20)
4167                    .with_host_os(3)
4168                    .with_password(b"password".to_vec()),
4169            ],
4170            rar50_options_with_features(ArchiveVersion::Rar50, features),
4171            32,
4172            None,
4173        );
4174        assert!(matches!(
4175            rar50::Archive::parse(&parts[0]),
4176            Err(Error::NeedPassword)
4177        ));
4178        let archives: Vec<_> = parts
4179            .iter()
4180            .map(|part| rar50::Archive::parse_with_password(part, Some(b"password")).unwrap())
4181            .collect();
4182        assert!(archives.iter().all(|archive| archive.main.is_solid()));
4183
4184        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4185        assert_eq!(
4186            extracted[0].name,
4187            b"split-header-solid-secret-compressed50.txt"
4188        );
4189        assert_eq!(extracted[0].data, payload);
4190    }
4191
4192    #[test]
4193    fn direct_writer_creates_rar50_stored_volumes() {
4194        let payload = b"facade rar5 stored split payload\n".repeat(20);
4195        let parts = write_rar50_volume_set(
4196            &[rar50_entry(b"split50.txt", &payload)
4197                .with_attributes(0x20)
4198                .with_host_os(3)],
4199            rar50_options(ArchiveVersion::Rar50).with_compression_level(0),
4200            80,
4201            None,
4202        );
4203        let archives: Vec<_> = parts
4204            .iter()
4205            .map(|part| rar50::Archive::parse(part).unwrap())
4206            .collect();
4207        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4208
4209        assert_eq!(extracted[0].name, b"split50.txt");
4210        assert_eq!(extracted[0].data, payload);
4211    }
4212
4213    #[test]
4214    fn direct_writer_creates_rar50_stored_volumes_with_recovery() {
4215        let features = FeatureSet::store_only();
4216        let payload = b"facade rar5 stored recovery split payload\n".repeat(20);
4217        let parts = write_rar50_volume_set(
4218            &[rar50_entry(b"split50-rr.txt", &payload)
4219                .with_attributes(0x20)
4220                .with_host_os(3)],
4221            rar50_options_with_features(ArchiveVersion::Rar50, features).with_compression_level(0),
4222            80,
4223            Some(8),
4224        );
4225        let archives: Vec<_> = parts
4226            .iter()
4227            .map(|part| rar50::Archive::parse(part).unwrap())
4228            .collect();
4229        assert_rar50_volume_recovery_records(&archives, 8);
4230        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4231
4232        assert_eq!(extracted[0].name, b"split50-rr.txt");
4233        assert_eq!(extracted[0].data, payload);
4234    }
4235
4236    #[test]
4237    fn direct_writer_creates_rar50_compressed_volumes() {
4238        let payload: Vec<u8> = (0..512).map(|index| (index * 53 + 17) as u8).collect();
4239        let parts = write_rar50_volume_set(
4240            &[rar50_entry(b"split-compressed50.txt", &payload)
4241                .with_attributes(0x20)
4242                .with_host_os(3)],
4243            rar50_options(ArchiveVersion::Rar50),
4244            64,
4245            None,
4246        );
4247        let archives: Vec<_> = parts
4248            .iter()
4249            .map(|part| rar50::Archive::parse(part).unwrap())
4250            .collect();
4251        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4252
4253        assert_eq!(extracted[0].name, b"split-compressed50.txt");
4254        assert_eq!(extracted[0].data, payload);
4255    }
4256
4257    #[test]
4258    fn direct_writer_creates_rar50_compressed_volumes_with_recovery() {
4259        let features = FeatureSet::store_only();
4260        let payload: Vec<u8> = (0..512).map(|index| (index * 53 + 17) as u8).collect();
4261        let entries = [rar50_entry(b"split-compressed50-rr.txt", &payload)
4262            .with_attributes(0x20)
4263            .with_host_os(3)];
4264        let parts = write_rar50_volume_set(
4265            &entries,
4266            rar50_options_with_features(ArchiveVersion::Rar50, features),
4267            64,
4268            Some(8),
4269        );
4270        let archives: Vec<_> = parts
4271            .iter()
4272            .map(|part| rar50::Archive::parse(part).unwrap())
4273            .collect();
4274        assert_rar50_volume_recovery_records(&archives, 8);
4275        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4276
4277        assert_eq!(extracted[0].name, b"split-compressed50-rr.txt");
4278        assert_eq!(extracted[0].data, payload);
4279    }
4280
4281    #[test]
4282    fn direct_writer_creates_rar50_solid_compressed_volumes() {
4283        let mut features = FeatureSet::store_only();
4284        features.solid = true;
4285        let payload = b"facade rar5 solid compressed split payload\n".repeat(12);
4286        let parts = write_rar50_volume_set(
4287            &[rar50_entry(b"split-solid-compressed50.txt", &payload)
4288                .with_attributes(0x20)
4289                .with_host_os(3)],
4290            rar50_options_with_features(ArchiveVersion::Rar50, features),
4291            32,
4292            None,
4293        );
4294        let archives: Vec<_> = parts
4295            .iter()
4296            .map(|part| rar50::Archive::parse(part).unwrap())
4297            .collect();
4298        assert!(archives.iter().all(|archive| archive.main.is_solid()));
4299        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4300
4301        assert_eq!(extracted[0].name, b"split-solid-compressed50.txt");
4302        assert_eq!(extracted[0].data, payload);
4303    }
4304
4305    #[test]
4306    fn direct_writer_creates_rar50_multi_file_solid_compressed_volumes() {
4307        let mut features = FeatureSet::store_only();
4308        features.solid = true;
4309        let mut first = b"facade rar5 multi-file solid split shared phrase\n"
4310            .repeat(8)
4311            .to_vec();
4312        first.extend_from_slice(&deterministic_noise(2048));
4313        let mut second = b"facade rar5 multi-file solid split shared phrase\nsecond\n"
4314            .repeat(8)
4315            .to_vec();
4316        second.extend_from_slice(&deterministic_noise(2048));
4317        let entries = [
4318            rar50_entry(b"solid-volume-one.txt", &first)
4319                .with_attributes(0x20)
4320                .with_host_os(3),
4321            rar50_entry(b"solid-volume-two.txt", &second)
4322                .with_attributes(0x20)
4323                .with_host_os(3),
4324        ];
4325        let parts = write_rar50_volume_set(
4326            &entries,
4327            rar50_options_with_features(ArchiveVersion::Rar50, features),
4328            512,
4329            None,
4330        );
4331        let archives: Vec<_> = parts
4332            .iter()
4333            .map(|part| rar50::Archive::parse(part).unwrap())
4334            .collect();
4335        assert!(archives.iter().all(|archive| archive.main.is_solid()));
4336        let extracted = collect_rar50_volumes(&archives, None).unwrap();
4337
4338        assert_eq!(extracted[0].name, b"solid-volume-one.txt");
4339        assert_eq!(extracted[0].data, first);
4340        assert_eq!(extracted[1].name, b"solid-volume-two.txt");
4341        assert_eq!(extracted[1].data, second);
4342    }
4343
4344    #[test]
4345    fn archive_as_rar13_returns_some_only_for_rar13_family() {
4346        let bytes = rar13::write_stored_archive(
4347            &[rar13::StoredEntry {
4348                name: b"old.txt",
4349                data: b"r13 downcast",
4350                file_time: 0,
4351                file_attr: 0x20,
4352                password: None,
4353                file_comment: None,
4354            }],
4355            rar13_options(ArchiveVersion::Rar14),
4356        )
4357        .unwrap();
4358        let archive = ArchiveReader::read(&bytes).unwrap();
4359        let raw = archive.as_rar13().unwrap();
4360        assert_eq!(raw.entries[0].name, b"old.txt");
4361        assert!(archive.as_rar15_40().is_none());
4362        assert!(archive.as_rar50().is_none());
4363
4364        // Other-family archives should refuse the rar13 downcast.
4365        let rar15_bytes = rar15_40::write_stored_archive(
4366            &[rar15_40::StoredEntry {
4367                name: b"mid.txt",
4368                data: b"r15 downcast",
4369                file_time: 0,
4370                file_attr: 0x20,
4371                host_os: 3,
4372                password: None,
4373                file_comment: None,
4374            }],
4375            rar15_options(ArchiveVersion::Rar15),
4376        )
4377        .unwrap();
4378        let rar15_archive = ArchiveReader::read(&rar15_bytes).unwrap();
4379        assert!(rar15_archive.as_rar13().is_none());
4380
4381        let rar50_bytes =
4382            rar50::Rar50Writer::new(rar50_options(ArchiveVersion::Rar50).with_compression_level(0))
4383                .entries(
4384                    [rar50_entry(b"new.txt", b"r50 downcast")
4385                        .with_attributes(0x20)
4386                        .with_host_os(3)]
4387                    .to_vec(),
4388                )
4389                .finish()
4390                .unwrap();
4391        let rar50_archive = ArchiveReader::read(&rar50_bytes).unwrap();
4392        assert!(rar50_archive.as_rar13().is_none());
4393    }
4394
4395    #[test]
4396    fn archive_facade_repair_recovery_returns_full_repaired_archive_bytes() {
4397        let bytes = std::fs::read(rar15_40_fixture("rar250_protect_head_rr5.rar")).unwrap();
4398        let mut damaged = bytes.clone();
4399        damaged[512 + 16..512 + 80].fill(0xa5);
4400        let damaged_archive = ArchiveReader::read(&damaged).unwrap();
4401
4402        let repaired = damaged_archive.repair_recovery().unwrap();
4403        assert_eq!(repaired, bytes);
4404    }
4405
4406    #[test]
4407    fn archive_facade_repair_recovery_rejects_rar13_archives() {
4408        let bytes = rar13::write_stored_archive(
4409            &[rar13::StoredEntry {
4410                name: b"old.txt",
4411                data: b"old data",
4412                file_time: 0,
4413                file_attr: 0x20,
4414                password: None,
4415                file_comment: None,
4416            }],
4417            rar13_options(ArchiveVersion::Rar14),
4418        )
4419        .unwrap();
4420        let archive = ArchiveReader::read(&bytes).unwrap();
4421        assert_eq!(
4422            archive.repair_recovery(),
4423            Err(Error::UnsupportedFamilyFeature {
4424                family: ArchiveFamily::Rar13,
4425                feature: "recovery repair for RAR 1.3/1.4 archives",
4426            })
4427        );
4428    }
4429
4430    #[test]
4431    fn archive_reader_read_path_dispatches_to_default_options() {
4432        // Existing tests cover read_path_with_options; this ensures the
4433        // zero-arg convenience wrapper actually delegates to it.
4434        let archive =
4435            ArchiveReader::read_path(rar15_40_fixture("rar250_protect_head_rr5.rar")).unwrap();
4436        assert_eq!(archive.family(), ArchiveFamily::Rar15To40);
4437        assert!(archive.as_rar15_40().unwrap().main.has_recovery_record());
4438    }
4439}