Skip to main content

rars_format/
rar13.rs

1use crate::detect::{find_archive_start, ArchiveSignature, RAR13_SIGNATURE};
2use crate::error::{Error, Result};
3use crate::features::FeatureSet;
4use crate::io_util::{read_exact_at, read_u16, read_u32};
5pub(crate) use crate::source::ArchiveSource;
6use crate::version::{ArchiveFamily, ArchiveVersion};
7use rars_codec::rar13::{
8    unpack15_decode, unpack15_encode, unpack15_encode_with_options,
9    EncodeOptions as Rar15EncodeOptions, Unpack15, Unpack15Encoder,
10};
11use rars_crypto::rar13::{Rar13Cipher, Rar13DecryptReader};
12use std::fs::File;
13use std::io::{Read, Seek, SeekFrom, Write};
14use std::ops::Range;
15use std::path::Path;
16use std::sync::Arc;
17
18const MAIN_HEAD_SIZE: u16 = 7;
19const FILE_HEAD_BASE_SIZE: usize = 21;
20const MHD_VOLUME: u8 = 0x01;
21const MHD_COMMENT: u8 = 0x02;
22const MHD_SOLID: u8 = 0x08;
23const MHD_PACK_COMMENT: u8 = 0x10;
24const MHD_AV: u8 = 0x20;
25const MHD_ALWAYS_SET: u8 = 0x80;
26const RAR13_AV_PREFIX: &[u8; 6] = b"\x1ai\x6d\x02\xda\xae";
27const COPY_BUFFER_SIZE: usize = 64 * 1024;
28const LHD_SPLIT_BEFORE: u8 = 0x01;
29const LHD_SPLIT_AFTER: u8 = 0x02;
30const LHD_PASSWORD: u8 = 0x04;
31const LHD_COMMENT: u8 = 0x08;
32const LHD_SOLID: u8 = 0x10;
33const METHOD_STORE: u8 = 0;
34const METHOD_BEST: u8 = 5;
35const DEFAULT_UNP_VER: u8 = 2;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[non_exhaustive]
39pub struct MainHeader {
40    pub flags: u8,
41    pub head_size: u16,
42    pub extra: Vec<u8>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct FileHeader {
48    pub flags: u8,
49    pub pack_size: u32,
50    pub unp_size: u32,
51    pub file_crc: u16,
52    pub file_time: u32,
53    pub file_attr: u8,
54    pub unp_ver: u8,
55    pub method: u8,
56    pub head_size: u16,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct Entry {
62    pub header: FileHeader,
63    pub name: Vec<u8>,
64    pub extra: Vec<u8>,
65    pub packed_range: Range<usize>,
66}
67
68#[derive(Debug, Clone)]
69#[non_exhaustive]
70pub struct Archive {
71    pub sfx_offset: usize,
72    pub main: MainHeader,
73    pub entries: Vec<Entry>,
74    source: ArchiveSource,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78#[non_exhaustive]
79pub struct AuthenticityVerification {
80    pub size: u16,
81    pub prefix: [u8; 6],
82    pub cipher_body: Vec<u8>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum AuthenticityVerificationStatus {
88    Absent,
89    StructurallyPresent,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[non_exhaustive]
94pub struct ExtractedEntryMeta {
95    pub name: Vec<u8>,
96    pub file_time: u32,
97    pub file_attr: u8,
98    pub is_directory: bool,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[non_exhaustive]
103pub struct WriterOptions {
104    pub target: ArchiveVersion,
105    pub features: FeatureSet,
106    pub compression_level: Option<u8>,
107}
108
109impl WriterOptions {
110    pub const fn new(target: ArchiveVersion, features: FeatureSet) -> Self {
111        Self {
112            target,
113            features,
114            compression_level: None,
115        }
116    }
117
118    pub const fn with_compression_level(mut self, level: u8) -> Self {
119        self.compression_level = Some(level);
120        self
121    }
122}
123
124impl Default for WriterOptions {
125    fn default() -> Self {
126        Self {
127            target: ArchiveVersion::Rar14,
128            features: FeatureSet::store_only(),
129            compression_level: None,
130        }
131    }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct StoredEntry<'a> {
136    pub name: &'a [u8],
137    pub data: &'a [u8],
138    pub file_time: u32,
139    pub file_attr: u8,
140    pub password: Option<&'a [u8]>,
141    pub file_comment: Option<&'a [u8]>,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct FileEntry<'a> {
146    pub name: &'a [u8],
147    pub data: &'a [u8],
148    pub file_time: u32,
149    pub file_attr: u8,
150    pub password: Option<&'a [u8]>,
151    pub file_comment: Option<&'a [u8]>,
152}
153
154impl MainHeader {
155    pub fn is_volume(&self) -> bool {
156        self.flags & MHD_VOLUME != 0
157    }
158
159    pub fn has_archive_comment(&self) -> bool {
160        self.flags & MHD_COMMENT != 0
161    }
162
163    pub fn has_packed_comment(&self) -> bool {
164        self.flags & MHD_PACK_COMMENT != 0
165    }
166
167    pub fn is_solid(&self) -> bool {
168        self.flags & MHD_SOLID != 0
169    }
170
171    pub fn has_authenticity_verification(&self) -> bool {
172        self.flags & MHD_AV != 0
173    }
174
175    fn parse(input: &[u8]) -> Result<Self> {
176        if input.len() < MAIN_HEAD_SIZE as usize {
177            return Err(Error::TooShort);
178        }
179        if !input.starts_with(RAR13_SIGNATURE) {
180            return Err(Error::UnsupportedSignature);
181        }
182
183        let head_size = read_u16(input, 4)?;
184        let flags = input[6];
185        if head_size < MAIN_HEAD_SIZE {
186            return Err(Error::InvalidHeader(
187                "RAR 1.3 main header is shorter than 7 bytes",
188            ));
189        }
190        if head_size as usize > input.len() {
191            return Err(Error::TooShort);
192        }
193
194        let extra = input[MAIN_HEAD_SIZE as usize..head_size as usize].to_vec();
195
196        Ok(Self {
197            flags,
198            head_size,
199            extra,
200        })
201    }
202}
203
204impl FileHeader {
205    fn parse(input: &[u8]) -> Result<(Self, Vec<u8>, Vec<u8>, usize)> {
206        if input.len() < FILE_HEAD_BASE_SIZE {
207            return Err(Error::TooShort);
208        }
209
210        let pack_size = read_u32(input, 0)?;
211        let unp_size = read_u32(input, 4)?;
212        let file_crc = read_u16(input, 8)?;
213        let head_size = read_u16(input, 10)?;
214        let file_time = read_u32(input, 12)?;
215        let file_attr = input[16];
216        let flags = input[17];
217        let unp_ver = input[18];
218        let name_size = input[19] as usize;
219        let method = input[20];
220        let minimum_size = FILE_HEAD_BASE_SIZE + name_size;
221
222        if (head_size as usize) < minimum_size {
223            return Err(Error::InvalidHeader(
224                "RAR 1.3 file header is shorter than its name",
225            ));
226        }
227        if input.len() < head_size as usize {
228            return Err(Error::TooShort);
229        }
230
231        let name = input[FILE_HEAD_BASE_SIZE..FILE_HEAD_BASE_SIZE + name_size].to_vec();
232        let extra = input[minimum_size..head_size as usize].to_vec();
233        Ok((
234            Self {
235                flags,
236                pack_size,
237                unp_size,
238                file_crc,
239                file_time,
240                file_attr,
241                unp_ver,
242                method,
243                head_size,
244            },
245            name,
246            extra,
247            head_size as usize,
248        ))
249    }
250}
251
252impl Archive {
253    pub fn parse(input: &[u8]) -> Result<Self> {
254        let data: Arc<[u8]> = Arc::from(input.to_vec().into_boxed_slice());
255        Self::parse_shared(data)
256    }
257
258    pub fn parse_owned(input: Vec<u8>) -> Result<Self> {
259        Self::parse_shared(Arc::from(input.into_boxed_slice()))
260    }
261
262    pub fn parse_path(path: impl AsRef<Path>) -> Result<Self> {
263        let path = Arc::new(path.as_ref().to_path_buf());
264        let mut file = File::open(path.as_ref())?;
265        let len = file.metadata()?.len();
266        let scan_len = len.min(128 * 1024) as usize;
267        let mut scan = vec![0; scan_len];
268        file.read_exact(&mut scan)?;
269        let sig = find_archive_start(&scan, 128 * 1024).ok_or(Error::UnsupportedSignature)?;
270        if sig.family != ArchiveFamily::Rar13 {
271            return Err(Error::UnsupportedSignature);
272        }
273        Self::parse_seekable(file, len, sig.offset, ArchiveSource::File(path))
274    }
275
276    pub fn parse_path_with_signature(
277        path: impl AsRef<Path>,
278        signature: ArchiveSignature,
279    ) -> Result<Self> {
280        if signature.family != ArchiveFamily::Rar13 {
281            return Err(Error::UnsupportedSignature);
282        }
283        let path = Arc::new(path.as_ref().to_path_buf());
284        let file = File::open(path.as_ref())?;
285        let len = file.metadata()?.len();
286        Self::parse_seekable(file, len, signature.offset, ArchiveSource::File(path))
287    }
288
289    fn parse_shared(input: Arc<[u8]>) -> Result<Self> {
290        let sig = find_archive_start(&input, 128 * 1024).ok_or(Error::UnsupportedSignature)?;
291        if sig.family != ArchiveFamily::Rar13 {
292            return Err(Error::UnsupportedSignature);
293        }
294
295        let archive = &input[sig.offset..];
296        let main = MainHeader::parse(archive)?;
297        let mut pos = main.head_size as usize;
298        let mut entries = Vec::new();
299
300        while pos < archive.len() {
301            if archive.len() - pos < FILE_HEAD_BASE_SIZE {
302                break;
303            }
304
305            let (header, name, extra, consumed) = FileHeader::parse(&archive[pos..])?;
306            let data_start = pos + consumed;
307            let data_end =
308                data_start
309                    .checked_add(header.pack_size as usize)
310                    .ok_or(Error::InvalidHeader(
311                        "RAR 1.3 file data size overflows usize",
312                    ))?;
313            if data_end > archive.len() {
314                return Err(Error::TooShort);
315            }
316
317            entries.push(Entry {
318                header,
319                name,
320                extra,
321                packed_range: sig.offset + data_start..sig.offset + data_end,
322            });
323            pos = data_end;
324        }
325
326        Ok(Self {
327            sfx_offset: sig.offset,
328            main,
329            entries,
330            source: ArchiveSource::Memory(input),
331        })
332    }
333
334    fn parse_seekable(
335        mut file: File,
336        file_len: u64,
337        sfx_offset: usize,
338        source: ArchiveSource,
339    ) -> Result<Self> {
340        let main_prefix = read_exact_at(&mut file, sfx_offset, MAIN_HEAD_SIZE as usize)?;
341        let head_size = read_u16(&main_prefix, 4)? as usize;
342        let main_bytes = read_exact_at(&mut file, sfx_offset, head_size)?;
343        let main = MainHeader::parse(&main_bytes)?;
344        let mut pos = main.head_size as usize;
345        let mut entries = Vec::new();
346
347        while (sfx_offset + pos) as u64 + FILE_HEAD_BASE_SIZE as u64 <= file_len {
348            let header_prefix = read_exact_at(&mut file, sfx_offset + pos, FILE_HEAD_BASE_SIZE)?;
349            let head_size = read_u16(&header_prefix, 10)? as usize;
350            let header_bytes = read_exact_at(&mut file, sfx_offset + pos, head_size)?;
351            let (header, name, extra, consumed) = FileHeader::parse(&header_bytes)?;
352            let data_start = pos + consumed;
353            let data_end =
354                data_start
355                    .checked_add(header.pack_size as usize)
356                    .ok_or(Error::InvalidHeader(
357                        "RAR 1.3 file data size overflows usize",
358                    ))?;
359            if (sfx_offset + data_end) as u64 > file_len {
360                return Err(Error::TooShort);
361            }
362            entries.push(Entry {
363                header,
364                name,
365                extra,
366                packed_range: sfx_offset + data_start..sfx_offset + data_end,
367            });
368            pos = data_end;
369        }
370
371        Ok(Self {
372            sfx_offset,
373            main,
374            entries,
375            source,
376        })
377    }
378
379    fn copy_range_to(&self, range: Range<usize>, out: &mut impl Write) -> Result<()> {
380        self.source.copy_range_to(range, out)
381    }
382
383    fn range_reader(&self, range: Range<usize>) -> Result<Box<dyn Read + '_>> {
384        self.source.range_reader(range)
385    }
386
387    fn copy_decrypted_range_to(
388        &self,
389        range: Range<usize>,
390        mut cipher: Rar13Cipher,
391        out: &mut impl Write,
392    ) -> Result<()> {
393        let mut buffer = [0u8; COPY_BUFFER_SIZE];
394        match &self.source {
395            ArchiveSource::Memory(data) => {
396                let data = data.get(range).ok_or(Error::TooShort)?;
397                for chunk in data.chunks(COPY_BUFFER_SIZE) {
398                    buffer[..chunk.len()].copy_from_slice(chunk);
399                    for byte in &mut buffer[..chunk.len()] {
400                        *byte = cipher.decrypt_byte(*byte);
401                    }
402                    out.write_all(&buffer[..chunk.len()])?;
403                }
404            }
405            ArchiveSource::File(path) => {
406                let mut file = File::open(path.as_ref())?;
407                file.seek(SeekFrom::Start(range.start as u64))?;
408                let mut remaining = range.len();
409                while remaining > 0 {
410                    let to_read = remaining.min(buffer.len());
411                    file.read_exact(&mut buffer[..to_read])?;
412                    for byte in &mut buffer[..to_read] {
413                        *byte = cipher.decrypt_byte(*byte);
414                    }
415                    out.write_all(&buffer[..to_read])?;
416                    remaining -= to_read;
417                }
418            }
419        }
420        Ok(())
421    }
422
423    /// Streams extracted entries to caller-provided writers.
424    pub fn extract_to<F>(&self, password: Option<&[u8]>, mut open: F) -> Result<()>
425    where
426        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
427    {
428        let mut unpack15 = Unpack15::new();
429        let mut extracted_count = 0usize;
430        for entry in &self.entries {
431            if entry.is_split_before() || entry.is_split_after() {
432                return Err(Error::InvalidHeader(
433                    "RAR 1.3 split entry requires multivolume extraction",
434                ));
435            }
436            let meta = entry.metadata();
437            if meta.is_directory {
438                let _ = open(&meta)?;
439                extracted_count += 1;
440                continue;
441            }
442            let mut writer = open(&meta)?;
443            if entry.is_stored() && !entry.is_encrypted() {
444                entry
445                    .write_stored_to(self, password, &mut writer)
446                    .map_err(|error| entry.entry_error("extracting", error))?;
447            } else {
448                entry
449                    .write_compressed_to(
450                        self,
451                        password,
452                        &mut unpack15,
453                        self.main.is_solid() && extracted_count != 0,
454                        &mut writer,
455                    )
456                    .map_err(|error| entry.entry_error("extracting", error))?;
457            }
458            extracted_count += 1;
459        }
460        Ok(())
461    }
462
463    pub fn archive_comment(&self) -> Result<Option<Vec<u8>>> {
464        if !self.main.has_archive_comment() {
465            return Ok(None);
466        }
467
468        let length = read_u16(&self.main.extra, 0)? as usize;
469        if self.main.has_packed_comment() {
470            if length < 2 {
471                return Err(Error::InvalidHeader(
472                    "RAR 1.3 packed archive comment is shorter than size field",
473                ));
474            }
475            let unpacked_len = read_u16(&self.main.extra, 2)? as usize;
476            let packed_len = length - 2;
477            let packed_start = 4usize;
478            let packed_end = packed_start
479                .checked_add(packed_len)
480                .ok_or(Error::InvalidHeader(
481                    "RAR 1.3 archive comment size overflows",
482                ))?;
483            if packed_end > self.main.extra.len() {
484                return Err(Error::TooShort);
485            }
486
487            let mut packed = self.main.extra[packed_start..packed_end].to_vec();
488            Rar13Cipher::new_comment().decrypt_in_place(&mut packed);
489            return Ok(Some(unpack15_decode(&packed, unpacked_len)?));
490        }
491
492        let comment_start = 2usize;
493        let comment_end = comment_start
494            .checked_add(length)
495            .ok_or(Error::InvalidHeader(
496                "RAR 1.3 archive comment size overflows",
497            ))?;
498        if comment_end > self.main.extra.len() {
499            return Err(Error::TooShort);
500        }
501        Ok(Some(self.main.extra[comment_start..comment_end].to_vec()))
502    }
503
504    pub fn authenticity_verification(&self) -> Result<Option<AuthenticityVerification>> {
505        if !self.main.has_authenticity_verification() {
506            return Ok(None);
507        }
508        let size = read_u16(&self.main.extra, 0)?;
509        if size < RAR13_AV_PREFIX.len() as u16 {
510            return Err(Error::InvalidHeader("RAR 1.3 AV payload is too short"));
511        }
512        let payload_end = 2usize
513            .checked_add(size as usize)
514            .ok_or(Error::InvalidHeader("RAR 1.3 AV payload size overflows"))?;
515        if payload_end > self.main.extra.len() {
516            return Err(Error::TooShort);
517        }
518        let prefix_bytes = self
519            .main
520            .extra
521            .get(2..2 + RAR13_AV_PREFIX.len())
522            .ok_or(Error::TooShort)?;
523        let prefix: [u8; 6] = prefix_bytes
524            .try_into()
525            .expect("RAR 1.3 AV prefix slice has fixed length");
526        if &prefix != RAR13_AV_PREFIX {
527            return Err(Error::InvalidHeader("RAR 1.3 AV prefix mismatch"));
528        }
529        Ok(Some(AuthenticityVerification {
530            size,
531            prefix,
532            cipher_body: self.main.extra[2 + RAR13_AV_PREFIX.len()..payload_end].to_vec(),
533        }))
534    }
535
536    pub fn authenticity_verification_status(&self) -> Result<AuthenticityVerificationStatus> {
537        Ok(if self.authenticity_verification()?.is_some() {
538            AuthenticityVerificationStatus::StructurallyPresent
539        } else {
540            AuthenticityVerificationStatus::Absent
541        })
542    }
543}
544
545impl Entry {
546    pub fn name_bytes(&self) -> &[u8] {
547        &self.name
548    }
549
550    /// Returns the entry name with invalid UTF-8 replaced for display only.
551    ///
552    /// Use [`Self::name_bytes`] when exact archive bytes matter.
553    pub fn name_lossy(&self) -> String {
554        String::from_utf8_lossy(&self.name).into_owned()
555    }
556
557    pub fn is_encrypted(&self) -> bool {
558        self.header.flags & LHD_PASSWORD != 0
559    }
560
561    pub fn is_split_before(&self) -> bool {
562        self.header.flags & LHD_SPLIT_BEFORE != 0
563    }
564
565    pub fn is_split_after(&self) -> bool {
566        self.header.flags & LHD_SPLIT_AFTER != 0
567    }
568
569    pub fn is_directory(&self) -> bool {
570        self.header.file_attr & 0x10 != 0
571    }
572
573    pub fn has_file_comment(&self) -> bool {
574        self.header.flags & LHD_COMMENT != 0
575    }
576
577    pub fn file_comment(&self) -> Result<Option<Vec<u8>>> {
578        if !self.has_file_comment() {
579            return Ok(None);
580        }
581        let length = read_u16(&self.extra, 0)? as usize;
582        let comment_start = 2usize;
583        let comment_end = comment_start
584            .checked_add(length)
585            .ok_or(Error::InvalidHeader("RAR 1.3 file comment size overflows"))?;
586        if comment_end > self.extra.len() {
587            return Err(Error::TooShort);
588        }
589        Ok(Some(self.extra[comment_start..comment_end].to_vec()))
590    }
591
592    pub fn is_stored(&self) -> bool {
593        self.header.method == METHOD_STORE
594    }
595
596    pub fn packed_data<'a>(&self, archive: &'a Archive) -> Result<&'a [u8]> {
597        match &archive.source {
598            ArchiveSource::Memory(data) => {
599                data.get(self.packed_range.clone()).ok_or(Error::TooShort)
600            }
601            ArchiveSource::File(_) => Err(Error::InvalidHeader(
602                "RAR 1.3 file-backed packed data requires owned read",
603            )),
604        }
605    }
606
607    pub fn write_packed_data(&self, archive: &Archive, out: &mut impl Write) -> Result<()> {
608        archive.copy_range_to(self.packed_range.clone(), out)
609    }
610
611    pub fn verify_checksum(&self, data: &[u8]) -> Result<()> {
612        let actual = file_checksum(data);
613        if actual == self.header.file_crc {
614            Ok(())
615        } else {
616            Err(Error::CrcMismatch {
617                expected: self.header.file_crc,
618                actual,
619            })
620        }
621    }
622
623    pub fn metadata(&self) -> ExtractedEntryMeta {
624        ExtractedEntryMeta {
625            name: self.name.clone(),
626            file_time: self.header.file_time,
627            file_attr: self.header.file_attr,
628            is_directory: self.is_directory(),
629        }
630    }
631
632    fn write_stored_to(
633        &self,
634        archive: &Archive,
635        password: Option<&[u8]>,
636        out: &mut impl Write,
637    ) -> Result<()> {
638        if !self.is_stored() {
639            return Err(Error::InvalidHeader("RAR 1.3 entry is not stored"));
640        }
641        if self.is_encrypted() {
642            let password = password.ok_or(Error::NeedPassword)?;
643            let mut checksum = Rar13Checksum::new();
644            let mut checksum_writer = Rar13ChecksumWriter {
645                inner: out,
646                checksum: &mut checksum,
647            };
648            archive.copy_decrypted_range_to(
649                self.packed_range.clone(),
650                Rar13Cipher::new(password),
651                &mut checksum_writer,
652            )?;
653            let actual = checksum.finish();
654            return if actual == self.header.file_crc {
655                Ok(())
656            } else {
657                Err(Error::CrcMismatch {
658                    expected: self.header.file_crc,
659                    actual,
660                })
661            };
662        }
663        let mut checksum = Rar13Checksum::new();
664        let mut checksum_writer = Rar13ChecksumWriter {
665            inner: out,
666            checksum: &mut checksum,
667        };
668        self.write_packed_data(archive, &mut checksum_writer)?;
669        let actual = checksum.finish();
670        if actual == self.header.file_crc {
671            Ok(())
672        } else {
673            Err(Error::CrcMismatch {
674                expected: self.header.file_crc,
675                actual,
676            })
677        }
678    }
679
680    fn write_compressed_to(
681        &self,
682        archive: &Archive,
683        password: Option<&[u8]>,
684        unpack15: &mut Unpack15,
685        solid: bool,
686        out: &mut impl Write,
687    ) -> Result<()> {
688        if self.is_stored() || self.is_directory() {
689            return self.write_stored_to(archive, password, out);
690        }
691        let mut checksum = Rar13Checksum::new();
692        let mut checksum_writer = Rar13ChecksumWriter {
693            inner: out,
694            checksum: &mut checksum,
695        };
696        if self.is_encrypted() {
697            let password = password.ok_or(Error::NeedPassword)?;
698            let packed = archive.range_reader(self.packed_range.clone())?;
699            let mut packed = Rar13DecryptReader::new(packed, Rar13Cipher::new(password));
700            unpack15.decode_member_from_reader(
701                &mut packed,
702                self.header.unp_size as usize,
703                solid,
704                &mut checksum_writer,
705            )?;
706        } else {
707            let mut packed = archive.range_reader(self.packed_range.clone())?;
708            unpack15.decode_member_from_reader(
709                &mut packed,
710                self.header.unp_size as usize,
711                solid,
712                &mut checksum_writer,
713            )?;
714        }
715        let actual = checksum.finish();
716        if actual == self.header.file_crc {
717            Ok(())
718        } else {
719            Err(Error::CrcMismatch {
720                expected: self.header.file_crc,
721                actual,
722            })
723        }
724    }
725
726    pub fn write_to(
727        &self,
728        archive: &Archive,
729        password: Option<&[u8]>,
730        out: &mut impl Write,
731    ) -> Result<()> {
732        self.write_compressed_to(archive, password, &mut Unpack15::new(), false, out)
733    }
734
735    fn entry_error(&self, operation: &'static str, error: Error) -> Error {
736        if matches!(
737            error,
738            Error::NeedPassword | Error::WrongPasswordOrCorruptData
739        ) {
740            return error;
741        }
742        if self.is_encrypted()
743            && matches!(
744                error,
745                Error::InvalidHeader(_)
746                    | Error::Codec(_)
747                    | Error::CrcMismatch { .. }
748                    | Error::Crc32Mismatch { .. }
749                    | Error::HashMismatch { .. }
750            )
751        {
752            return Error::WrongPasswordOrCorruptData;
753        }
754        error.at_entry(self.name.clone(), operation)
755    }
756}
757
758/// Streams a multivolume archive set to caller-provided writers.
759pub fn extract_volumes_to<F>(
760    volumes: &[Archive],
761    password: Option<&[u8]>,
762    mut open: F,
763) -> Result<()>
764where
765    F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
766{
767    let mut pending: Option<PendingSplitRefs> = None;
768    let mut unpack15 = Unpack15::new();
769    let mut extracted_count = 0usize;
770
771    for (volume_index, archive) in volumes.iter().enumerate() {
772        for (entry_index, entry) in archive.entries.iter().enumerate() {
773            if !entry.is_split_before() && !entry.is_split_after() {
774                if pending.is_some() {
775                    return Err(Error::InvalidHeader(
776                        "RAR 1.3 split entry is interrupted by a regular entry",
777                    ));
778                }
779                let meta = entry.metadata();
780                if meta.is_directory {
781                    let _ = open(&meta)?;
782                    extracted_count += 1;
783                    continue;
784                }
785                let mut writer = open(&meta)?;
786                entry
787                    .write_compressed_to(
788                        archive,
789                        password,
790                        &mut unpack15,
791                        archive.main.is_solid() && extracted_count != 0,
792                        &mut writer,
793                    )
794                    .map_err(|error| entry.entry_error("extracting", error))?;
795                extracted_count += 1;
796                continue;
797            }
798
799            match (
800                &mut pending,
801                entry.is_split_before(),
802                entry.is_split_after(),
803            ) {
804                (None, false, true) => {
805                    pending = Some(PendingSplitRefs::new(entry, volume_index, entry_index));
806                }
807                (Some(current), true, true) => {
808                    current.append(entry, volume_index, entry_index)?;
809                }
810                (Some(current), true, false) => {
811                    current.append(entry, volume_index, entry_index)?;
812                    let completed = pending.take().expect("pending split");
813                    let solid = archive.main.is_solid() && extracted_count != 0;
814                    completed
815                        .write_to(volumes, entry, password, &mut unpack15, solid, &mut open)
816                        .map_err(|error| entry.entry_error("extracting", error))?;
817                    extracted_count += 1;
818                }
819                _ => {
820                    return Err(Error::InvalidHeader(
821                        "RAR 1.3 split entry flags are inconsistent",
822                    ));
823                }
824            }
825        }
826    }
827
828    if pending.is_some() {
829        return Err(Error::InvalidHeader("RAR 1.3 split entry is incomplete"));
830    }
831
832    Ok(())
833}
834
835struct Rar13ChecksumWriter<'a, W: Write + ?Sized> {
836    inner: &'a mut W,
837    checksum: &'a mut Rar13Checksum,
838}
839
840impl<W: Write + ?Sized> Write for Rar13ChecksumWriter<'_, W> {
841    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
842        let written = self.inner.write(buf)?;
843        self.checksum.update(&buf[..written]);
844        Ok(written)
845    }
846
847    fn flush(&mut self) -> std::io::Result<()> {
848        self.inner.flush()
849    }
850}
851
852struct Rar13Checksum {
853    value: u16,
854}
855
856impl Rar13Checksum {
857    fn new() -> Self {
858        Self { value: 0 }
859    }
860
861    fn update(&mut self, input: &[u8]) {
862        for &byte in input {
863            self.value = self.value.wrapping_add(byte as u16).rotate_left(1);
864        }
865    }
866
867    fn finish(self) -> u16 {
868        self.value
869    }
870}
871
872struct PendingSplitRefs {
873    name: Vec<u8>,
874    fragments: Vec<(usize, usize)>,
875    file_time: u32,
876    file_attr: u8,
877    method: u8,
878    unp_ver: u8,
879    was_encrypted: bool,
880}
881
882impl PendingSplitRefs {
883    fn new(entry: &Entry, volume_index: usize, entry_index: usize) -> Self {
884        Self {
885            name: entry.name.clone(),
886            fragments: vec![(volume_index, entry_index)],
887            file_time: entry.header.file_time,
888            file_attr: entry.header.file_attr,
889            method: entry.header.method,
890            unp_ver: entry.header.unp_ver,
891            was_encrypted: entry.is_encrypted(),
892        }
893    }
894
895    fn append(&mut self, entry: &Entry, volume_index: usize, entry_index: usize) -> Result<()> {
896        if entry.name != self.name {
897            return Err(Error::InvalidHeader("RAR 1.3 split entry name changed"));
898        }
899        if entry.header.method != self.method {
900            return Err(Error::InvalidHeader(
901                "RAR 1.3 split entry compression method changed",
902            ));
903        }
904        if entry.header.unp_ver != self.unp_ver {
905            return Err(Error::InvalidHeader(
906                "RAR 1.3 split entry unpack version changed",
907            ));
908        }
909        if entry.is_encrypted() != self.was_encrypted {
910            return Err(Error::InvalidHeader(
911                "RAR 1.3 split entry encryption flag changed",
912            ));
913        }
914        self.fragments.push((volume_index, entry_index));
915        Ok(())
916    }
917
918    fn write_to<F>(
919        self,
920        volumes: &[Archive],
921        final_entry: &Entry,
922        password: Option<&[u8]>,
923        unpack15: &mut Unpack15,
924        solid: bool,
925        open: &mut F,
926    ) -> Result<()>
927    where
928        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
929    {
930        let mut reader = self.fragment_reader(volumes, password)?;
931        let meta = ExtractedEntryMeta {
932            name: self.name,
933            file_time: self.file_time,
934            file_attr: self.file_attr,
935            is_directory: false,
936        };
937        let mut writer = open(&meta)?;
938        let mut checksum = Rar13Checksum::new();
939        let mut checksum_writer = Rar13ChecksumWriter {
940            inner: &mut writer,
941            checksum: &mut checksum,
942        };
943        if self.method == METHOD_STORE {
944            std::io::copy(&mut reader, &mut checksum_writer)?;
945        } else {
946            unpack15.decode_member_from_reader(
947                &mut reader,
948                final_entry.header.unp_size as usize,
949                solid,
950                &mut checksum_writer,
951            )?;
952        }
953        let actual = checksum.finish();
954        if actual == final_entry.header.file_crc {
955            Ok(())
956        } else {
957            Err(Error::CrcMismatch {
958                expected: final_entry.header.file_crc,
959                actual,
960            })
961        }
962    }
963
964    fn fragment_reader<'a>(
965        &self,
966        volumes: &'a [Archive],
967        password: Option<&'a [u8]>,
968    ) -> Result<ChainedReader<'a>> {
969        let mut readers = Vec::with_capacity(self.fragments.len());
970        for &(volume_index, entry_index) in &self.fragments {
971            let archive = volumes
972                .get(volume_index)
973                .ok_or(Error::InvalidHeader("RAR 1.3 split volume is missing"))?;
974            let entry = archive
975                .entries
976                .get(entry_index)
977                .ok_or(Error::InvalidHeader("RAR 1.3 split entry is missing"))?;
978            let reader = archive.range_reader(entry.packed_range.clone())?;
979            if entry.is_encrypted() {
980                let password = password.ok_or(Error::NeedPassword)?;
981                readers.push(
982                    Box::new(Rar13DecryptReader::new(reader, Rar13Cipher::new(password)))
983                        as Box<dyn Read + 'a>,
984                );
985            } else {
986                readers.push(reader);
987            }
988        }
989        Ok(ChainedReader { readers, index: 0 })
990    }
991}
992
993struct ChainedReader<'a> {
994    readers: Vec<Box<dyn Read + 'a>>,
995    index: usize,
996}
997
998impl Read for ChainedReader<'_> {
999    fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
1000        while let Some(reader) = self.readers.get_mut(self.index) {
1001            let read = reader.read(out)?;
1002            if read != 0 {
1003                return Ok(read);
1004            }
1005            self.index += 1;
1006        }
1007        Ok(0)
1008    }
1009}
1010
1011pub fn write_stored_archive(
1012    entries: &[StoredEntry<'_>],
1013    options: WriterOptions,
1014) -> Result<Vec<u8>> {
1015    write_stored_archive_with_comment(entries, options, None)
1016}
1017
1018pub fn write_stored_archive_with_comment(
1019    entries: &[StoredEntry<'_>],
1020    options: WriterOptions,
1021    archive_comment: Option<&[u8]>,
1022) -> Result<Vec<u8>> {
1023    if !options.target.is_rar13_family() {
1024        return Err(Error::UnsupportedVersion(options.target));
1025    }
1026    options.features.validate_for(options.target)?;
1027    validate_stored_writer_features(options.target, options.features)?;
1028
1029    let mut out = Vec::new();
1030    write_main_header(&mut out, options.features, archive_comment)?;
1031
1032    for entry in entries {
1033        validate_stored_entry(entry)?;
1034        write_stored_entry(&mut out, entry, options.features)?;
1035    }
1036
1037    Ok(out)
1038}
1039
1040pub fn write_compressed_archive(
1041    entries: &[FileEntry<'_>],
1042    options: WriterOptions,
1043) -> Result<Vec<u8>> {
1044    write_compressed_archive_with_comment(entries, options, None)
1045}
1046
1047pub fn write_compressed_archive_with_comment(
1048    entries: &[FileEntry<'_>],
1049    options: WriterOptions,
1050    archive_comment: Option<&[u8]>,
1051) -> Result<Vec<u8>> {
1052    if !options.target.is_rar13_family() {
1053        return Err(Error::UnsupportedVersion(options.target));
1054    }
1055    options.features.validate_for(options.target)?;
1056    validate_compressed_writer_features(options.target, options.features)?;
1057    validate_compression_level(options)?;
1058
1059    let mut out = Vec::new();
1060    write_main_header(&mut out, options.features, archive_comment)?;
1061
1062    let encode_options = rar15_encode_options_for_level(options.compression_level)?;
1063    let mut solid_encoder = options
1064        .features
1065        .solid
1066        .then(|| Unpack15Encoder::with_options(encode_options));
1067
1068    for entry in entries {
1069        validate_file_entry(entry.name, entry.data)?;
1070        let solid = solid_encoder.is_some();
1071        let mut packed = if let Some(encoder) = solid_encoder.as_mut() {
1072            encoder.encode_member(entry.data)?
1073        } else if options.compression_level == Some(0) {
1074            entry.data.to_vec()
1075        } else {
1076            encode_verified_rar15_payload(entry.data, encode_options)?
1077                .unwrap_or_else(|| entry.data.to_vec())
1078        };
1079        let method = if options.compression_level == Some(0)
1080            || (!solid && packed.len() >= entry.data.len())
1081        {
1082            packed = entry.data.to_vec();
1083            METHOD_STORE
1084        } else {
1085            METHOD_BEST
1086        };
1087        if let Some(password) = entry.password {
1088            Rar13Cipher::new(password).encrypt_in_place(&mut packed);
1089        }
1090        let mut flags = 0;
1091        if options.features.solid {
1092            flags |= LHD_SOLID;
1093        }
1094        if entry.password.is_some() {
1095            flags |= LHD_PASSWORD;
1096        }
1097        if entry.file_comment.is_some() {
1098            flags |= LHD_COMMENT;
1099        }
1100        let file_extra = encode_file_comment(entry.file_comment)?;
1101        write_file_entry(
1102            &mut out,
1103            FileEntryRecord {
1104                name: entry.name,
1105                unpacked_size: entry.data.len() as u32,
1106                file_crc: file_checksum(entry.data),
1107                packed: &packed,
1108                file_time: entry.file_time,
1109                file_attr: entry.file_attr,
1110                flags,
1111                unp_ver: DEFAULT_UNP_VER,
1112                method,
1113                extra: &file_extra,
1114            },
1115        )?;
1116    }
1117
1118    Ok(out)
1119}
1120
1121pub fn write_stored_volumes(
1122    entry: StoredEntry<'_>,
1123    options: WriterOptions,
1124    max_packed_per_volume: usize,
1125) -> Result<Vec<Vec<u8>>> {
1126    if !options.target.is_rar13_family() {
1127        return Err(Error::UnsupportedVersion(options.target));
1128    }
1129    options.features.validate_for(options.target)?;
1130    validate_stored_writer_features(options.target, options.features)?;
1131    validate_volume_writer_inputs(
1132        entry.name,
1133        entry.data,
1134        entry.password,
1135        entry.file_comment,
1136        options,
1137    )?;
1138
1139    let body = entry.data.to_vec();
1140    write_split_volumes(SplitVolumeRecord {
1141        name: entry.name,
1142        unpacked: entry.data,
1143        packed: &body,
1144        file_time: entry.file_time,
1145        file_attr: entry.file_attr,
1146        method: METHOD_STORE,
1147        base_flags: 0,
1148        features: options.features,
1149        max_packed_per_volume,
1150    })
1151}
1152
1153pub fn write_compressed_volumes(
1154    entry: FileEntry<'_>,
1155    options: WriterOptions,
1156    max_packed_per_volume: usize,
1157) -> Result<Vec<Vec<u8>>> {
1158    if !options.target.is_rar13_family() {
1159        return Err(Error::UnsupportedVersion(options.target));
1160    }
1161    options.features.validate_for(options.target)?;
1162    validate_compressed_writer_features(options.target, options.features)?;
1163    validate_volume_writer_inputs(
1164        entry.name,
1165        entry.data,
1166        entry.password,
1167        entry.file_comment,
1168        options,
1169    )?;
1170
1171    validate_compression_level(options)?;
1172    let mut packed = encode_verified_rar15_payload(
1173        entry.data,
1174        rar15_encode_options_for_level(options.compression_level)?,
1175    )?
1176    .unwrap_or_else(|| entry.data.to_vec());
1177    let method = if packed.len() >= entry.data.len() {
1178        packed = entry.data.to_vec();
1179        METHOD_STORE
1180    } else {
1181        METHOD_BEST
1182    };
1183    write_split_volumes(SplitVolumeRecord {
1184        name: entry.name,
1185        unpacked: entry.data,
1186        packed: &packed,
1187        file_time: entry.file_time,
1188        file_attr: entry.file_attr,
1189        method,
1190        base_flags: 0,
1191        features: options.features,
1192        max_packed_per_volume,
1193    })
1194}
1195
1196fn validate_stored_writer_features(version: ArchiveVersion, features: FeatureSet) -> Result<()> {
1197    reject_writer_feature(version, features.sfx, "sfx")?;
1198    reject_writer_feature(
1199        version,
1200        features.authenticity_verification,
1201        "authenticity_verification",
1202    )?;
1203    Ok(())
1204}
1205
1206fn validate_volume_writer_inputs(
1207    name: &[u8],
1208    data: &[u8],
1209    password: Option<&[u8]>,
1210    file_comment: Option<&[u8]>,
1211    options: WriterOptions,
1212) -> Result<()> {
1213    validate_file_entry(name, data)?;
1214    if password.is_some() {
1215        return Err(Error::UnsupportedFeature {
1216            version: options.target,
1217            feature: "volume_password",
1218        });
1219    }
1220    if file_comment.is_some() || options.features.file_comment {
1221        return Err(Error::UnsupportedFeature {
1222            version: options.target,
1223            feature: "volume_file_comment",
1224        });
1225    }
1226    if options.features.archive_comment {
1227        return Err(Error::UnsupportedFeature {
1228            version: options.target,
1229            feature: "volume_archive_comment",
1230        });
1231    }
1232    Ok(())
1233}
1234
1235fn validate_compressed_writer_features(
1236    version: ArchiveVersion,
1237    features: FeatureSet,
1238) -> Result<()> {
1239    reject_writer_feature(version, features.sfx, "sfx")?;
1240    reject_writer_feature(
1241        version,
1242        features.authenticity_verification,
1243        "authenticity_verification",
1244    )?;
1245    Ok(())
1246}
1247
1248fn validate_compression_level(options: WriterOptions) -> Result<()> {
1249    if matches!(options.compression_level, Some(level) if level > 5) {
1250        return Err(Error::InvalidHeader(
1251            "RAR compression level must be in the range 0..5",
1252        ));
1253    }
1254    Ok(())
1255}
1256
1257fn rar15_encode_options_for_level(level: Option<u8>) -> Result<Rar15EncodeOptions> {
1258    let level = level.unwrap_or(5);
1259    // DOS RAR 1.402 rejects some streams produced with the old-distance
1260    // short-LZ codes, even though the rars decoder can read them. Keep the
1261    // writer on the older compatible subset until that encoding is pinned
1262    // against a real oracle.
1263    let compatible = Rar15EncodeOptions::new().with_old_distance_tokens(false);
1264    match level {
1265        0 => Ok(compatible
1266            .with_lazy_matching(false)
1267            .with_stmode_literal_runs(false)
1268            .with_max_long_match_distance(0)),
1269        1 => Ok(compatible
1270            .with_lazy_matching(false)
1271            .with_stmode_literal_runs(false)
1272            .with_max_long_match_distance(4 * 1024)),
1273        2 => Ok(compatible
1274            .with_lazy_matching(false)
1275            .with_stmode_literal_runs(false)
1276            .with_max_long_match_distance(8 * 1024)),
1277        3 => Ok(compatible
1278            .with_lazy_matching(false)
1279            .with_max_long_match_distance(16 * 1024)),
1280        4 => Ok(compatible
1281            .with_lazy_matching(false)
1282            .with_max_long_match_distance(24 * 1024)),
1283        5 => Ok(compatible.with_lazy_matching(false)),
1284        _ => Err(Error::InvalidHeader(
1285            "RAR compression level must be in the range 0..5",
1286        )),
1287    }
1288}
1289
1290fn encode_verified_rar15_payload(
1291    data: &[u8],
1292    options: Rar15EncodeOptions,
1293) -> Result<Option<Vec<u8>>> {
1294    for candidate_options in rar15_encode_fallback_options(options) {
1295        let packed = unpack15_encode_with_options(data, candidate_options)?;
1296        if unpack15_payload_matches(&packed, data)? {
1297            return Ok(Some(packed));
1298        }
1299    }
1300    Ok(None)
1301}
1302
1303fn rar15_encode_fallback_options(options: Rar15EncodeOptions) -> Vec<Rar15EncodeOptions> {
1304    let mut candidates = vec![options];
1305    let distance_limited = options.with_max_long_match_distance(24 * 1024);
1306    if distance_limited != options {
1307        candidates.push(distance_limited);
1308    }
1309    let conservative = options
1310        .with_lazy_matching(false)
1311        .with_stmode_literal_runs(false)
1312        .with_max_long_match_distance(8 * 1024);
1313    if !candidates.contains(&conservative) {
1314        candidates.push(conservative);
1315    }
1316    candidates
1317}
1318
1319fn unpack15_payload_matches(packed: &[u8], data: &[u8]) -> Result<bool> {
1320    match unpack15_decode(packed, data.len()) {
1321        Ok(decoded) => Ok(decoded == data),
1322        Err(_) => Ok(false),
1323    }
1324}
1325
1326fn reject_writer_feature(
1327    version: ArchiveVersion,
1328    enabled: bool,
1329    feature: &'static str,
1330) -> Result<()> {
1331    if enabled {
1332        Err(Error::UnsupportedFeature { version, feature })
1333    } else {
1334        Ok(())
1335    }
1336}
1337
1338fn write_main_header(
1339    out: &mut Vec<u8>,
1340    features: FeatureSet,
1341    archive_comment: Option<&[u8]>,
1342) -> Result<()> {
1343    write_main_header_with_flags(out, features, archive_comment, 0)
1344}
1345
1346fn write_main_header_with_flags(
1347    out: &mut Vec<u8>,
1348    features: FeatureSet,
1349    archive_comment: Option<&[u8]>,
1350    extra_flags: u8,
1351) -> Result<()> {
1352    let comment_extra = encode_archive_comment(archive_comment)?;
1353    let mut flags = MHD_ALWAYS_SET | extra_flags;
1354    if archive_comment.is_some() {
1355        flags |= MHD_COMMENT;
1356        flags |= MHD_PACK_COMMENT;
1357    }
1358    if features.solid {
1359        flags |= MHD_SOLID;
1360    }
1361    out.extend_from_slice(RAR13_SIGNATURE);
1362    let head_size = MAIN_HEAD_SIZE as usize + comment_extra.len();
1363    if head_size > u16::MAX as usize {
1364        return Err(Error::InvalidHeader(
1365            "RAR 1.3 main header comment extension is too large",
1366        ));
1367    }
1368    out.extend_from_slice(&(head_size as u16).to_le_bytes());
1369    out.push(flags);
1370    out.extend_from_slice(&comment_extra);
1371    Ok(())
1372}
1373
1374fn write_stored_entry(
1375    out: &mut Vec<u8>,
1376    entry: &StoredEntry<'_>,
1377    features: FeatureSet,
1378) -> Result<()> {
1379    let mut flags = 0u8;
1380    if entry.password.is_some() {
1381        flags |= LHD_PASSWORD;
1382    }
1383    if entry.file_comment.is_some() {
1384        flags |= LHD_COMMENT;
1385    }
1386    if features.solid {
1387        flags |= LHD_SOLID;
1388    }
1389
1390    let mut body = entry.data.to_vec();
1391    if let Some(password) = entry.password {
1392        Rar13Cipher::new(password).encrypt_in_place(&mut body);
1393    }
1394
1395    let file_extra = encode_file_comment(entry.file_comment)?;
1396    write_file_entry(
1397        out,
1398        FileEntryRecord {
1399            name: entry.name,
1400            unpacked_size: entry.data.len() as u32,
1401            file_crc: file_checksum(entry.data),
1402            packed: &body,
1403            file_time: entry.file_time,
1404            file_attr: entry.file_attr,
1405            flags,
1406            unp_ver: DEFAULT_UNP_VER,
1407            method: METHOD_STORE,
1408            extra: &file_extra,
1409        },
1410    )?;
1411    Ok(())
1412}
1413
1414fn validate_stored_entry(entry: &StoredEntry<'_>) -> Result<()> {
1415    validate_file_entry(entry.name, entry.data)
1416}
1417
1418struct FileEntryRecord<'a> {
1419    name: &'a [u8],
1420    unpacked_size: u32,
1421    file_crc: u16,
1422    packed: &'a [u8],
1423    file_time: u32,
1424    file_attr: u8,
1425    flags: u8,
1426    unp_ver: u8,
1427    method: u8,
1428    extra: &'a [u8],
1429}
1430
1431fn write_file_entry(out: &mut Vec<u8>, entry: FileEntryRecord<'_>) -> Result<()> {
1432    let head_size = FILE_HEAD_BASE_SIZE + entry.name.len() + entry.extra.len();
1433    out.extend_from_slice(&(entry.packed.len() as u32).to_le_bytes());
1434    out.extend_from_slice(&entry.unpacked_size.to_le_bytes());
1435    out.extend_from_slice(&entry.file_crc.to_le_bytes());
1436    out.extend_from_slice(&(head_size as u16).to_le_bytes());
1437    out.extend_from_slice(&entry.file_time.to_le_bytes());
1438    out.push(entry.file_attr);
1439    out.push(entry.flags);
1440    out.push(entry.unp_ver);
1441    out.push(entry.name.len() as u8);
1442    out.push(entry.method);
1443    out.extend_from_slice(entry.name);
1444    out.extend_from_slice(entry.extra);
1445    out.extend_from_slice(entry.packed);
1446    Ok(())
1447}
1448
1449struct SplitVolumeRecord<'a> {
1450    name: &'a [u8],
1451    unpacked: &'a [u8],
1452    packed: &'a [u8],
1453    file_time: u32,
1454    file_attr: u8,
1455    method: u8,
1456    base_flags: u8,
1457    features: FeatureSet,
1458    max_packed_per_volume: usize,
1459}
1460
1461fn write_split_volumes(entry: SplitVolumeRecord<'_>) -> Result<Vec<Vec<u8>>> {
1462    if entry.max_packed_per_volume == 0 {
1463        return Err(Error::InvalidHeader(
1464            "RAR 1.3 volume payload size must be non-zero",
1465        ));
1466    }
1467    if entry.packed.is_empty() {
1468        return Err(Error::InvalidHeader(
1469            "RAR 1.3 volume writer needs a non-empty packed payload",
1470        ));
1471    }
1472
1473    let chunks: Vec<&[u8]> = entry.packed.chunks(entry.max_packed_per_volume).collect();
1474    if chunks.len() < 2 {
1475        return Err(Error::InvalidHeader(
1476            "RAR 1.3 volume writer needs at least two volumes",
1477        ));
1478    }
1479
1480    let mut volumes = Vec::with_capacity(chunks.len());
1481    for (index, chunk) in chunks.iter().enumerate() {
1482        let split_before = index > 0;
1483        let split_after = index + 1 < chunks.len();
1484        let mut flags = entry.base_flags;
1485        if split_before {
1486            flags |= LHD_SPLIT_BEFORE;
1487        }
1488        if split_after {
1489            flags |= LHD_SPLIT_AFTER;
1490        }
1491        if entry.features.solid {
1492            flags |= LHD_SOLID;
1493        }
1494
1495        let mut out = Vec::new();
1496        write_main_header_with_flags(&mut out, entry.features, None, MHD_VOLUME)?;
1497        let checksum_data = if split_after { *chunk } else { entry.unpacked };
1498        write_file_entry(
1499            &mut out,
1500            FileEntryRecord {
1501                name: entry.name,
1502                unpacked_size: entry.unpacked.len() as u32,
1503                file_crc: file_checksum(checksum_data),
1504                packed: chunk,
1505                file_time: entry.file_time,
1506                file_attr: entry.file_attr,
1507                flags,
1508                unp_ver: DEFAULT_UNP_VER,
1509                method: entry.method,
1510                extra: &[],
1511            },
1512        )?;
1513        volumes.push(out);
1514    }
1515
1516    Ok(volumes)
1517}
1518
1519fn encode_archive_comment(comment: Option<&[u8]>) -> Result<Vec<u8>> {
1520    let Some(comment) = comment else {
1521        return Ok(Vec::new());
1522    };
1523    if comment.len() > u16::MAX as usize {
1524        return Err(Error::InvalidHeader(
1525            "RAR 1.3 archive comment is longer than 65535 bytes",
1526        ));
1527    }
1528    let mut packed = unpack15_encode(comment)?;
1529    Rar13Cipher::new_comment().encrypt_in_place(&mut packed);
1530    let packed_field_len = packed.len().checked_add(2).ok_or(Error::InvalidHeader(
1531        "RAR 1.3 archive comment size overflows",
1532    ))?;
1533    if packed_field_len > u16::MAX as usize {
1534        return Err(Error::InvalidHeader(
1535            "RAR 1.3 packed archive comment is longer than 65535 bytes",
1536        ));
1537    }
1538
1539    let mut out = Vec::with_capacity(4 + packed.len());
1540    out.extend_from_slice(&(packed_field_len as u16).to_le_bytes());
1541    out.extend_from_slice(&(comment.len() as u16).to_le_bytes());
1542    out.extend_from_slice(&packed);
1543    Ok(out)
1544}
1545
1546fn encode_file_comment(comment: Option<&[u8]>) -> Result<Vec<u8>> {
1547    let Some(comment) = comment else {
1548        return Ok(Vec::new());
1549    };
1550    if comment.len() > u16::MAX as usize {
1551        return Err(Error::InvalidHeader(
1552            "RAR 1.3 file comment is longer than 65535 bytes",
1553        ));
1554    }
1555    let mut out = Vec::with_capacity(2 + comment.len());
1556    out.extend_from_slice(&(comment.len() as u16).to_le_bytes());
1557    out.extend_from_slice(comment);
1558    Ok(out)
1559}
1560
1561fn validate_file_entry(name: &[u8], data: &[u8]) -> Result<()> {
1562    if name.is_empty() {
1563        return Err(Error::InvalidHeader("RAR 1.3 file name is empty"));
1564    }
1565    if name.len() > u8::MAX as usize {
1566        return Err(Error::InvalidHeader(
1567            "RAR 1.3 file name is longer than 255 bytes",
1568        ));
1569    }
1570    if data.len() > u32::MAX as usize {
1571        return Err(Error::InvalidHeader(
1572            "RAR 1.3 file is larger than 32-bit size fields",
1573        ));
1574    }
1575    Ok(())
1576}
1577
1578pub fn file_checksum(input: &[u8]) -> u16 {
1579    let mut checksum = Rar13Checksum::new();
1580    checksum.update(input);
1581    checksum.finish()
1582}
1583
1584#[cfg(test)]
1585mod tests {
1586    use super::*;
1587    use rars_codec::rar13::{find_long_lz, LongLz};
1588    use std::cell::RefCell;
1589    use std::rc::Rc;
1590
1591    struct CollectWriter(Rc<RefCell<Vec<u8>>>);
1592
1593    #[derive(Debug, Clone, PartialEq, Eq)]
1594    struct CollectedEntry {
1595        name: Vec<u8>,
1596        data: Vec<u8>,
1597        file_time: u32,
1598        file_attr: u8,
1599        is_directory: bool,
1600    }
1601
1602    impl Write for CollectWriter {
1603        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1604            self.0.borrow_mut().extend_from_slice(buf);
1605            Ok(buf.len())
1606        }
1607
1608        fn flush(&mut self) -> std::io::Result<()> {
1609            Ok(())
1610        }
1611    }
1612
1613    fn collect_extract(archive: &Archive, password: Option<&[u8]>) -> Result<Vec<CollectedEntry>> {
1614        let entries = RefCell::new(Vec::new());
1615        archive.extract_to(password, |meta| {
1616            let data = Rc::new(RefCell::new(Vec::new()));
1617            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
1618            Ok(Box::new(CollectWriter(data)))
1619        })?;
1620        Ok(entries
1621            .into_inner()
1622            .into_iter()
1623            .map(|(meta, data)| CollectedEntry {
1624                name: meta.name,
1625                data: data.borrow().clone(),
1626                file_time: meta.file_time,
1627                file_attr: meta.file_attr,
1628                is_directory: meta.is_directory,
1629            })
1630            .collect())
1631    }
1632
1633    fn collect_extract_volumes(
1634        volumes: &[Archive],
1635        password: Option<&[u8]>,
1636    ) -> Result<Vec<CollectedEntry>> {
1637        let entries = RefCell::new(Vec::new());
1638        extract_volumes_to(volumes, password, |meta| {
1639            let data = Rc::new(RefCell::new(Vec::new()));
1640            entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
1641            Ok(Box::new(CollectWriter(data)))
1642        })?;
1643        Ok(entries
1644            .into_inner()
1645            .into_iter()
1646            .map(|(meta, data)| CollectedEntry {
1647                name: meta.name,
1648                data: data.borrow().clone(),
1649                file_time: meta.file_time,
1650                file_attr: meta.file_attr,
1651                is_directory: meta.is_directory,
1652            })
1653            .collect())
1654    }
1655
1656    fn synthetic_log_payload(lines: usize) -> Vec<u8> {
1657        let mut data = Vec::new();
1658        for index in 0..lines {
1659            data.extend_from_slice(
1660                format!(
1661                    "2026-05-12T12:{:02}:{:02}.000Z INFO worker-{:02} request_id={:04x}-{:05} path=/api/v1/items/{} status={} elapsed_ms={} bytes={} message=processed archive chunk retry={} user=service-{}\n",
1662                    index % 60,
1663                    (index * 7) % 60,
1664                    index % 16,
1665                    index % 10000,
1666                    (index * 17) % 100000,
1667                    index % 2048,
1668                    200 + (index % 5),
1669                    (index * 37) % 5000,
1670                    (index * 911) % 65536,
1671                    index % 3,
1672                    index % 32
1673                )
1674                .as_bytes(),
1675            );
1676        }
1677        data
1678    }
1679
1680    #[test]
1681    fn writes_and_reads_stored_archive() {
1682        let input = [
1683            StoredEntry {
1684                name: b"README.md",
1685                data: b"hello rar 1.3",
1686                file_time: 0,
1687                file_attr: 0x20,
1688                password: None,
1689                file_comment: None,
1690            },
1691            StoredEntry {
1692                name: b"docs",
1693                data: b"",
1694                file_time: 0,
1695                file_attr: 0x10,
1696                password: None,
1697                file_comment: None,
1698            },
1699        ];
1700
1701        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
1702        let archive = Archive::parse(&bytes).unwrap();
1703        assert_eq!(archive.main.flags, 0x80);
1704        assert_eq!(archive.entries.len(), 2);
1705        assert_eq!(archive.entries[0].name_bytes(), b"README.md");
1706        assert_eq!(archive.entries[0].name_lossy(), "README.md");
1707        let extracted = collect_extract(&archive, None).unwrap();
1708        assert_eq!(extracted[0].data, b"hello rar 1.3");
1709        assert!(archive.entries[1].is_directory());
1710        assert!(extracted[1].is_directory);
1711    }
1712
1713    #[test]
1714    fn rejects_malformed_main_header_boundaries() {
1715        assert_eq!(MainHeader::parse(b"RE~"), Err(Error::TooShort));
1716
1717        let mut too_small = Vec::from(&b"RE~^"[..]);
1718        too_small.extend_from_slice(&6u16.to_le_bytes());
1719        too_small.push(0x80);
1720        assert_eq!(
1721            MainHeader::parse(&too_small),
1722            Err(Error::InvalidHeader(
1723                "RAR 1.3 main header is shorter than 7 bytes"
1724            ))
1725        );
1726
1727        let mut truncated_extra = Vec::from(&b"RE~^"[..]);
1728        truncated_extra.extend_from_slice(&8u16.to_le_bytes());
1729        truncated_extra.push(0x80);
1730        assert_eq!(MainHeader::parse(&truncated_extra), Err(Error::TooShort));
1731
1732        assert!(matches!(
1733            Archive::parse(b"Rar!\x1a\x07\x00"),
1734            Err(Error::UnsupportedSignature)
1735        ));
1736    }
1737
1738    #[test]
1739    fn rejects_file_header_shorter_than_its_name() {
1740        let mut bytes = Vec::from(&b"RE~^"[..]);
1741        bytes.extend_from_slice(&7u16.to_le_bytes());
1742        bytes.push(0x80);
1743        bytes.extend_from_slice(&0u32.to_le_bytes());
1744        bytes.extend_from_slice(&0u32.to_le_bytes());
1745        bytes.extend_from_slice(&0u16.to_le_bytes());
1746        bytes.extend_from_slice(&(FILE_HEAD_BASE_SIZE as u16).to_le_bytes());
1747        bytes.extend_from_slice(&0u32.to_le_bytes());
1748        bytes.push(0x20);
1749        bytes.push(0);
1750        bytes.push(DEFAULT_UNP_VER);
1751        bytes.push(10);
1752        bytes.push(METHOD_STORE);
1753
1754        assert!(matches!(
1755            Archive::parse(&bytes),
1756            Err(Error::InvalidHeader(
1757                "RAR 1.3 file header is shorter than its name"
1758            ))
1759        ));
1760    }
1761
1762    #[test]
1763    fn rejects_truncated_file_payload_during_parse() {
1764        let input = [StoredEntry {
1765            name: b"hello.txt",
1766            data: b"hello",
1767            file_time: 0,
1768            file_attr: 0x20,
1769            password: None,
1770            file_comment: None,
1771        }];
1772        let mut bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
1773        bytes.pop();
1774
1775        assert!(matches!(Archive::parse(&bytes), Err(Error::TooShort)));
1776    }
1777
1778    #[test]
1779    fn returns_none_for_absent_archive_comment() {
1780        let bytes = write_stored_archive(&[], WriterOptions::default()).unwrap();
1781        let archive = Archive::parse(&bytes).unwrap();
1782
1783        assert_eq!(archive.archive_comment().unwrap(), None);
1784    }
1785
1786    #[test]
1787    fn rejects_normal_extract_on_split_entries() {
1788        let entry = StoredEntry {
1789            name: b"split.bin",
1790            data: b"abcdefghijklmnopqrstuvwxyz",
1791            file_time: 0,
1792            file_attr: 0x20,
1793            password: None,
1794            file_comment: None,
1795        };
1796        let volumes = write_stored_volumes(entry, WriterOptions::default(), 8).unwrap();
1797        let first = Archive::parse(&volumes[0]).unwrap();
1798
1799        assert_eq!(
1800            collect_extract(&first, None),
1801            Err(Error::InvalidHeader(
1802                "RAR 1.3 split entry requires multivolume extraction"
1803            ))
1804        );
1805        assert_eq!(
1806            collect_extract(&first, None),
1807            Err(Error::InvalidHeader(
1808                "RAR 1.3 split entry requires multivolume extraction"
1809            ))
1810        );
1811    }
1812
1813    #[test]
1814    fn rejects_malformed_comment_extensions() {
1815        let packed_too_short = Archive {
1816            sfx_offset: 0,
1817            main: MainHeader {
1818                flags: MHD_COMMENT | MHD_PACK_COMMENT,
1819                head_size: MAIN_HEAD_SIZE,
1820                extra: 1u16.to_le_bytes().to_vec(),
1821            },
1822            entries: Vec::new(),
1823            source: ArchiveSource::Memory(Arc::new([])),
1824        };
1825        assert_eq!(
1826            packed_too_short.archive_comment(),
1827            Err(Error::InvalidHeader(
1828                "RAR 1.3 packed archive comment is shorter than size field"
1829            ))
1830        );
1831
1832        let unpacked_too_short = Archive {
1833            sfx_offset: 0,
1834            main: MainHeader {
1835                flags: MHD_COMMENT,
1836                head_size: MAIN_HEAD_SIZE,
1837                extra: 4u16.to_le_bytes().to_vec(),
1838            },
1839            entries: Vec::new(),
1840            source: ArchiveSource::Memory(Arc::new([])),
1841        };
1842        assert_eq!(unpacked_too_short.archive_comment(), Err(Error::TooShort));
1843    }
1844
1845    #[test]
1846    fn rejects_malformed_av_extensions() {
1847        let too_short = Archive {
1848            sfx_offset: 0,
1849            main: MainHeader {
1850                flags: MHD_AV,
1851                head_size: MAIN_HEAD_SIZE,
1852                extra: 5u16.to_le_bytes().to_vec(),
1853            },
1854            entries: Vec::new(),
1855            source: ArchiveSource::Memory(Arc::new([])),
1856        };
1857        assert_eq!(
1858            too_short.authenticity_verification(),
1859            Err(Error::InvalidHeader("RAR 1.3 AV payload is too short"))
1860        );
1861
1862        let bad_prefix = Archive {
1863            sfx_offset: 0,
1864            main: MainHeader {
1865                flags: MHD_AV,
1866                head_size: MAIN_HEAD_SIZE,
1867                extra: {
1868                    let mut extra = 6u16.to_le_bytes().to_vec();
1869                    extra.extend_from_slice(b"badbad");
1870                    extra
1871                },
1872            },
1873            entries: Vec::new(),
1874            source: ArchiveSource::Memory(Arc::new([])),
1875        };
1876        assert_eq!(
1877            bad_prefix.authenticity_verification(),
1878            Err(Error::InvalidHeader("RAR 1.3 AV prefix mismatch"))
1879        );
1880    }
1881
1882    #[test]
1883    fn writes_and_reads_encrypted_stored_archive() {
1884        let input = [StoredEntry {
1885            name: b"secret.txt",
1886            data: b"secret bytes",
1887            file_time: 0,
1888            file_attr: 0x20,
1889            password: Some(b"pass"),
1890            file_comment: None,
1891        }];
1892
1893        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
1894        let archive = Archive::parse(&bytes).unwrap();
1895        assert!(archive.entries[0].is_encrypted());
1896        match collect_extract(&archive, None) {
1897            Err(Error::NeedPassword) => {}
1898            Err(Error::AtEntry { source, .. }) if matches!(*source, Error::NeedPassword) => {}
1899            other => panic!("expected missing password error, got {other:?}"),
1900        }
1901
1902        let extracted = collect_extract(&archive, Some(b"pass")).unwrap();
1903        assert_eq!(extracted[0].data, b"secret bytes");
1904    }
1905
1906    #[test]
1907    fn writes_and_reads_archive_comment() {
1908        let input = [StoredEntry {
1909            name: b"README.md",
1910            data: b"hello rar 1.3",
1911            file_time: 0,
1912            file_attr: 0x20,
1913            password: None,
1914            file_comment: None,
1915        }];
1916
1917        let bytes = write_stored_archive_with_comment(
1918            &input,
1919            WriterOptions::default(),
1920            Some(b"This is an archive comment."),
1921        )
1922        .unwrap();
1923        let archive = Archive::parse(&bytes).unwrap();
1924        assert!(archive.main.has_archive_comment());
1925        assert!(archive.main.has_packed_comment());
1926        assert_eq!(
1927            archive.archive_comment().unwrap().as_deref(),
1928            Some(&b"This is an archive comment."[..])
1929        );
1930        assert_eq!(
1931            collect_extract(&archive, None).unwrap()[0].data,
1932            b"hello rar 1.3"
1933        );
1934    }
1935
1936    #[test]
1937    fn writes_and_reads_file_comment() {
1938        let input = [StoredEntry {
1939            name: b"README.md",
1940            data: b"hello rar 1.3",
1941            file_time: 0,
1942            file_attr: 0x20,
1943            password: None,
1944            file_comment: Some(b"file comment\r\n"),
1945        }];
1946
1947        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
1948        let archive = Archive::parse(&bytes).unwrap();
1949        assert!(archive.entries[0].has_file_comment());
1950        assert_eq!(
1951            archive.entries[0].file_comment().unwrap().as_deref(),
1952            Some(&b"file comment\r\n"[..])
1953        );
1954        assert_eq!(
1955            collect_extract(&archive, None).unwrap()[0].data,
1956            b"hello rar 1.3"
1957        );
1958    }
1959
1960    #[test]
1961    fn writes_and_reads_literal_only_compressed_archive() {
1962        let input = [FileEntry {
1963            name: b"tiny.txt",
1964            data: b"literal bytes over sixteen",
1965            file_time: 0,
1966            file_attr: 0x20,
1967            password: None,
1968            file_comment: None,
1969        }];
1970
1971        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
1972        let archive = Archive::parse(&bytes).unwrap();
1973        assert_eq!(archive.main.flags, 0x80);
1974        assert_eq!(archive.entries.len(), 1);
1975        assert_eq!(archive.entries[0].name, b"tiny.txt");
1976        assert!(archive.entries[0].is_stored());
1977        assert_eq!(archive.entries[0].header.method, METHOD_STORE);
1978        assert_eq!(
1979            archive.entries[0].header.pack_size,
1980            input[0].data.len() as u32
1981        );
1982
1983        let extracted = collect_extract(&archive, None).unwrap();
1984        assert_eq!(extracted[0].data, b"literal bytes over sixteen");
1985    }
1986
1987    #[test]
1988    fn writes_and_reads_literal_only_compressed_archive_with_repeated_stmode() {
1989        let data =
1990            b"this literal-only payload is long enough to enter and exit stmode more than once";
1991        let input = [FileEntry {
1992            name: b"long.txt",
1993            data,
1994            file_time: 0,
1995            file_attr: 0x20,
1996            password: None,
1997            file_comment: None,
1998        }];
1999
2000        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2001        let archive = Archive::parse(&bytes).unwrap();
2002        assert_eq!(archive.entries[0].header.method, METHOD_BEST);
2003
2004        let extracted = collect_extract(&archive, None).unwrap();
2005        assert_eq!(extracted[0].data, data);
2006    }
2007
2008    #[test]
2009    fn compressed_writer_levels_control_rar15_encoder_policy() {
2010        let mut data: Vec<_> = (0..5000).map(|index| (index * 73 + 19) as u8).collect();
2011        data.extend_from_within(..256);
2012        let input = [FileEntry {
2013            name: b"level-policy.bin",
2014            data: &data,
2015            file_time: 0,
2016            file_attr: 0x20,
2017            password: None,
2018            file_comment: None,
2019        }];
2020
2021        let level_one =
2022            write_compressed_archive(&input, WriterOptions::default().with_compression_level(1))
2023                .unwrap();
2024        let level_five =
2025            write_compressed_archive(&input, WriterOptions::default().with_compression_level(5))
2026                .unwrap();
2027        let level_one = Archive::parse(&level_one).unwrap();
2028        let level_five = Archive::parse(&level_five).unwrap();
2029        let level_one_file = &level_one.entries[0];
2030        let level_five_file = &level_five.entries[0];
2031
2032        assert_eq!(level_one_file.header.method, METHOD_BEST);
2033        assert_eq!(level_five_file.header.method, METHOD_BEST);
2034        assert!(level_five_file.header.pack_size < level_one_file.header.pack_size);
2035        assert_eq!(collect_extract(&level_one, None).unwrap()[0].data, data);
2036        assert_eq!(collect_extract(&level_five, None).unwrap()[0].data, data);
2037    }
2038
2039    #[test]
2040    fn rar14_writer_uses_dos_compatible_old_distance_policy() {
2041        for level in 0..=5 {
2042            let options = rar15_encode_options_for_level(Some(level)).unwrap();
2043            assert!(
2044                !options.old_distance_tokens_enabled(),
2045                "RAR 1.4 level {level} must not emit old-distance tokens"
2046            );
2047        }
2048    }
2049
2050    #[test]
2051    fn compressed_writer_keeps_adaptive_lz_planning_in_sync_after_literals() {
2052        let data = synthetic_log_payload(8000);
2053        let input = [FileEntry {
2054            name: b"synthetic.log",
2055            data: &data,
2056            file_time: 0,
2057            file_attr: 0x20,
2058            password: None,
2059            file_comment: None,
2060        }];
2061
2062        let bytes =
2063            write_compressed_archive(&input, WriterOptions::default().with_compression_level(2))
2064                .unwrap();
2065        let archive = Archive::parse(&bytes).unwrap();
2066        let extracted = collect_extract(&archive, None).unwrap();
2067
2068        assert_eq!(extracted[0].data, data);
2069    }
2070
2071    #[test]
2072    fn compressed_writer_emits_short_lz_matches() {
2073        let data = b"abcabcabcabcabcabcabcabcabcabcabcabc";
2074        let input = [FileEntry {
2075            name: b"repeat.txt",
2076            data,
2077            file_time: 0,
2078            file_attr: 0x20,
2079            password: None,
2080            file_comment: None,
2081        }];
2082
2083        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2084        let archive = Archive::parse(&bytes).unwrap();
2085        assert_eq!(archive.entries[0].header.method, METHOD_BEST);
2086        assert!(
2087            archive.entries[0].header.pack_size < data.len() as u32,
2088            "ShortLZ should make the repeated payload smaller than stored data"
2089        );
2090
2091        let extracted = collect_extract(&archive, None).unwrap();
2092        assert_eq!(extracted[0].data, data);
2093    }
2094
2095    #[test]
2096    fn compressed_writer_emits_long_lz_matches() {
2097        let mut data = short_lz_resistant_prefix(300);
2098        data.extend_from_within(..32);
2099        assert_eq!(
2100            find_long_lz(&data, 300, 0x8000),
2101            Some(LongLz {
2102                distance: 300,
2103                length: 32
2104            })
2105        );
2106        let input = [FileEntry {
2107            name: b"far.txt",
2108            data: &data,
2109            file_time: 0,
2110            file_attr: 0x20,
2111            password: None,
2112            file_comment: None,
2113        }];
2114
2115        let literal_only = Unpack15Encoder::new()
2116            .encode_literals_only(&data)
2117            .unwrap()
2118            .len();
2119        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2120        let archive = Archive::parse(&bytes).unwrap();
2121        assert_eq!(archive.entries[0].header.method, METHOD_BEST);
2122        assert!(
2123            (archive.entries[0].header.pack_size as usize) < literal_only,
2124            "LongLZ should make a >256-byte-distance repeat smaller than literal-only output"
2125        );
2126
2127        let extracted = collect_extract(&archive, None).unwrap();
2128        assert_eq!(extracted[0].data, data);
2129    }
2130
2131    #[test]
2132    fn compressed_writer_stores_incompressible_member_when_smaller() {
2133        let mut state = 0x8765_4321u32;
2134        let data: Vec<_> = (0..8192)
2135            .map(|_| {
2136                state ^= state << 13;
2137                state ^= state >> 17;
2138                state ^= state << 5;
2139                state as u8
2140            })
2141            .collect();
2142        let input = [FileEntry {
2143            name: b"randomish.bin",
2144            data: &data,
2145            file_time: 0,
2146            file_attr: 0x20,
2147            password: None,
2148            file_comment: None,
2149        }];
2150
2151        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2152        let archive = Archive::parse(&bytes).unwrap();
2153
2154        assert_eq!(archive.entries[0].header.method, METHOD_STORE);
2155        assert_eq!(archive.entries[0].header.pack_size, data.len() as u32);
2156        assert_eq!(collect_extract(&archive, None).unwrap()[0].data, data);
2157    }
2158
2159    #[test]
2160    fn compressed_writer_stores_tiny_incompressible_member_when_smaller() {
2161        let data = b"\x00\xff\x12\xed\x34\xcb\x56\xa9\x78\x87\x9a\x65\xbc\x43\xde\x21";
2162        let input = [FileEntry {
2163            name: b"tiny.bin",
2164            data,
2165            file_time: 0,
2166            file_attr: 0x20,
2167            password: None,
2168            file_comment: None,
2169        }];
2170
2171        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2172        let archive = Archive::parse(&bytes).unwrap();
2173
2174        assert_eq!(archive.entries[0].header.method, METHOD_STORE);
2175        assert_eq!(archive.entries[0].header.pack_size, data.len() as u32);
2176        assert_eq!(collect_extract(&archive, None).unwrap()[0].data, data);
2177    }
2178
2179    #[test]
2180    fn writes_and_reads_solid_compressed_archive() {
2181        let input = [
2182            FileEntry {
2183                name: b"first.txt",
2184                data: b"first member primes the adaptive unpack15 state",
2185                file_time: 0,
2186                file_attr: 0x20,
2187                password: None,
2188                file_comment: None,
2189            },
2190            FileEntry {
2191                name: b"second.txt",
2192                data: b"second member is encoded without resetting that state",
2193                file_time: 0,
2194                file_attr: 0x20,
2195                password: None,
2196                file_comment: None,
2197            },
2198        ];
2199        let mut features = FeatureSet::store_only();
2200        features.solid = true;
2201        let options = WriterOptions {
2202            target: ArchiveVersion::Rar14,
2203            features,
2204            ..WriterOptions::default()
2205        };
2206
2207        let bytes = write_compressed_archive(&input, options).unwrap();
2208        let archive = Archive::parse(&bytes).unwrap();
2209        assert!(archive.main.is_solid());
2210        assert_eq!(archive.entries.len(), 2);
2211        assert!(archive
2212            .entries
2213            .iter()
2214            .all(|entry| entry.header.flags & LHD_SOLID != 0));
2215
2216        let extracted = collect_extract(&archive, None).unwrap();
2217        assert_eq!(extracted[0].data, input[0].data);
2218        assert_eq!(extracted[1].data, input[1].data);
2219    }
2220
2221    #[test]
2222    fn writes_and_reads_encrypted_compressed_archive() {
2223        let input = [FileEntry {
2224            name: b"secret.txt",
2225            data: b"secret compressed bytes over sixteen",
2226            file_time: 0,
2227            file_attr: 0x20,
2228            password: Some(b"pass"),
2229            file_comment: None,
2230        }];
2231
2232        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2233        let archive = Archive::parse(&bytes).unwrap();
2234        assert!(archive.entries[0].is_encrypted());
2235        assert_eq!(archive.entries[0].header.method, METHOD_STORE);
2236        assert!(matches!(
2237            collect_extract(&archive, None),
2238            Err(Error::NeedPassword)
2239        ));
2240
2241        let extracted = collect_extract(&archive, Some(b"pass")).unwrap();
2242        assert_eq!(extracted[0].data, input[0].data);
2243    }
2244
2245    #[test]
2246    fn writes_and_reads_compressed_file_comment() {
2247        let input = [FileEntry {
2248            name: b"commented.txt",
2249            data: b"compressed member with file comment",
2250            file_time: 0,
2251            file_attr: 0x20,
2252            password: None,
2253            file_comment: Some(b"compressed file comment"),
2254        }];
2255
2256        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2257        let archive = Archive::parse(&bytes).unwrap();
2258        assert_eq!(
2259            archive.entries[0].file_comment().unwrap().as_deref(),
2260            Some(&b"compressed file comment"[..])
2261        );
2262
2263        let extracted = collect_extract(&archive, None).unwrap();
2264        assert_eq!(extracted[0].data, input[0].data);
2265    }
2266
2267    #[test]
2268    fn writes_and_reads_stored_multivolume_archive() {
2269        let entry = StoredEntry {
2270            name: b"random.bin",
2271            data: b"abcdefghijklmnopqrstuvwxyz0123456789",
2272            file_time: 0,
2273            file_attr: 0x20,
2274            password: None,
2275            file_comment: None,
2276        };
2277
2278        let bytes = write_stored_volumes(entry, WriterOptions::default(), 10).unwrap();
2279        assert_eq!(bytes.len(), 4);
2280        let volumes: Vec<_> = bytes
2281            .iter()
2282            .map(|bytes| Archive::parse(bytes).unwrap())
2283            .collect();
2284        assert!(volumes.iter().all(|archive| archive.main.is_volume()));
2285        assert!(!volumes[0].entries[0].is_split_before());
2286        assert!(volumes[0].entries[0].is_split_after());
2287        assert!(volumes[1].entries[0].is_split_before());
2288        assert!(volumes[1].entries[0].is_split_after());
2289        assert!(volumes[3].entries[0].is_split_before());
2290        assert!(!volumes[3].entries[0].is_split_after());
2291        assert!(volumes.iter().all(|archive| archive.entries[0].is_stored()));
2292
2293        let extracted = collect_extract_volumes(&volumes, None).unwrap();
2294        assert_eq!(extracted.len(), 1);
2295        assert_eq!(extracted[0].name, b"random.bin");
2296        assert_eq!(extracted[0].data, entry.data);
2297    }
2298
2299    #[test]
2300    fn writes_and_reads_compressed_multivolume_archive() {
2301        let data = b"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc";
2302        let entry = FileEntry {
2303            name: b"repeat.txt",
2304            data,
2305            file_time: 0,
2306            file_attr: 0x20,
2307            password: None,
2308            file_comment: None,
2309        };
2310
2311        let bytes = write_compressed_volumes(entry, WriterOptions::default(), 8).unwrap();
2312        assert!(bytes.len() >= 2);
2313        let volumes: Vec<_> = bytes
2314            .iter()
2315            .map(|bytes| Archive::parse(bytes).unwrap())
2316            .collect();
2317        assert!(volumes.iter().all(|archive| archive.main.is_volume()));
2318        assert!(!volumes[0].entries[0].is_stored());
2319        assert!(volumes[0].entries[0].is_split_after());
2320        assert!(volumes.last().unwrap().entries[0].is_split_before());
2321        assert!(!volumes.last().unwrap().entries[0].is_split_after());
2322
2323        let extracted = collect_extract_volumes(&volumes, None).unwrap();
2324        assert_eq!(extracted.len(), 1);
2325        assert_eq!(extracted[0].name, b"repeat.txt");
2326        assert_eq!(extracted[0].data, data);
2327    }
2328
2329    fn short_lz_resistant_prefix(len: usize) -> Vec<u8> {
2330        let mut data = Vec::with_capacity(len);
2331        while data.len() < len {
2332            let next = (0u8..=u8::MAX)
2333                .find(|&candidate| {
2334                    if data.len() < 2 {
2335                        return true;
2336                    }
2337                    let start = data.len().saturating_sub(256);
2338                    !data[start..].windows(3).any(|window| {
2339                        window == [data[data.len() - 2], data[data.len() - 1], candidate]
2340                    })
2341                })
2342                .expect("byte alphabet can avoid local 3-byte repeats");
2343            data.push(next);
2344        }
2345        data
2346    }
2347
2348    #[test]
2349    fn writes_empty_compressed_archive_member() {
2350        let input = [FileEntry {
2351            name: b"empty.bin",
2352            data: b"",
2353            file_time: 0,
2354            file_attr: 0x20,
2355            password: None,
2356            file_comment: None,
2357        }];
2358
2359        let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
2360        let archive = Archive::parse(&bytes).unwrap();
2361        assert_eq!(archive.entries[0].header.method, METHOD_STORE);
2362        assert_eq!(archive.entries[0].header.pack_size, 0);
2363
2364        let extracted = collect_extract(&archive, None).unwrap();
2365        assert_eq!(extracted[0].data, b"");
2366    }
2367
2368    #[test]
2369    fn rejects_rar5_only_features_for_rar13() {
2370        let mut features = FeatureSet::store_only();
2371        features.quick_open = true;
2372
2373        let options = WriterOptions {
2374            target: ArchiveVersion::Rar13,
2375            features,
2376            ..WriterOptions::default()
2377        };
2378        let err = write_stored_archive(&[], options).unwrap_err();
2379        assert_eq!(
2380            err,
2381            Error::UnsupportedFeature {
2382                version: ArchiveVersion::Rar13,
2383                feature: "quick_open"
2384            }
2385        );
2386    }
2387
2388    #[test]
2389    fn rejects_unimplemented_rar13_writer_features() {
2390        let mut features = FeatureSet::store_only();
2391        features.sfx = true;
2392
2393        let options = WriterOptions {
2394            target: ArchiveVersion::Rar14,
2395            features,
2396            ..WriterOptions::default()
2397        };
2398        let err = write_stored_archive(&[], options).unwrap_err();
2399        assert_eq!(
2400            err,
2401            Error::UnsupportedFeature {
2402                version: ArchiveVersion::Rar14,
2403                feature: "sfx"
2404            }
2405        );
2406    }
2407
2408    #[test]
2409    fn file_checksum_matches_rar13_algorithm() {
2410        assert_eq!(file_checksum(b""), 0x0000);
2411        assert_eq!(file_checksum(b"123456789"), 0xc78a);
2412    }
2413
2414    #[test]
2415    fn rar13_checksum_writer_flush_propagates_to_inner_writer() {
2416        struct FlushSpy {
2417            data: Vec<u8>,
2418            flushed: usize,
2419        }
2420        impl Write for FlushSpy {
2421            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2422                self.data.extend_from_slice(buf);
2423                Ok(buf.len())
2424            }
2425            fn flush(&mut self) -> std::io::Result<()> {
2426                self.flushed += 1;
2427                Ok(())
2428            }
2429        }
2430        let mut inner = FlushSpy {
2431            data: Vec::new(),
2432            flushed: 0,
2433        };
2434        let mut checksum = Rar13Checksum::new();
2435        let mut writer = Rar13ChecksumWriter {
2436            inner: &mut inner,
2437            checksum: &mut checksum,
2438        };
2439        writer.write_all(b"hello").unwrap();
2440        writer.flush().unwrap();
2441        assert_eq!(inner.data, b"hello");
2442        assert_eq!(inner.flushed, 1);
2443    }
2444
2445    #[test]
2446    fn entry_packed_data_returns_borrowed_slice_for_memory_archives() {
2447        let payload = b"packed_data direct accessor coverage";
2448        let input = [StoredEntry {
2449            name: b"slice.bin",
2450            data: payload,
2451            file_time: 0,
2452            file_attr: 0x20,
2453            password: None,
2454            file_comment: None,
2455        }];
2456
2457        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
2458        let archive = Archive::parse(&bytes).unwrap();
2459        let entry = &archive.entries[0];
2460
2461        let packed = entry.packed_data(&archive).unwrap();
2462        assert_eq!(packed, payload);
2463        assert!(!packed.is_empty());
2464    }
2465
2466    #[test]
2467    fn extract_volumes_to_annotates_failed_non_split_entry_with_at_entry() {
2468        let payload = b"corrupt-me-please";
2469        let input = [StoredEntry {
2470            name: b"plain.bin",
2471            data: payload,
2472            file_time: 0,
2473            file_attr: 0x20,
2474            password: None,
2475            file_comment: None,
2476        }];
2477
2478        let mut bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
2479        let archive = Archive::parse(&bytes).unwrap();
2480        let range = archive.entries[0].packed_range.clone();
2481        // Flip a byte in the stored payload so the checksum no longer matches.
2482        bytes[range.start] ^= 0xff;
2483
2484        let corrupted = Archive::parse(&bytes).unwrap();
2485        let err = collect_extract_volumes(std::slice::from_ref(&corrupted), None).unwrap_err();
2486        match err {
2487            Error::AtEntry {
2488                name,
2489                operation,
2490                source,
2491            } => {
2492                assert_eq!(name, b"plain.bin");
2493                assert_eq!(operation, "extracting");
2494                assert!(matches!(*source, Error::CrcMismatch { .. }));
2495            }
2496            other => panic!("expected AtEntry annotation, got {other:?}"),
2497        }
2498    }
2499
2500    #[test]
2501    fn extract_volumes_to_annotates_failed_split_completion_with_at_entry() {
2502        let entry = StoredEntry {
2503            name: b"split.bin",
2504            data: b"abcdefghijklmnopqrstuvwxyz0123456789",
2505            file_time: 0,
2506            file_attr: 0x20,
2507            password: None,
2508            file_comment: None,
2509        };
2510
2511        let mut volume_bytes = write_stored_volumes(entry, WriterOptions::default(), 10).unwrap();
2512        assert!(
2513            volume_bytes.len() >= 2,
2514            "need at least two volumes to exercise the split-completion path"
2515        );
2516
2517        // Corrupt the last fragment so PendingSplitRefs::write_to fails on assembly.
2518        let last_index = volume_bytes.len() - 1;
2519        let last_archive = Archive::parse(&volume_bytes[last_index]).unwrap();
2520        let last_range = last_archive.entries[0].packed_range.clone();
2521        volume_bytes[last_index][last_range.start] ^= 0x7f;
2522
2523        let volumes: Vec<_> = volume_bytes
2524            .iter()
2525            .map(|bytes| Archive::parse(bytes).unwrap())
2526            .collect();
2527
2528        let err = collect_extract_volumes(&volumes, None).unwrap_err();
2529        match err {
2530            Error::AtEntry {
2531                name,
2532                operation,
2533                source,
2534            } => {
2535                assert_eq!(name, b"split.bin");
2536                assert_eq!(operation, "extracting");
2537                assert!(
2538                    matches!(*source, Error::CrcMismatch { .. }),
2539                    "expected CrcMismatch source, got {source:?}"
2540                );
2541            }
2542            other => panic!("expected AtEntry annotation, got {other:?}"),
2543        }
2544    }
2545
2546    #[test]
2547    fn entry_packed_data_refuses_to_buffer_file_backed_archives() {
2548        let payload = b"packed_data refuses file-backed";
2549        let input = [StoredEntry {
2550            name: b"file.bin",
2551            data: payload,
2552            file_time: 0,
2553            file_attr: 0x20,
2554            password: None,
2555            file_comment: None,
2556        }];
2557        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
2558
2559        let dir =
2560            std::env::temp_dir().join(format!("rars-rar13-packed-data-{}", std::process::id()));
2561        std::fs::create_dir_all(&dir).unwrap();
2562        let path = dir.join("entry.rar");
2563        std::fs::write(&path, &bytes).unwrap();
2564
2565        let archive = Archive::parse_path(&path).unwrap();
2566        let result = archive.entries[0].packed_data(&archive);
2567        assert_eq!(
2568            result,
2569            Err(Error::InvalidHeader(
2570                "RAR 1.3 file-backed packed data requires owned read"
2571            ))
2572        );
2573
2574        std::fs::remove_file(&path).ok();
2575        std::fs::remove_dir(&dir).ok();
2576    }
2577
2578    fn parse_volumes(bytes: &[Vec<u8>]) -> Vec<Archive> {
2579        bytes.iter().map(|b| Archive::parse(b).unwrap()).collect()
2580    }
2581
2582    fn split_volumes_for(name: &[u8], data: &[u8]) -> Vec<Vec<u8>> {
2583        write_stored_volumes(
2584            StoredEntry {
2585                name,
2586                data,
2587                file_time: 0,
2588                file_attr: 0x20,
2589                password: None,
2590                file_comment: None,
2591            },
2592            WriterOptions::default(),
2593            10,
2594        )
2595        .unwrap()
2596    }
2597
2598    #[test]
2599    fn extract_volumes_to_rejects_pending_split_interrupted_by_regular_entry() {
2600        let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
2601        let mut volumes = parse_volumes(&bytes);
2602
2603        // After volume 0's split_after entry, append a regular entry to the
2604        // same volume so the loop sees pending=Some when it hits a non-split.
2605        let mut intruder = volumes[0].entries[0].clone();
2606        intruder.header.flags &= !(LHD_SPLIT_BEFORE | LHD_SPLIT_AFTER);
2607        intruder.name = b"intruder.bin".to_vec();
2608        volumes[0].entries.push(intruder);
2609
2610        let err = collect_extract_volumes(&volumes, None).unwrap_err();
2611        assert_eq!(
2612            err,
2613            Error::InvalidHeader("RAR 1.3 split entry is interrupted by a regular entry"),
2614        );
2615    }
2616
2617    #[test]
2618    fn extract_volumes_to_rejects_split_with_inconsistent_flags() {
2619        let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
2620        let volumes = parse_volumes(&bytes);
2621
2622        // Take just the *middle* volume in isolation: it has split_before=true
2623        // but no preceding pending state, which the match arm treats as
2624        // structurally inconsistent.
2625        let middle = volumes.into_iter().nth(1).unwrap();
2626        let err = collect_extract_volumes(std::slice::from_ref(&middle), None).unwrap_err();
2627        assert_eq!(
2628            err,
2629            Error::InvalidHeader("RAR 1.3 split entry flags are inconsistent"),
2630        );
2631    }
2632
2633    #[test]
2634    fn extract_volumes_to_rejects_pending_split_left_incomplete_at_end() {
2635        let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
2636        let volumes = parse_volumes(&bytes);
2637
2638        // Use only the first volume, which leaves pending=Some after the loop.
2639        let err = collect_extract_volumes(std::slice::from_ref(&volumes[0]), None).unwrap_err();
2640        assert_eq!(
2641            err,
2642            Error::InvalidHeader("RAR 1.3 split entry is incomplete")
2643        );
2644    }
2645
2646    #[test]
2647    fn extract_volumes_to_rejects_split_fragments_with_drifted_attributes() {
2648        let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
2649        let mut volumes = parse_volumes(&bytes);
2650
2651        // Mutate the second volume's entry name so PendingSplitRefs::append
2652        // refuses it on a name-mismatch.
2653        volumes[1].entries[0].name = b"different.bin".to_vec();
2654
2655        let err = collect_extract_volumes(&volumes, None).unwrap_err();
2656        assert_eq!(
2657            err,
2658            Error::InvalidHeader("RAR 1.3 split entry name changed")
2659        );
2660    }
2661
2662    #[test]
2663    fn extract_volumes_to_rejects_split_fragments_with_drifted_method() {
2664        let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
2665        let mut volumes = parse_volumes(&bytes);
2666
2667        // Drift the compression method on the second fragment.
2668        volumes[1].entries[0].header.method = METHOD_BEST;
2669        let err = collect_extract_volumes(&volumes, None).unwrap_err();
2670        assert_eq!(
2671            err,
2672            Error::InvalidHeader("RAR 1.3 split entry compression method changed"),
2673        );
2674    }
2675
2676    #[test]
2677    fn extract_volumes_to_carries_directory_entries_across_volume_array() {
2678        // A directory entry has zero-length data and gets the open() callback
2679        // invoked but no payload write. Putting it in a volumes array keeps
2680        // the directory branch in extract_volumes_to (rather than extract_to)
2681        // exercised.
2682        let input = [StoredEntry {
2683            name: b"docs",
2684            data: b"",
2685            file_time: 0,
2686            file_attr: 0x10,
2687            password: None,
2688            file_comment: None,
2689        }];
2690        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
2691        let archive = Archive::parse(&bytes).unwrap();
2692
2693        let extracted = collect_extract_volumes(std::slice::from_ref(&archive), None).unwrap();
2694        assert_eq!(extracted.len(), 1);
2695        assert!(extracted[0].is_directory);
2696        assert_eq!(extracted[0].name, b"docs");
2697    }
2698
2699    #[test]
2700    fn extract_volumes_to_routes_pending_split_reader_through_fragment_chain() {
2701        // Larger payload over more volumes guarantees that the chained
2702        // fragment reader reads from each volume's range_reader at least once,
2703        // exercising the success arms of fragment_reader, write_to, and
2704        // ChainedReader::read across multiple volumes.
2705        let payload: Vec<u8> = (0..96).map(|i| ((i * 53) ^ 0xa5) as u8).collect();
2706        let bytes = split_volumes_for(b"chain.bin", &payload);
2707        assert!(
2708            bytes.len() >= 3,
2709            "need at least three volumes for the chain"
2710        );
2711        let volumes = parse_volumes(&bytes);
2712
2713        let extracted = collect_extract_volumes(&volumes, None).unwrap();
2714        assert_eq!(extracted.len(), 1);
2715        assert_eq!(extracted[0].data, payload);
2716    }
2717
2718    #[test]
2719    fn write_compressed_archive_with_comment_round_trips_through_archive_comment() {
2720        let data = b"compressed archive comment payload payload payload";
2721        let comment = b"This is a compressed archive comment.";
2722        let input = [FileEntry {
2723            name: b"payload.txt",
2724            data,
2725            file_time: 0,
2726            file_attr: 0x20,
2727            password: None,
2728            file_comment: None,
2729        }];
2730
2731        let bytes =
2732            write_compressed_archive_with_comment(&input, WriterOptions::default(), Some(comment))
2733                .unwrap();
2734        let archive = Archive::parse(&bytes).unwrap();
2735        assert!(archive.main.has_archive_comment());
2736        assert!(archive.main.has_packed_comment());
2737        assert_eq!(
2738            archive.archive_comment().unwrap().as_deref(),
2739            Some(&comment[..])
2740        );
2741
2742        let extracted = collect_extract(&archive, None).unwrap();
2743        assert_eq!(extracted[0].data, data);
2744    }
2745
2746    #[test]
2747    fn write_compressed_archive_with_comment_emits_solid_compressed_archive() {
2748        let data1 = b"solid compressed payload one with overlap overlap overlap";
2749        let data2 = b"solid compressed payload two with overlap overlap overlap";
2750        let mut features = FeatureSet::store_only();
2751        features.solid = true;
2752        let options = WriterOptions {
2753            target: ArchiveVersion::Rar14,
2754            features,
2755            ..WriterOptions::default()
2756        };
2757        let input = [
2758            FileEntry {
2759                name: b"a.txt",
2760                data: data1,
2761                file_time: 0,
2762                file_attr: 0x20,
2763                password: None,
2764                file_comment: None,
2765            },
2766            FileEntry {
2767                name: b"b.txt",
2768                data: data2,
2769                file_time: 0,
2770                file_attr: 0x20,
2771                password: None,
2772                file_comment: None,
2773            },
2774        ];
2775
2776        let bytes = write_compressed_archive_with_comment(&input, options, None).unwrap();
2777        let archive = Archive::parse(&bytes).unwrap();
2778        assert!(archive.main.is_solid());
2779        assert_eq!(archive.entries.len(), 2);
2780
2781        let extracted = collect_extract(&archive, None).unwrap();
2782        assert_eq!(extracted[0].data, data1);
2783        assert_eq!(extracted[1].data, data2);
2784    }
2785
2786    #[test]
2787    fn write_compressed_archive_with_comment_rejects_non_rar13_target() {
2788        let options = WriterOptions {
2789            target: ArchiveVersion::Rar15,
2790            ..WriterOptions::default()
2791        };
2792        let err = write_compressed_archive_with_comment(&[], options, None).unwrap_err();
2793        assert_eq!(err, Error::UnsupportedVersion(ArchiveVersion::Rar15));
2794    }
2795
2796    #[test]
2797    fn parse_path_round_trips_multi_entry_archive_via_file_backed_seekable_path() {
2798        // Two entries plus an archive comment forces parse_seekable to walk
2799        // through more than one file-header read iteration.
2800        let input = [
2801            StoredEntry {
2802                name: b"first.txt",
2803                data: b"first payload",
2804                file_time: 0,
2805                file_attr: 0x20,
2806                password: None,
2807                file_comment: None,
2808            },
2809            StoredEntry {
2810                name: b"second.txt",
2811                data: b"second payload",
2812                file_time: 0,
2813                file_attr: 0x20,
2814                password: None,
2815                file_comment: None,
2816            },
2817        ];
2818        let bytes = write_stored_archive_with_comment(
2819            &input,
2820            WriterOptions::default(),
2821            Some(b"file-backed comment"),
2822        )
2823        .unwrap();
2824
2825        let dir =
2826            std::env::temp_dir().join(format!("rars-rar13-parse-seekable-{}", std::process::id()));
2827        std::fs::create_dir_all(&dir).unwrap();
2828        let path = dir.join("multi.rar");
2829        std::fs::write(&path, &bytes).unwrap();
2830
2831        let archive = Archive::parse_path(&path).unwrap();
2832        assert_eq!(archive.entries.len(), 2);
2833        assert_eq!(archive.entries[0].name, b"first.txt");
2834        assert_eq!(archive.entries[1].name, b"second.txt");
2835        assert_eq!(
2836            archive.archive_comment().unwrap().as_deref(),
2837            Some(&b"file-backed comment"[..])
2838        );
2839
2840        std::fs::remove_file(&path).ok();
2841        std::fs::remove_dir(&dir).ok();
2842    }
2843
2844    #[test]
2845    fn parse_path_rejects_files_without_rar13_signature() {
2846        let dir =
2847            std::env::temp_dir().join(format!("rars-rar13-parse-path-bad-{}", std::process::id()));
2848        std::fs::create_dir_all(&dir).unwrap();
2849        let path = dir.join("not_a_rar.bin");
2850        std::fs::write(&path, [0u8; 64]).unwrap();
2851
2852        let err = Archive::parse_path(&path).unwrap_err();
2853        assert_eq!(err, Error::UnsupportedSignature);
2854
2855        std::fs::remove_file(&path).ok();
2856        std::fs::remove_dir(&dir).ok();
2857    }
2858
2859    #[test]
2860    fn extract_to_encrypted_archive_reads_through_file_backed_decrypted_range() {
2861        // The Memory-backed path is already exercised; this test takes the
2862        // same encrypted archive out to disk so copy_decrypted_range_to runs
2863        // its ArchiveSource::File branch.
2864        let input = [StoredEntry {
2865            name: b"secret.bin",
2866            data: b"file-backed secret payload",
2867            file_time: 0,
2868            file_attr: 0x20,
2869            password: Some(b"pw"),
2870            file_comment: None,
2871        }];
2872        let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
2873
2874        let dir =
2875            std::env::temp_dir().join(format!("rars-rar13-decrypt-file-{}", std::process::id()));
2876        std::fs::create_dir_all(&dir).unwrap();
2877        let path = dir.join("encrypted.rar");
2878        std::fs::write(&path, &bytes).unwrap();
2879
2880        let archive = Archive::parse_path(&path).unwrap();
2881        let extracted = collect_extract(&archive, Some(b"pw")).unwrap();
2882        assert_eq!(extracted[0].data, b"file-backed secret payload");
2883
2884        std::fs::remove_file(&path).ok();
2885        std::fs::remove_dir(&dir).ok();
2886    }
2887
2888    #[test]
2889    fn write_stored_volumes_rejects_password_protected_entries() {
2890        let entry = StoredEntry {
2891            name: b"locked.bin",
2892            data: b"data",
2893            file_time: 0,
2894            file_attr: 0x20,
2895            password: Some(b"pw"),
2896            file_comment: None,
2897        };
2898        let err = write_stored_volumes(entry, WriterOptions::default(), 16).unwrap_err();
2899        assert_eq!(
2900            err,
2901            Error::UnsupportedFeature {
2902                version: ArchiveVersion::Rar14,
2903                feature: "volume_password",
2904            }
2905        );
2906    }
2907
2908    #[test]
2909    fn write_compressed_volumes_rejects_archive_comment_feature() {
2910        let mut features = FeatureSet::store_only();
2911        features.archive_comment = true;
2912        let entry = FileEntry {
2913            name: b"with-comment.bin",
2914            data: b"data",
2915            file_time: 0,
2916            file_attr: 0x20,
2917            password: None,
2918            file_comment: None,
2919        };
2920        let err = write_compressed_volumes(
2921            entry,
2922            WriterOptions {
2923                target: ArchiveVersion::Rar14,
2924                features,
2925                ..WriterOptions::default()
2926            },
2927            16,
2928        )
2929        .unwrap_err();
2930        assert_eq!(
2931            err,
2932            Error::UnsupportedFeature {
2933                version: ArchiveVersion::Rar14,
2934                feature: "volume_archive_comment",
2935            }
2936        );
2937    }
2938
2939    #[test]
2940    fn write_compressed_volumes_rejects_non_rar13_target() {
2941        let options = WriterOptions {
2942            target: ArchiveVersion::Rar20,
2943            ..WriterOptions::default()
2944        };
2945        let entry = FileEntry {
2946            name: b"x.bin",
2947            data: b"data",
2948            file_time: 0,
2949            file_attr: 0x20,
2950            password: None,
2951            file_comment: None,
2952        };
2953        let err = write_compressed_volumes(entry, options, 16).unwrap_err();
2954        assert_eq!(err, Error::UnsupportedVersion(ArchiveVersion::Rar20));
2955    }
2956
2957    #[test]
2958    fn file_header_parse_rejects_input_below_base_size() {
2959        let err = FileHeader::parse(&[0u8; FILE_HEAD_BASE_SIZE - 1]).unwrap_err();
2960        assert_eq!(err, Error::TooShort);
2961    }
2962
2963    #[test]
2964    fn file_header_parse_rejects_truncated_input_against_declared_head_size() {
2965        // Build a syntactically OK FILE_HEAD_BASE_SIZE buffer that declares a
2966        // head_size larger than the slice we pass in — exercises the
2967        // post-name-size length check at the end of FileHeader::parse.
2968        let mut header = [0u8; FILE_HEAD_BASE_SIZE];
2969        // pack_size, unp_size, file_crc, file_time stay zero.
2970        let declared_head_size: u16 = (FILE_HEAD_BASE_SIZE + 32) as u16;
2971        header[10..12].copy_from_slice(&declared_head_size.to_le_bytes());
2972        // name_size = 0 keeps minimum_size == FILE_HEAD_BASE_SIZE so the
2973        // earlier "shorter than its name" branch is bypassed.
2974        header[19] = 0;
2975        let err = FileHeader::parse(&header).unwrap_err();
2976        assert_eq!(err, Error::TooShort);
2977    }
2978
2979    #[test]
2980    fn archive_comment_rejects_size_field_shorter_than_two_bytes() {
2981        // Build a valid stored archive then patch its main header so the
2982        // declared comment field is shorter than two bytes — exercises the
2983        // "packed archive comment is shorter than size field" arm.
2984        let input = [StoredEntry {
2985            name: b"file.bin",
2986            data: b"data",
2987            file_time: 0,
2988            file_attr: 0x20,
2989            password: None,
2990            file_comment: None,
2991        }];
2992        let bytes =
2993            write_stored_archive_with_comment(&input, WriterOptions::default(), Some(b"hi"))
2994                .unwrap();
2995        let mut archive = Archive::parse(&bytes).unwrap();
2996        // The first two bytes of `main.extra` are the comment_field length —
2997        // overwrite them with 1 to declare a sub-2-byte payload while keeping
2998        // the packed-comment flag set.
2999        archive.main.extra[0] = 1;
3000        archive.main.extra[1] = 0;
3001        assert_eq!(
3002            archive.archive_comment(),
3003            Err(Error::InvalidHeader(
3004                "RAR 1.3 packed archive comment is shorter than size field"
3005            ))
3006        );
3007    }
3008
3009    #[test]
3010    fn archive_comment_rejects_packed_payload_extending_past_extra_buffer() {
3011        let input = [StoredEntry {
3012            name: b"file.bin",
3013            data: b"data",
3014            file_time: 0,
3015            file_attr: 0x20,
3016            password: None,
3017            file_comment: None,
3018        }];
3019        let bytes =
3020            write_stored_archive_with_comment(&input, WriterOptions::default(), Some(b"hi"))
3021                .unwrap();
3022        let mut archive = Archive::parse(&bytes).unwrap();
3023        // Pump the declared comment field length up so the packed range walks
3024        // past the end of `main.extra`.
3025        let inflated = (archive.main.extra.len() as u16 + 16).to_le_bytes();
3026        archive.main.extra[0] = inflated[0];
3027        archive.main.extra[1] = inflated[1];
3028        assert_eq!(archive.archive_comment(), Err(Error::TooShort));
3029    }
3030}