Skip to main content

rars_format/
rar15_40.rs

1use crate::detect::{find_archive_start, ArchiveSignature, RAR15_SIGNATURE};
2use crate::error::{Error, Result};
3use crate::features::FeatureSet;
4use crate::io_util::{align16 as checked_align16, read_exact_at, read_u16, read_u32};
5pub(crate) use crate::source::ArchiveSource;
6use crate::version::ArchiveFamily;
7use crate::ArchiveVersion;
8use rars_codec::rar13::Unpack15;
9use rars_codec::rar20::Unpack20;
10use rars_codec::rar29::Unpack29;
11use rars_crc32::{crc32, Crc32};
12use rars_crypto::rar15::Rar15Cipher;
13use rars_crypto::rar20::Rar20Cipher;
14use rars_crypto::rar30::{Error as Rar30Error, Rar30Cipher};
15use std::fs::File;
16use std::io::{Read, Write};
17use std::ops::Range;
18use std::path::Path;
19use std::sync::Arc;
20
21mod extract;
22mod write;
23pub use extract::extract_volumes_to;
24use extract::{DecoderSession, DecryptingReader};
25pub use write::{
26    write_compressed_archive, write_compressed_archive_with_comment, write_compressed_volumes,
27    write_rar29_compressed_archive_with_filter_policy, write_stored_archive,
28    write_stored_archive_with_comment, write_stored_volumes, FilterKind, FilterPolicy, FilterSpec,
29};
30
31const MARK_HEAD: u8 = 0x72;
32const MAIN_HEAD: u8 = 0x73;
33const FILE_HEAD: u8 = 0x74;
34const COMM_HEAD: u8 = 0x75;
35const PROTECT_HEAD: u8 = 0x78;
36const NEWSUB_HEAD: u8 = 0x7a;
37const ENDARC_HEAD: u8 = 0x7b;
38
39const LONG_BLOCK: u16 = 0x8000;
40const MHD_VOLUME: u16 = 0x0001;
41const MHD_COMMENT: u16 = 0x0002;
42const MHD_SOLID: u16 = 0x0008;
43const MHD_NEWNUMBERING: u16 = 0x0010;
44const MHD_PROTECT: u16 = 0x0040;
45const MHD_PASSWORD: u16 = 0x0080;
46const MHD_FIRSTVOLUME: u16 = 0x0100;
47const MHD_ENCRYPTVER: u16 = 0x0200;
48
49const FHD_SPLIT_BEFORE: u16 = 0x0001;
50const FHD_SPLIT_AFTER: u16 = 0x0002;
51const FHD_PASSWORD: u16 = 0x0004;
52const FHD_COMMENT: u16 = 0x0008;
53const FHD_SOLID: u16 = 0x0010;
54const FHD_LARGE: u16 = 0x0100;
55const FHD_UNICODE: u16 = 0x0200;
56const FHD_SALT: u16 = 0x0400;
57const FHD_EXTTIME: u16 = 0x1000;
58const FHD_DIRECTORY_MASK: u16 = 0x00e0;
59
60#[derive(Debug, Clone)]
61#[non_exhaustive]
62pub struct Archive {
63    pub sfx_offset: usize,
64    pub main: MainHeader,
65    pub blocks: Vec<Block>,
66    source: ArchiveSource,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[non_exhaustive]
71pub struct MainHeader {
72    pub head_crc: u16,
73    pub flags: u16,
74    pub head_size: u16,
75    pub reserved1: u16,
76    pub reserved2: u32,
77    pub encrypt_version: Option<u8>,
78}
79
80impl MainHeader {
81    pub fn has_archive_comment(&self) -> bool {
82        self.flags & MHD_COMMENT != 0
83    }
84
85    pub fn is_volume(&self) -> bool {
86        self.flags & MHD_VOLUME != 0
87    }
88
89    pub fn is_solid(&self) -> bool {
90        self.flags & MHD_SOLID != 0
91    }
92
93    pub fn uses_new_numbering(&self) -> bool {
94        self.flags & MHD_NEWNUMBERING != 0
95    }
96
97    pub fn has_recovery_record(&self) -> bool {
98        self.flags & MHD_PROTECT != 0
99    }
100
101    pub fn has_encrypted_headers(&self) -> bool {
102        self.flags & MHD_PASSWORD != 0
103    }
104
105    pub fn is_first_volume(&self) -> bool {
106        self.flags & MHD_FIRSTVOLUME != 0
107    }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111#[non_exhaustive]
112pub enum Block {
113    File(FileHeader),
114    Comment(CommentHeader),
115    Protect(ProtectHeader),
116    NewSub(NewSubHeader),
117    End(BlockHeader),
118    Unknown(BlockHeader),
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122#[non_exhaustive]
123pub struct BlockHeader {
124    pub head_crc: u16,
125    pub head_type: u8,
126    pub flags: u16,
127    pub head_size: u16,
128    pub add_size: Option<u64>,
129    pub offset: usize,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133#[non_exhaustive]
134pub struct FileHeader {
135    pub block: BlockHeader,
136    pub pack_size: u64,
137    pub unp_size: u64,
138    pub host_os: u8,
139    pub file_crc: u32,
140    pub file_time: u32,
141    pub unp_ver: u8,
142    pub method: u8,
143    pub name: Vec<u8>,
144    pub attr: u32,
145    pub salt: Option<[u8; 8]>,
146    pub file_comment: Vec<u8>,
147    pub ext_time: Vec<u8>,
148    pub packed_range: Range<usize>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152#[non_exhaustive]
153pub struct NewSubHeader {
154    pub file: FileHeader,
155    pub kind: NewSubKind,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
159#[non_exhaustive]
160pub struct CommentHeader {
161    pub block: BlockHeader,
162    pub unp_size: u16,
163    pub unp_ver: u8,
164    pub method: u8,
165    pub comment_crc: u16,
166    pub packed_range: Range<usize>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170#[non_exhaustive]
171pub struct ProtectHeader {
172    pub block: BlockHeader,
173    pub version: u8,
174    pub rec_sectors: u16,
175    pub total_blocks: u32,
176    pub mark: [u8; 8],
177    pub data_range: Range<usize>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181#[non_exhaustive]
182pub enum NewSubKind {
183    ArchiveComment,
184    RecoveryRecord,
185    Unknown(Vec<u8>),
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189#[non_exhaustive]
190pub struct WriterOptions {
191    pub target: ArchiveVersion,
192    pub features: FeatureSet,
193    pub compression_level: Option<u8>,
194    pub dictionary_size: Option<usize>,
195}
196
197impl WriterOptions {
198    pub const fn new(target: ArchiveVersion, features: FeatureSet) -> Self {
199        Self {
200            target,
201            features,
202            compression_level: None,
203            dictionary_size: None,
204        }
205    }
206
207    pub const fn with_compression_level(mut self, level: u8) -> Self {
208        self.compression_level = Some(level);
209        self
210    }
211
212    pub const fn with_dictionary_size(mut self, size: usize) -> Self {
213        self.dictionary_size = Some(size);
214        self
215    }
216}
217
218impl Default for WriterOptions {
219    fn default() -> Self {
220        Self {
221            target: ArchiveVersion::Rar15,
222            features: FeatureSet::store_only(),
223            compression_level: None,
224            dictionary_size: None,
225        }
226    }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct StoredEntry<'a> {
231    pub name: &'a [u8],
232    pub data: &'a [u8],
233    pub file_time: u32,
234    pub file_attr: u32,
235    pub host_os: u8,
236    pub password: Option<&'a [u8]>,
237    pub file_comment: Option<&'a [u8]>,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct FileEntry<'a> {
242    pub name: &'a [u8],
243    pub data: &'a [u8],
244    pub file_time: u32,
245    pub file_attr: u32,
246    pub host_os: u8,
247    pub password: Option<&'a [u8]>,
248    pub file_comment: Option<&'a [u8]>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252#[non_exhaustive]
253pub struct ExtractedEntryMeta {
254    pub name: Vec<u8>,
255    pub file_time: u32,
256    pub attr: u32,
257    pub host_os: u8,
258    pub is_directory: bool,
259}
260
261impl FileHeader {
262    pub fn name_bytes(&self) -> &[u8] {
263        &self.name
264    }
265
266    /// Returns the file name with invalid UTF-8 replaced for display only.
267    ///
268    /// Use [`Self::name_bytes`] when exact archive bytes matter.
269    pub fn name_lossy(&self) -> String {
270        String::from_utf8_lossy(&self.name).into_owned()
271    }
272
273    pub fn is_split_before(&self) -> bool {
274        self.block.flags & FHD_SPLIT_BEFORE != 0
275    }
276
277    pub fn is_split_after(&self) -> bool {
278        self.block.flags & FHD_SPLIT_AFTER != 0
279    }
280
281    pub fn is_encrypted(&self) -> bool {
282        self.block.flags & FHD_PASSWORD != 0
283    }
284
285    pub fn is_solid(&self) -> bool {
286        self.block.flags & FHD_SOLID != 0
287    }
288
289    pub fn is_directory(&self) -> bool {
290        self.block.flags & FHD_DIRECTORY_MASK == FHD_DIRECTORY_MASK
291    }
292
293    pub fn has_ext_time(&self) -> bool {
294        self.block.flags & FHD_EXTTIME != 0
295    }
296
297    pub fn has_file_comment(&self) -> bool {
298        self.block.flags & FHD_COMMENT != 0 && !self.file_comment.is_empty()
299    }
300
301    pub fn file_comment(&self) -> Result<Option<Vec<u8>>> {
302        if !self.has_file_comment() {
303            return Ok(None);
304        }
305        let size = read_u16(&self.file_comment, 0)? as usize;
306        let start = 2usize;
307        let end = start
308            .checked_add(size)
309            .ok_or(Error::InvalidHeader("RAR 1.5 file comment size overflows"))?;
310        let comment = self.file_comment.get(start..end).ok_or(Error::TooShort)?;
311        Ok(Some(comment.to_vec()))
312    }
313
314    pub fn is_stored(&self) -> bool {
315        self.method == 0x30
316    }
317
318    pub fn packed_data(&self, archive: &Archive) -> Result<Vec<u8>> {
319        archive.read_range(self.packed_range.clone())
320    }
321
322    pub fn write_packed_data(&self, archive: &Archive, out: &mut impl Write) -> Result<()> {
323        archive.copy_range_to(self.packed_range.clone(), out)
324    }
325
326    pub(crate) fn stored_data(&self, archive: &Archive) -> Result<Vec<u8>> {
327        self.stored_data_with_password(archive, None)
328    }
329
330    pub(crate) fn stored_data_with_password(
331        &self,
332        archive: &Archive,
333        password: Option<&[u8]>,
334    ) -> Result<Vec<u8>> {
335        if !self.is_stored() {
336            return Err(self.unsupported_compression());
337        }
338        if !self.is_encrypted() && self.pack_size != self.unp_size {
339            return Err(Error::InvalidHeader(
340                "RAR 1.5 stored file has mismatched packed and unpacked sizes",
341            ));
342        }
343        let mut data = self.packed_data_for_decode(archive, password)?;
344        if self.is_encrypted() {
345            data.truncate(
346                usize::try_from(self.unp_size)
347                    .map_err(|_| Error::InvalidHeader("RAR 1.5 unpacked size overflows usize"))?,
348            );
349        }
350        Ok(data)
351    }
352
353    pub(crate) fn unpacked_data(&self, archive: &Archive) -> Result<Vec<u8>> {
354        if self.is_stored() {
355            return self.stored_data(archive);
356        }
357        let mut session = DecoderSession::new(false);
358        session.decode_file_data(archive, self)
359    }
360
361    pub(crate) fn unpacked_data_with_rar29(
362        &self,
363        archive: &Archive,
364        decoder: &mut Unpack29,
365        solid: bool,
366    ) -> Result<Vec<u8>> {
367        if self.is_stored() {
368            return self.stored_data(archive);
369        }
370        if self.is_encrypted() {
371            return Err(self.unsupported_encryption());
372        }
373        if self.unp_ver < 29 {
374            return Err(self.unsupported_compression());
375        }
376        let packed = self.packed_data(archive)?;
377        let target = usize::try_from(self.unp_size)
378            .map_err(|_| Error::InvalidHeader("RAR 2.9 unpacked size overflows usize"))?;
379        if solid {
380            decoder.decode_member(&packed, target)
381        } else {
382            decoder.decode_non_solid_member(&packed, target)
383        }
384        .map_err(Into::into)
385    }
386
387    pub(crate) fn unpacked_data_with_unpack15(
388        &self,
389        archive: &Archive,
390        decoder: &mut Unpack15,
391        solid: bool,
392    ) -> Result<Vec<u8>> {
393        if self.is_stored() {
394            return self.stored_data(archive);
395        }
396        if self.is_encrypted() {
397            return Err(self.unsupported_encryption());
398        }
399        if self.unp_ver != 15 {
400            return Err(self.unsupported_compression());
401        }
402        decoder
403            .decode_member(
404                &self.packed_data(archive)?,
405                usize::try_from(self.unp_size)
406                    .map_err(|_| Error::InvalidHeader("RAR 1.5 unpacked size overflows usize"))?,
407                solid,
408            )
409            .map_err(Into::into)
410    }
411
412    pub(crate) fn unpacked_data_with_unpack20(
413        &self,
414        archive: &Archive,
415        decoder: &mut Unpack20,
416        password: Option<&[u8]>,
417    ) -> Result<Vec<u8>> {
418        if self.is_stored() {
419            return self.stored_data_with_password(archive, password);
420        }
421        if self.unp_ver != 20 && self.unp_ver != 26 {
422            return Err(self.unsupported_compression());
423        }
424        let mut packed = self.packed_reader_for_decode(archive, password)?;
425        let mut out = Vec::new();
426        decoder
427            .decode_member_from_reader(
428                &mut packed,
429                usize::try_from(self.unp_size)
430                    .map_err(|_| Error::InvalidHeader("RAR 2.0 unpacked size overflows usize"))?,
431                &mut out,
432            )
433            .map(|_| out)
434            .map_err(Into::into)
435    }
436
437    fn packed_data_for_decode(
438        &self,
439        archive: &Archive,
440        password: Option<&[u8]>,
441    ) -> Result<Vec<u8>> {
442        let mut reader = self.packed_reader_for_decode(archive, password)?;
443        let mut data = Vec::new();
444        reader.read_to_end(&mut data)?;
445        Ok(data)
446    }
447
448    pub(super) fn packed_reader_for_decode<'a>(
449        &self,
450        archive: &'a Archive,
451        password: Option<&[u8]>,
452    ) -> Result<Box<dyn Read + 'a>> {
453        let reader = archive.range_reader(self.packed_range.clone())?;
454        if !self.is_encrypted() {
455            return Ok(reader);
456        }
457        let Some(password) = password else {
458            return Err(Error::NeedPassword);
459        };
460        if (self.unp_ver == 20 || self.unp_ver == 26 || self.unp_ver >= 29)
461            && !self.packed_range.len().is_multiple_of(16)
462        {
463            return Err(Error::InvalidHeader(
464                "RAR encrypted payload is not block aligned",
465            ));
466        }
467        Ok(Box::new(DecryptingReader::new(
468            reader,
469            self.unp_ver,
470            password,
471            self.salt,
472        )?))
473    }
474
475    pub fn verify_crc32(&self, data: &[u8]) -> Result<()> {
476        let actual = crc32(data);
477        if actual == self.file_crc {
478            Ok(())
479        } else {
480            Err(Error::Crc32Mismatch {
481                expected: self.file_crc,
482                actual,
483            })
484        }
485    }
486
487    pub fn metadata(&self) -> ExtractedEntryMeta {
488        ExtractedEntryMeta {
489            name: self.name.clone(),
490            file_time: self.file_time,
491            attr: self.attr,
492            host_os: self.host_os,
493            is_directory: self.is_directory(),
494        }
495    }
496
497    pub fn write_to(
498        &self,
499        archive: &Archive,
500        password: Option<&[u8]>,
501        out: &mut impl Write,
502    ) -> Result<()> {
503        if self.is_directory() {
504            return Ok(());
505        }
506        let mut session = DecoderSession::new_with_password(false, password);
507        session.write_file_to(archive, self, out)
508    }
509
510    fn write_stored_to(
511        &self,
512        archive: &Archive,
513        password: Option<&[u8]>,
514        out: &mut impl Write,
515    ) -> Result<()> {
516        if !self.is_stored() {
517            return Err(self.unsupported_compression());
518        }
519        if !self.is_encrypted() && self.pack_size != self.unp_size {
520            return Err(Error::InvalidHeader(
521                "RAR 1.5 stored file has mismatched packed and unpacked sizes",
522            ));
523        }
524        let mut reader = self
525            .packed_reader_for_decode(archive, password)
526            .map_err(|error| self.map_encrypted_payload_error(password, error))?;
527        let expected_len = usize::try_from(self.unp_size)
528            .map_err(|_| Error::InvalidHeader("RAR 1.5 unpacked size overflows usize"))?;
529        let mut crc = Crc32::new();
530        let mut crc_writer = CrcWriter {
531            inner: out,
532            crc: &mut crc,
533        };
534        let copied = std::io::copy(
535            &mut reader.by_ref().take(expected_len as u64),
536            &mut crc_writer,
537        )?;
538        if copied != expected_len as u64 {
539            return Err(self.map_encrypted_payload_error(
540                password,
541                Error::InvalidHeader("RAR 1.5 stored file ended before unpacked size"),
542            ));
543        }
544        let actual = crc.finish();
545        if actual == self.file_crc {
546            Ok(())
547        } else {
548            Err(self.map_encrypted_payload_error(
549                password,
550                Error::Crc32Mismatch {
551                    expected: self.file_crc,
552                    actual,
553                },
554            ))
555        }
556    }
557
558    fn map_encrypted_payload_error(&self, password: Option<&[u8]>, error: Error) -> Error {
559        if !self.is_encrypted() || password.is_none() {
560            return error;
561        }
562        match error {
563            Error::NeedPassword => Error::NeedPassword,
564            Error::UnsupportedSignature
565            | Error::UnsupportedVersion(_)
566            | Error::UnsupportedFeature { .. }
567            | Error::UnsupportedFamilyFeature { .. }
568            | Error::UnsupportedCompression { .. }
569            | Error::UnsupportedEncryption { .. }
570            | Error::TooShort
571            | Error::Io(_)
572            | Error::AtArchiveOffset { .. }
573            | Error::AtEntry { .. } => error,
574            Error::InvalidHeader(_)
575            | Error::Codec(_)
576            | Error::Rar3Recovery(_)
577            | Error::Rar5Recovery(_)
578            | Error::Rar20Crypto(_)
579            | Error::Rar30Crypto(_)
580            | Error::Rar50Crypto(_)
581            | Error::CrcMismatch { .. }
582            | Error::Crc32Mismatch { .. }
583            | Error::HashMismatch { .. }
584            | Error::WrongPasswordOrCorruptData => Error::WrongPasswordOrCorruptData,
585        }
586    }
587
588    fn entry_error(&self, operation: &'static str, error: Error) -> Error {
589        if matches!(
590            error,
591            Error::NeedPassword | Error::WrongPasswordOrCorruptData
592        ) {
593            return error;
594        }
595        error.at_entry(self.name.clone(), operation)
596    }
597
598    fn crc_result(&self, actual: u32, password: Option<&[u8]>) -> Result<()> {
599        if actual == self.file_crc {
600            Ok(())
601        } else {
602            Err(self.map_encrypted_payload_error(
603                password,
604                Error::Crc32Mismatch {
605                    expected: self.file_crc,
606                    actual,
607                },
608            ))
609        }
610    }
611
612    fn write_rar29_to(
613        &self,
614        archive: &Archive,
615        decoder: &mut Unpack29,
616        out: &mut impl Write,
617    ) -> Result<()> {
618        if self.is_stored() {
619            return self.write_stored_to(archive, None, out);
620        }
621        if self.is_encrypted() {
622            return Err(self.unsupported_encryption());
623        }
624        if self.unp_ver < 29 {
625            return Err(self.unsupported_compression());
626        }
627
628        let mut packed = archive.range_reader(self.packed_range.clone())?;
629        let mut crc = Crc32::new();
630        let mut crc_writer = CrcWriter {
631            inner: out,
632            crc: &mut crc,
633        };
634        decoder
635            .decode_member_from_reader(
636                &mut packed,
637                usize::try_from(self.unp_size)
638                    .map_err(|_| Error::InvalidHeader("RAR 1.5 unpacked size overflows usize"))?,
639                &mut crc_writer,
640            )
641            .map_err(Error::from)?;
642        let actual = crc.finish();
643        if actual == self.file_crc {
644            Ok(())
645        } else {
646            Err(Error::Crc32Mismatch {
647                expected: self.file_crc,
648                actual,
649            })
650        }
651    }
652
653    fn write_unpack15_to(
654        &self,
655        archive: &Archive,
656        decoder: &mut Unpack15,
657        solid: bool,
658        password: Option<&[u8]>,
659        out: &mut impl Write,
660    ) -> Result<()> {
661        if self.is_stored() {
662            return self.write_stored_to(archive, password, out);
663        }
664        if self.unp_ver != 15 {
665            return Err(self.unsupported_compression());
666        }
667
668        let mut input = self
669            .packed_reader_for_decode(archive, password)
670            .map_err(|error| self.map_encrypted_payload_error(password, error))?;
671        self.write_unpack15_decoded(decoder, solid, &mut input, out, password)
672            .map_err(|error| self.map_encrypted_payload_error(password, error))
673    }
674
675    fn write_unpack15_decoded(
676        &self,
677        decoder: &mut Unpack15,
678        solid: bool,
679        input: &mut impl Read,
680        out: &mut impl Write,
681        password: Option<&[u8]>,
682    ) -> Result<()> {
683        let mut crc = Crc32::new();
684        let mut crc_writer = CrcWriter {
685            inner: out,
686            crc: &mut crc,
687        };
688        decoder
689            .decode_member_from_reader(
690                input,
691                usize::try_from(self.unp_size)
692                    .map_err(|_| Error::InvalidHeader("RAR 1.5 unpacked size overflows usize"))?,
693                solid,
694                &mut crc_writer,
695            )
696            .map_err(Error::from)?;
697        let actual = crc.finish();
698        self.crc_result(actual, password)
699    }
700
701    fn write_unpack20_to(
702        &self,
703        archive: &Archive,
704        decoder: &mut Unpack20,
705        password: Option<&[u8]>,
706        out: &mut impl Write,
707    ) -> Result<()> {
708        if self.is_stored() {
709            return self.write_stored_to(archive, password, out);
710        }
711        if self.unp_ver != 20 && self.unp_ver != 26 {
712            return Err(self.unsupported_compression());
713        }
714
715        let mut crc = Crc32::new();
716        let mut crc_writer = CrcWriter {
717            inner: out,
718            crc: &mut crc,
719        };
720        let target = usize::try_from(self.unp_size)
721            .map_err(|_| Error::InvalidHeader("RAR 2.0 unpacked size overflows usize"))?;
722        let mut packed = self
723            .packed_reader_for_decode(archive, password)
724            .map_err(|error| self.map_encrypted_payload_error(password, error))?;
725        decoder
726            .decode_member_from_reader(&mut packed, target, &mut crc_writer)
727            .map_err(Error::from)
728            .map_err(|error| self.map_encrypted_payload_error(password, error))?;
729        let actual = crc.finish();
730        self.crc_result(actual, password)
731    }
732
733    fn unsupported_compression(&self) -> Error {
734        Error::UnsupportedCompression {
735            family: "RAR 1.5-4.x",
736            unpack_version: self.unp_ver,
737            method: self.method,
738        }
739    }
740
741    fn unsupported_encryption(&self) -> Error {
742        Error::UnsupportedEncryption {
743            family: "RAR 1.5-4.x",
744            unpack_version: self.unp_ver,
745        }
746    }
747}
748
749impl NewSubHeader {
750    pub fn name_bytes(&self) -> &[u8] {
751        self.file.name_bytes()
752    }
753
754    /// Returns the service-block name with invalid UTF-8 replaced for display only.
755    ///
756    /// Use [`Self::name_bytes`] when exact archive bytes matter.
757    pub fn name_lossy(&self) -> String {
758        self.file.name_lossy()
759    }
760}
761
762impl CommentHeader {
763    fn packed_data(&self, archive: &Archive) -> Result<Vec<u8>> {
764        archive.read_range(self.packed_range.clone())
765    }
766
767    fn unpacked_data(&self, archive: &Archive) -> Result<Vec<u8>> {
768        let target = usize::from(self.unp_size);
769        let data = if self.method == 0x30 {
770            let data = self.packed_data(archive)?;
771            if data.len() != target {
772                return Err(Error::InvalidHeader(
773                    "RAR 1.5 stored comment has mismatched packed and unpacked sizes",
774                ));
775            }
776            data
777        } else if self.unp_ver == 15 {
778            Unpack15::default().decode_member(&self.packed_data(archive)?, target, false)?
779        } else {
780            return Err(Error::UnsupportedCompression {
781                family: "RAR 1.5 comment",
782                unpack_version: self.unp_ver,
783                method: self.method,
784            });
785        };
786        let actual = (crc32(&data) & 0xffff) as u16;
787        if actual == self.comment_crc {
788            Ok(data)
789        } else {
790            Err(Error::CrcMismatch {
791                expected: self.comment_crc,
792                actual,
793            })
794        }
795    }
796}
797
798impl Archive {
799    pub fn parse(input: &[u8]) -> Result<Self> {
800        Self::parse_with_options(input, crate::ArchiveReadOptions::default())
801    }
802
803    pub fn parse_owned(input: Vec<u8>) -> Result<Self> {
804        Self::parse_owned_with_options(input, crate::ArchiveReadOptions::default())
805    }
806
807    pub fn parse_with_options(
808        input: &[u8],
809        options: crate::ArchiveReadOptions<'_>,
810    ) -> Result<Self> {
811        let data: Arc<[u8]> = Arc::from(input.to_vec().into_boxed_slice());
812        Self::parse_shared(data, options.password)
813    }
814
815    pub fn parse_owned_with_options(
816        input: Vec<u8>,
817        options: crate::ArchiveReadOptions<'_>,
818    ) -> Result<Self> {
819        Self::parse_shared(Arc::from(input.into_boxed_slice()), options.password)
820    }
821
822    pub fn parse_with_password(input: &[u8], password: Option<&[u8]>) -> Result<Self> {
823        Self::parse_with_options(input, crate::ArchiveReadOptions { password })
824    }
825
826    pub fn parse_owned_with_password(input: Vec<u8>, password: Option<&[u8]>) -> Result<Self> {
827        Self::parse_owned_with_options(input, crate::ArchiveReadOptions { password })
828    }
829
830    pub fn parse_path(path: impl AsRef<Path>) -> Result<Self> {
831        Self::parse_path_with_options(path, crate::ArchiveReadOptions::default())
832    }
833
834    pub fn parse_path_with_options(
835        path: impl AsRef<Path>,
836        options: crate::ArchiveReadOptions<'_>,
837    ) -> Result<Self> {
838        Self::parse_path_with_password(path, options.password)
839    }
840
841    pub fn parse_path_with_password(
842        path: impl AsRef<Path>,
843        password: Option<&[u8]>,
844    ) -> Result<Self> {
845        let path = Arc::new(path.as_ref().to_path_buf());
846        let mut file = File::open(path.as_ref())?;
847        let len = file.metadata()?.len();
848        let scan_len = len.min(128 * 1024) as usize;
849        let mut scan = vec![0; scan_len];
850        file.read_exact(&mut scan)?;
851        let sig = find_archive_start(&scan, 128 * 1024).ok_or(Error::UnsupportedSignature)?;
852        if sig.family != ArchiveFamily::Rar15To40 {
853            return Err(Error::UnsupportedSignature);
854        }
855        Self::parse_seekable(file, len, sig.offset, ArchiveSource::File(path), password)
856    }
857
858    pub fn parse_path_with_signature(
859        path: impl AsRef<Path>,
860        signature: ArchiveSignature,
861        options: crate::ArchiveReadOptions<'_>,
862    ) -> Result<Self> {
863        Self::parse_path_with_signature_and_password(path, signature, options.password)
864    }
865
866    pub fn parse_path_with_signature_and_password(
867        path: impl AsRef<Path>,
868        signature: ArchiveSignature,
869        password: Option<&[u8]>,
870    ) -> Result<Self> {
871        if signature.family != ArchiveFamily::Rar15To40 {
872            return Err(Error::UnsupportedSignature);
873        }
874        let path = Arc::new(path.as_ref().to_path_buf());
875        let file = File::open(path.as_ref())?;
876        let len = file.metadata()?.len();
877        Self::parse_seekable(
878            file,
879            len,
880            signature.offset,
881            ArchiveSource::File(path),
882            password,
883        )
884    }
885
886    fn parse_shared(input: Arc<[u8]>, password: Option<&[u8]>) -> Result<Self> {
887        let sig = find_archive_start(&input, 128 * 1024).ok_or(Error::UnsupportedSignature)?;
888        if sig.family != ArchiveFamily::Rar15To40 {
889            return Err(Error::UnsupportedSignature);
890        }
891
892        let archive = &input[sig.offset..];
893        if !archive.starts_with(RAR15_SIGNATURE) {
894            return Err(Error::UnsupportedSignature);
895        }
896
897        let marker = parse_block_header(archive, 0)?;
898        if marker.head_type != MARK_HEAD || marker.head_size != RAR15_SIGNATURE.len() as u16 {
899            return Err(Error::InvalidHeader("RAR 1.5 marker block is invalid"));
900        }
901
902        let main_block = parse_block_header(archive, marker.head_size as usize)?;
903        if main_block.head_type != MAIN_HEAD {
904            return Err(Error::InvalidHeader("RAR 1.5 main header is missing"));
905        }
906        let main = parse_main_header(archive, &main_block)?;
907        let mut pos = main_block.offset + main_block.head_size as usize;
908        let mut blocks = Vec::new();
909        let mut encrypted_header_ciphers = EncryptedHeaderCipherCache::default();
910
911        while pos < archive.len() {
912            if archive.len() - pos < 7 {
913                break;
914            }
915            let (block, header, total) = if main.has_encrypted_headers() {
916                let password = password.ok_or(Error::NeedPassword)?;
917                let encrypted = decrypt_encrypted_header_at(
918                    archive,
919                    pos,
920                    password,
921                    &mut encrypted_header_ciphers,
922                )?;
923                (encrypted.block, encrypted.header, encrypted.total_size)
924            } else {
925                let block = parse_block_header(archive, pos)?;
926                let total = block_total_size(&block)?;
927                let header = archive[pos..pos + block.head_size as usize].to_vec();
928                (block, header, total)
929            };
930            match block.head_type {
931                FILE_HEAD => {
932                    let mut file = parse_file_like_header(&header, relative_block(&block), 0)?;
933                    let total = file_block_total_size(&block, total, file.pack_size)?;
934                    let next = checked_block_next(&block, total, archive.len())?;
935                    file.block.offset = block.offset;
936                    file.packed_range =
937                        packed_range(sig.offset, block.offset, total, file.pack_size)?;
938                    blocks.push(Block::File(file));
939                    pos = next;
940                }
941                NEWSUB_HEAD => {
942                    let mut file = parse_file_like_header(&header, relative_block(&block), 0)?;
943                    let total = file_block_total_size(&block, total, file.pack_size)?;
944                    let next = checked_block_next(&block, total, archive.len())?;
945                    file.block.offset = block.offset;
946                    file.packed_range =
947                        packed_range(sig.offset, block.offset, total, file.pack_size)?;
948                    let kind = classify_new_sub(&file.name);
949                    blocks.push(Block::NewSub(NewSubHeader { file, kind }));
950                    pos = next;
951                }
952                COMM_HEAD => {
953                    let next = checked_block_next(&block, total, archive.len())?;
954                    let mut comment = parse_comment_header(&header, relative_block(&block))?;
955                    comment.block.offset = block.offset;
956                    comment.packed_range =
957                        sig.offset + block.offset + 13..sig.offset + block.offset + total;
958                    blocks.push(Block::Comment(comment));
959                    pos = next;
960                }
961                PROTECT_HEAD => {
962                    let next = checked_block_next(&block, total, archive.len())?;
963                    let protect = parse_protect_header(&header, &block, sig.offset, total)?;
964                    blocks.push(Block::Protect(protect));
965                    pos = next;
966                }
967                ENDARC_HEAD => {
968                    let _next = checked_block_next(&block, total, archive.len())?;
969                    blocks.push(Block::End(block));
970                    break;
971                }
972                _ => {
973                    let next = checked_block_next(&block, total, archive.len())?;
974                    blocks.push(Block::Unknown(block));
975                    pos = next;
976                }
977            }
978        }
979
980        Ok(Self {
981            sfx_offset: sig.offset,
982            main,
983            blocks,
984            source: ArchiveSource::Memory(input),
985        })
986    }
987
988    fn parse_seekable(
989        mut file: File,
990        file_len: u64,
991        sfx_offset: usize,
992        source: ArchiveSource,
993        password: Option<&[u8]>,
994    ) -> Result<Self> {
995        let marker = read_block_header_at(&mut file, file_len, sfx_offset, 0)?;
996        if marker.head_type != MARK_HEAD || marker.head_size != RAR15_SIGNATURE.len() as u16 {
997            return Err(Error::InvalidHeader("RAR 1.5 marker block is invalid"));
998        }
999
1000        let main_block =
1001            read_block_header_at(&mut file, file_len, sfx_offset, marker.head_size as usize)?;
1002        if main_block.head_type != MAIN_HEAD {
1003            return Err(Error::InvalidHeader("RAR 1.5 main header is missing"));
1004        }
1005        let main_header = read_exact_at(
1006            &mut file,
1007            sfx_offset + main_block.offset,
1008            main_block.head_size as usize,
1009        )?;
1010        let main = parse_main_header(&main_header, &relative_block(&main_block))?;
1011        let mut pos = main_block.offset + main_block.head_size as usize;
1012        let mut blocks = Vec::new();
1013        let mut encrypted_header_ciphers = EncryptedHeaderCipherCache::default();
1014
1015        while (sfx_offset + pos) as u64 + 7 <= file_len {
1016            let (block, header, total) = if main.has_encrypted_headers() {
1017                let password = password.ok_or(Error::NeedPassword)?;
1018                let encrypted = read_encrypted_header_at(
1019                    &mut file,
1020                    file_len,
1021                    sfx_offset,
1022                    pos,
1023                    password,
1024                    &mut encrypted_header_ciphers,
1025                )?;
1026                (encrypted.block, encrypted.header, encrypted.total_size)
1027            } else {
1028                let block = read_block_header_at(&mut file, file_len, sfx_offset, pos)?;
1029                let total = block_total_size(&block)?;
1030                let header = read_exact_at(&mut file, sfx_offset + pos, block.head_size as usize)?;
1031                (block, header, total)
1032            };
1033            match block.head_type {
1034                FILE_HEAD => {
1035                    let mut file_header =
1036                        parse_file_like_header(&header, relative_block(&block), 0)?;
1037                    let total = file_block_total_size(&block, total, file_header.pack_size)?;
1038                    let next = checked_file_block_next(sfx_offset, &block, total, file_len)?;
1039                    file_header.block.offset = block.offset;
1040                    file_header.packed_range =
1041                        packed_range(sfx_offset, block.offset, total, file_header.pack_size)?;
1042                    blocks.push(Block::File(file_header));
1043                    pos = next;
1044                }
1045                NEWSUB_HEAD => {
1046                    let mut file_header =
1047                        parse_file_like_header(&header, relative_block(&block), 0)?;
1048                    let total = file_block_total_size(&block, total, file_header.pack_size)?;
1049                    let next = checked_file_block_next(sfx_offset, &block, total, file_len)?;
1050                    file_header.block.offset = block.offset;
1051                    file_header.packed_range =
1052                        packed_range(sfx_offset, block.offset, total, file_header.pack_size)?;
1053                    let kind = classify_new_sub(&file_header.name);
1054                    blocks.push(Block::NewSub(NewSubHeader {
1055                        file: file_header,
1056                        kind,
1057                    }));
1058                    pos = next;
1059                }
1060                COMM_HEAD => {
1061                    let next = checked_file_block_next(sfx_offset, &block, total, file_len)?;
1062                    let mut comment = parse_comment_header(&header, relative_block(&block))?;
1063                    comment.block.offset = block.offset;
1064                    comment.packed_range =
1065                        sfx_offset + block.offset + 13..sfx_offset + block.offset + total;
1066                    blocks.push(Block::Comment(comment));
1067                    pos = next;
1068                }
1069                PROTECT_HEAD => {
1070                    let next = checked_file_block_next(sfx_offset, &block, total, file_len)?;
1071                    let protect = parse_protect_header(&header, &block, sfx_offset, total)?;
1072                    blocks.push(Block::Protect(protect));
1073                    pos = next;
1074                }
1075                ENDARC_HEAD => {
1076                    let _next = checked_file_block_next(sfx_offset, &block, total, file_len)?;
1077                    blocks.push(Block::End(block));
1078                    break;
1079                }
1080                _ => {
1081                    let next = checked_file_block_next(sfx_offset, &block, total, file_len)?;
1082                    blocks.push(Block::Unknown(block));
1083                    pos = next;
1084                }
1085            }
1086        }
1087
1088        Ok(Self {
1089            sfx_offset,
1090            main,
1091            blocks,
1092            source,
1093        })
1094    }
1095
1096    fn read_range(&self, range: Range<usize>) -> Result<Vec<u8>> {
1097        self.source.read_range(range)
1098    }
1099
1100    fn copy_range_to(&self, range: Range<usize>, out: &mut impl Write) -> Result<()> {
1101        self.source.copy_range_to(range, out)
1102    }
1103
1104    fn range_reader(&self, range: Range<usize>) -> Result<Box<dyn Read + '_>> {
1105        self.source.range_reader(range)
1106    }
1107
1108    pub fn files(&self) -> impl Iterator<Item = &FileHeader> {
1109        self.blocks.iter().filter_map(|block| match block {
1110            Block::File(file) => Some(file),
1111            _ => None,
1112        })
1113    }
1114
1115    pub fn new_subs(&self) -> impl Iterator<Item = &NewSubHeader> {
1116        self.blocks.iter().filter_map(|block| match block {
1117            Block::NewSub(sub) => Some(sub),
1118            _ => None,
1119        })
1120    }
1121
1122    pub fn protect_records(&self) -> impl Iterator<Item = &ProtectHeader> {
1123        self.blocks.iter().filter_map(|block| match block {
1124            Block::Protect(protect) => Some(protect),
1125            _ => None,
1126        })
1127    }
1128
1129    fn source_bytes(&self) -> Result<Vec<u8>> {
1130        self.source.bytes()
1131    }
1132
1133    pub fn repair_protect_head(&self) -> Result<Vec<u8>> {
1134        if let Some(recovery) = self
1135            .new_subs()
1136            .find(|sub| sub.kind == NewSubKind::RecoveryRecord)
1137        {
1138            return repair_newsub_recovery_bytes(
1139                &self.source_bytes()?,
1140                self.sfx_offset,
1141                self,
1142                recovery,
1143            );
1144        }
1145        let protect = self.protect_records().next().ok_or(Error::InvalidHeader(
1146            "RAR 2.x archive does not contain a PROTECT_HEAD recovery record",
1147        ))?;
1148        repair_protect_head_bytes(&self.source_bytes()?, self.sfx_offset, protect)
1149    }
1150
1151    /// Streams extracted entries to caller-provided writers.
1152    pub fn extract_to<F>(&self, options: crate::ArchiveReadOptions<'_>, mut open: F) -> Result<()>
1153    where
1154        F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
1155    {
1156        let password = options.password;
1157        let mut session = DecoderSession::new_with_password(self.main.is_solid(), password);
1158        for file in self.files() {
1159            if file.is_split_before() || file.is_split_after() {
1160                return Err(Error::InvalidHeader(
1161                    "RAR 1.5 split entry requires multivolume extraction",
1162                ));
1163            }
1164            let meta = file.metadata();
1165            if meta.is_directory {
1166                let _ = open(&meta)?;
1167                continue;
1168            }
1169            let mut writer = open(&meta)?;
1170            if file.is_stored() {
1171                file.write_stored_to(self, password, &mut writer)
1172                    .map_err(|error| file.entry_error("extracting", error))?;
1173            } else {
1174                session
1175                    .write_file_to(self, file, &mut writer)
1176                    .map_err(|error| file.entry_error("extracting", error))?;
1177            }
1178        }
1179        Ok(())
1180    }
1181
1182    pub fn archive_comment(&self) -> Result<Option<Vec<u8>>> {
1183        if let Some(comment) = self.blocks.iter().find_map(|block| match block {
1184            Block::Comment(comment) => Some(comment),
1185            _ => None,
1186        }) {
1187            return comment.unpacked_data(self).map(Some);
1188        }
1189
1190        let Some(comment) = self
1191            .new_subs()
1192            .find(|sub| sub.kind == NewSubKind::ArchiveComment)
1193        else {
1194            return Ok(None);
1195        };
1196        let data = comment.file.unpacked_data(self)?;
1197        comment.file.verify_crc32(&data)?;
1198        Ok(Some(data))
1199    }
1200}
1201
1202fn classify_new_sub(name: &[u8]) -> NewSubKind {
1203    match name {
1204        b"CMT" => NewSubKind::ArchiveComment,
1205        b"RR" => NewSubKind::RecoveryRecord,
1206        _ => NewSubKind::Unknown(name.to_vec()),
1207    }
1208}
1209
1210fn parse_main_header(input: &[u8], block: &BlockHeader) -> Result<MainHeader> {
1211    if block.head_size < 13 {
1212        return Err(Error::InvalidHeader("RAR 1.5 main header is too short"));
1213    }
1214    let start = block.offset;
1215    let head_end = start + block.head_size as usize;
1216    if head_end > input.len() {
1217        return Err(Error::TooShort);
1218    }
1219
1220    let encrypt_version = if block.flags & MHD_ENCRYPTVER != 0 {
1221        Some(*input.get(start + 13).ok_or(Error::TooShort)?)
1222    } else {
1223        None
1224    };
1225
1226    Ok(MainHeader {
1227        head_crc: block.head_crc,
1228        flags: block.flags,
1229        head_size: block.head_size,
1230        reserved1: read_u16(input, start + 7)?,
1231        reserved2: read_u32(input, start + 9)?,
1232        encrypt_version,
1233    })
1234}
1235
1236fn parse_comment_header(input: &[u8], block: BlockHeader) -> Result<CommentHeader> {
1237    if block.head_size < 13 {
1238        return Err(Error::InvalidHeader("RAR 1.5 comment header is too short"));
1239    }
1240    let start = block.offset;
1241    Ok(CommentHeader {
1242        block,
1243        unp_size: read_u16(input, start + 7)?,
1244        unp_ver: *input.get(start + 9).ok_or(Error::TooShort)?,
1245        method: *input.get(start + 10).ok_or(Error::TooShort)?,
1246        comment_crc: read_u16(input, start + 11)?,
1247        packed_range: 0..0,
1248    })
1249}
1250
1251fn parse_protect_header(
1252    input: &[u8],
1253    block: &BlockHeader,
1254    archive_offset: usize,
1255    total_size: usize,
1256) -> Result<ProtectHeader> {
1257    if block.head_size != 26 {
1258        return Err(Error::InvalidHeader(
1259            "RAR 2.x recovery header size is invalid",
1260        ));
1261    }
1262    let add_size = block.add_size.ok_or(Error::InvalidHeader(
1263        "RAR 2.x recovery header is missing data size",
1264    ))?;
1265    let rec_sectors = read_u16(input, 12)?;
1266    let total_blocks = read_u32(input, 14)?;
1267    let expected_add_size = u64::from(total_blocks)
1268        .checked_mul(2)
1269        .and_then(|size| size.checked_add(u64::from(rec_sectors) * 512))
1270        .ok_or(Error::InvalidHeader("RAR 2.x recovery data size overflows"))?;
1271    if add_size != expected_add_size {
1272        return Err(Error::InvalidHeader(
1273            "RAR 2.x recovery data size does not match header",
1274        ));
1275    }
1276    let mark: [u8; 8] = input
1277        .get(18..26)
1278        .ok_or(Error::TooShort)?
1279        .try_into()
1280        .expect("RAR protect mark size");
1281    let data_start = archive_offset
1282        .checked_add(block.offset)
1283        .and_then(|offset| offset.checked_add(block.head_size as usize))
1284        .ok_or(Error::InvalidHeader(
1285            "RAR 2.x recovery data range overflows",
1286        ))?;
1287    let data_end = archive_offset
1288        .checked_add(block.offset)
1289        .and_then(|offset| offset.checked_add(total_size))
1290        .ok_or(Error::InvalidHeader(
1291            "RAR 2.x recovery data range overflows",
1292        ))?;
1293    Ok(ProtectHeader {
1294        block: block.clone(),
1295        version: *input.get(11).ok_or(Error::TooShort)?,
1296        rec_sectors,
1297        total_blocks,
1298        mark,
1299        data_range: data_start..data_end,
1300    })
1301}
1302
1303fn repair_protect_head_bytes(
1304    source: &[u8],
1305    sfx_offset: usize,
1306    protect: &ProtectHeader,
1307) -> Result<Vec<u8>> {
1308    if protect.rec_sectors == 0 {
1309        return Err(Error::InvalidHeader(
1310            "RAR 2.x recovery record has no parity sectors",
1311        ));
1312    }
1313    if protect.mark != *b"Protect!" {
1314        return Err(Error::InvalidHeader("RAR 2.x recovery mark is invalid"));
1315    }
1316    let protected_start = sfx_offset;
1317    let protected_len = usize::try_from(protect.total_blocks)
1318        .ok()
1319        .and_then(|blocks| blocks.checked_mul(512))
1320        .ok_or(Error::InvalidHeader(
1321            "RAR 2.x protected sector size overflows",
1322        ))?;
1323    let protected_end = protected_start
1324        .checked_add(protected_len)
1325        .ok_or(Error::InvalidHeader(
1326            "RAR 2.x protected sector range overflows",
1327        ))?;
1328    if protected_end > source.len() {
1329        return Err(Error::InvalidHeader(
1330            "RAR 2.x protected sector range is invalid",
1331        ));
1332    }
1333    let recovery_data = source
1334        .get(protect.data_range.clone())
1335        .ok_or(Error::TooShort)?;
1336    let declared_blocks = usize::try_from(protect.total_blocks).map_err(|_| {
1337        Error::InvalidHeader("RAR 2.x recovery protected sector count overflows usize")
1338    })?;
1339    let tag_len = declared_blocks
1340        .checked_mul(2)
1341        .ok_or(Error::InvalidHeader("RAR 2.x recovery tag size overflows"))?;
1342    let parity_len =
1343        usize::from(protect.rec_sectors)
1344            .checked_mul(512)
1345            .ok_or(Error::InvalidHeader(
1346                "RAR 2.x recovery parity size overflows",
1347            ))?;
1348    if recovery_data.len() != tag_len + parity_len {
1349        return Err(Error::InvalidHeader(
1350            "RAR 2.x recovery data size is invalid",
1351        ));
1352    }
1353    let tags = &recovery_data[..tag_len];
1354    let parity = &recovery_data[tag_len..];
1355    // RAR 2.50 records may declare a final sector that starts before
1356    // PROTECT_HEAD but overlaps the recovery block. Only complete sectors
1357    // before the recovery block are safely repairable.
1358    let repairable_blocks = declared_blocks.min(protect.block.offset / 512);
1359
1360    let mut damaged = Vec::new();
1361    for index in 0..repairable_blocks {
1362        let sector_start = protected_start + index * 512;
1363        let sector = &source[sector_start..sector_start + 512];
1364        let actual = (!crc32(sector) & 0xffff) as u16;
1365        let expected = read_u16(tags, index * 2)?;
1366        if actual != expected {
1367            damaged.push(index);
1368        }
1369    }
1370    if damaged.is_empty() {
1371        return Ok(source.to_vec());
1372    }
1373    if damaged.len() > usize::from(protect.rec_sectors) {
1374        return Err(Error::InvalidHeader(
1375            "RAR 2.x recovery damage exceeds parity sector count",
1376        ));
1377    }
1378
1379    let mut used_slots = vec![false; usize::from(protect.rec_sectors)];
1380    for &index in &damaged {
1381        let slot = index % usize::from(protect.rec_sectors);
1382        if used_slots[slot] {
1383            return Err(Error::InvalidHeader(
1384                "RAR 2.x recovery cannot repair multiple sectors in the same parity group",
1385            ));
1386        }
1387        used_slots[slot] = true;
1388    }
1389
1390    let mut repaired = source.to_vec();
1391    for &missing_index in &damaged {
1392        let slot = missing_index % usize::from(protect.rec_sectors);
1393        let mut sector = parity[slot * 512..slot * 512 + 512].to_vec();
1394        for index in (slot..repairable_blocks).step_by(usize::from(protect.rec_sectors)) {
1395            if index == missing_index {
1396                continue;
1397            }
1398            let sector_start = protected_start + index * 512;
1399            for (out, byte) in sector
1400                .iter_mut()
1401                .zip(&repaired[sector_start..sector_start + 512])
1402            {
1403                *out ^= *byte;
1404            }
1405        }
1406        let sector_start = protected_start + missing_index * 512;
1407        repaired[sector_start..sector_start + 512].copy_from_slice(&sector);
1408        let actual = (!crc32(&sector) & 0xffff) as u16;
1409        let expected = u16::from_le_bytes(
1410            tags[missing_index * 2..missing_index * 2 + 2]
1411                .try_into()
1412                .unwrap(),
1413        );
1414        if actual != expected {
1415            return Err(Error::CrcMismatch { expected, actual });
1416        }
1417    }
1418    Ok(repaired)
1419}
1420
1421fn repair_newsub_recovery_bytes(
1422    source: &[u8],
1423    sfx_offset: usize,
1424    archive: &Archive,
1425    recovery: &NewSubHeader,
1426) -> Result<Vec<u8>> {
1427    let recovery_data = newsub_recovery_data(archive, recovery)?;
1428    let expected_unpacked = usize::try_from(recovery.file.unp_size)
1429        .map_err(|_| Error::InvalidHeader("RAR 3.x recovery unpacked size overflows usize"))?;
1430    if recovery_data.len() != expected_unpacked {
1431        return Err(Error::InvalidHeader(
1432            "RAR 3.x recovery data size does not match unpacked size",
1433        ));
1434    }
1435    let protected_start = sfx_offset;
1436    let protected_end =
1437        sfx_offset
1438            .checked_add(recovery.file.block.offset)
1439            .ok_or(Error::InvalidHeader(
1440                "RAR 3.x recovery protected range overflows",
1441            ))?;
1442    if protected_end > source.len() || protected_start > protected_end {
1443        return Err(Error::InvalidHeader(
1444            "RAR 3.x recovery protected range is invalid",
1445        ));
1446    }
1447    let protected_len = protected_end - protected_start;
1448    let protected_sectors = protected_len.div_ceil(512);
1449    if protected_sectors == 0 {
1450        return Err(Error::InvalidHeader(
1451            "RAR 3.x recovery record has no protected sectors",
1452        ));
1453    }
1454    let tag_len = protected_sectors
1455        .checked_mul(2)
1456        .ok_or(Error::InvalidHeader("RAR 3.x recovery tag size overflows"))?;
1457    if recovery_data.len() <= tag_len || (recovery_data.len() - tag_len) % 512 != 0 {
1458        return Err(Error::InvalidHeader(
1459            "RAR 3.x recovery data size is invalid",
1460        ));
1461    }
1462    let parity_sectors = (recovery_data.len() - tag_len) / 512;
1463    if parity_sectors == 0 {
1464        return Err(Error::InvalidHeader(
1465            "RAR 3.x recovery record has no parity sectors",
1466        ));
1467    }
1468    let tags = &recovery_data[..tag_len];
1469    let parity = &recovery_data[tag_len..];
1470
1471    let mut damaged = Vec::new();
1472    for index in 0..protected_sectors {
1473        let sector = protected_sector(source, protected_start, protected_len, index)?;
1474        let actual = (!crc32(&sector) & 0xffff) as u16;
1475        let expected = read_u16(tags, index * 2)?;
1476        if actual != expected {
1477            damaged.push(index);
1478        }
1479    }
1480    if damaged.is_empty() {
1481        return Ok(source.to_vec());
1482    }
1483    if damaged.len() > parity_sectors {
1484        return Err(Error::InvalidHeader(
1485            "RAR 3.x recovery damage exceeds parity sector count",
1486        ));
1487    }
1488
1489    let mut used_slots = vec![false; parity_sectors];
1490    for &index in &damaged {
1491        let slot = index % parity_sectors;
1492        if used_slots[slot] {
1493            return Err(Error::InvalidHeader(
1494                "RAR 3.x recovery cannot repair multiple sectors in the same parity group",
1495            ));
1496        }
1497        used_slots[slot] = true;
1498    }
1499
1500    let mut repaired = source.to_vec();
1501    for &missing_index in &damaged {
1502        let slot = missing_index % parity_sectors;
1503        let mut sector = parity[slot * 512..slot * 512 + 512].to_vec();
1504        for index in (slot..protected_sectors).step_by(parity_sectors) {
1505            if index == missing_index {
1506                continue;
1507            }
1508            let other = protected_sector(&repaired, protected_start, protected_len, index)?;
1509            for (out, byte) in sector.iter_mut().zip(other) {
1510                *out ^= byte;
1511            }
1512        }
1513        let actual = (!crc32(&sector) & 0xffff) as u16;
1514        let expected = u16::from_le_bytes(
1515            tags[missing_index * 2..missing_index * 2 + 2]
1516                .try_into()
1517                .unwrap(),
1518        );
1519        if actual != expected {
1520            return Err(Error::CrcMismatch { expected, actual });
1521        }
1522        let sector_start = protected_start + missing_index * 512;
1523        let write_len = 512.min(protected_end - sector_start);
1524        repaired[sector_start..sector_start + write_len].copy_from_slice(&sector[..write_len]);
1525    }
1526
1527    Ok(repaired)
1528}
1529
1530fn newsub_recovery_data(archive: &Archive, recovery: &NewSubHeader) -> Result<Vec<u8>> {
1531    if recovery.file.is_encrypted() {
1532        return Err(Error::UnsupportedFeature {
1533            version: ArchiveVersion::Rar30,
1534            feature: "encrypted RAR 3.x NEWSUB recovery record",
1535        });
1536    }
1537    if recovery.file.method == 0x30 {
1538        if recovery.file.pack_size != recovery.file.unp_size {
1539            return Err(Error::InvalidHeader(
1540                "RAR 3.x recovery record packed size does not match unpacked size",
1541            ));
1542        }
1543        return recovery.file.stored_data(archive);
1544    }
1545    let mut session = DecoderSession::new(false);
1546    session.decode_file_data(archive, &recovery.file)
1547}
1548
1549fn protected_sector(
1550    source: &[u8],
1551    protected_start: usize,
1552    protected_len: usize,
1553    index: usize,
1554) -> Result<[u8; 512]> {
1555    let sector_offset = index.checked_mul(512).ok_or(Error::InvalidHeader(
1556        "RAR 3.x recovery sector offset overflows",
1557    ))?;
1558    if sector_offset >= protected_len {
1559        return Err(Error::InvalidHeader(
1560            "RAR 3.x recovery sector offset is invalid",
1561        ));
1562    }
1563    let sector_start = protected_start
1564        .checked_add(sector_offset)
1565        .ok_or(Error::InvalidHeader(
1566            "RAR 3.x recovery sector range overflows",
1567        ))?;
1568    let available = 512.min(protected_len - sector_offset);
1569    let mut sector = [0u8; 512];
1570    sector[..available].copy_from_slice(
1571        source
1572            .get(sector_start..sector_start + available)
1573            .ok_or(Error::TooShort)?,
1574    );
1575    Ok(sector)
1576}
1577
1578pub fn repair_rev3_volumes_to<F>(
1579    data_volumes: &[Option<&[u8]>],
1580    recovery_count: usize,
1581    recovery_volumes: &[(usize, &[u8])],
1582    mut write: F,
1583) -> Result<()>
1584where
1585    F: FnMut(usize, &[u8]) -> Result<()>,
1586{
1587    for (index, bytes) in rars_recovery::rar3::reconstruct_data_volumes(
1588        data_volumes,
1589        recovery_count,
1590        recovery_volumes,
1591    )
1592    .map_err(Error::from)?
1593    .into_iter()
1594    .enumerate()
1595    {
1596        let bytes = truncate_repaired_rev3_volume(bytes)?;
1597        write(index, &bytes)?;
1598    }
1599    Ok(())
1600}
1601
1602fn truncate_repaired_rev3_volume(mut bytes: Vec<u8>) -> Result<Vec<u8>> {
1603    let Ok(archive) = Archive::parse(&bytes) else {
1604        return Ok(bytes);
1605    };
1606    let Some(end) = archive.blocks.iter().find_map(|block| match block {
1607        Block::End(end) => Some(end),
1608        _ => None,
1609    }) else {
1610        return Ok(bytes);
1611    };
1612    let end_pos = archive
1613        .sfx_offset
1614        .checked_add(end.offset)
1615        .and_then(|offset| offset.checked_add(block_total_size(end).ok()?))
1616        .ok_or(Error::InvalidHeader(
1617            "RAR 3 repaired volume end offset overflows",
1618        ))?;
1619    if end_pos < bytes.len() && bytes[end_pos..].iter().all(|&byte| byte == 0) {
1620        bytes.truncate(end_pos);
1621    }
1622    Ok(bytes)
1623}
1624
1625struct EncryptedHeader {
1626    block: BlockHeader,
1627    header: Vec<u8>,
1628    total_size: usize,
1629}
1630
1631#[derive(Default)]
1632struct EncryptedHeaderCipherCache {
1633    salt: Option<[u8; 8]>,
1634    cipher: Option<Rar30Cipher>,
1635}
1636
1637impl EncryptedHeaderCipherCache {
1638    fn cipher(&mut self, password: &[u8], salt: [u8; 8]) -> Result<Rar30Cipher> {
1639        if self.salt != Some(salt) {
1640            self.salt = Some(salt);
1641            self.cipher =
1642                Some(Rar30Cipher::new(password, Some(salt)).map_err(map_rar30_crypto_error)?);
1643        }
1644        Ok(self
1645            .cipher
1646            .as_ref()
1647            .expect("RAR 3 encrypted header cipher cache initialized")
1648            .clone())
1649    }
1650}
1651
1652fn map_rar30_crypto_error(error: Rar30Error) -> Error {
1653    Error::from(error)
1654}
1655
1656fn decrypt_encrypted_header_at(
1657    archive: &[u8],
1658    offset: usize,
1659    password: &[u8],
1660    cipher_cache: &mut EncryptedHeaderCipherCache,
1661) -> Result<EncryptedHeader> {
1662    let salt = read_header_salt(archive, offset)?;
1663    let first_ciphertext = archive
1664        .get(offset + 8..offset + 24)
1665        .ok_or(Error::TooShort)?;
1666    let mut cipher = cipher_cache.cipher(password, salt)?;
1667    let mut first_block = [0u8; 16];
1668    first_block.copy_from_slice(first_ciphertext);
1669    cipher
1670        .decrypt_in_place(&mut first_block)
1671        .map_err(map_rar30_crypto_error)?;
1672    let head_size = read_u16(&first_block, 5)? as usize;
1673    if head_size < 7 {
1674        return Err(Error::InvalidHeader("RAR 1.5 block header is too short"));
1675    }
1676    let encrypted_header_size = checked_align16(head_size, "RAR 1.5 block size overflows usize")?;
1677    let encrypted_start = offset
1678        .checked_add(8)
1679        .ok_or(Error::InvalidHeader("RAR 1.5 block offset overflows usize"))?;
1680    let encrypted_end = encrypted_start
1681        .checked_add(encrypted_header_size)
1682        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
1683    let encrypted_rest = archive
1684        .get(offset + 24..encrypted_end)
1685        .ok_or(Error::TooShort)?;
1686    let mut header = Vec::with_capacity(encrypted_header_size);
1687    header.extend_from_slice(&first_block);
1688    header.extend_from_slice(encrypted_rest);
1689    cipher
1690        .decrypt_in_place(&mut header[16..])
1691        .map_err(map_rar30_crypto_error)?;
1692    header.truncate(head_size);
1693
1694    let mut block = parse_block_header(&header, 0)?;
1695    block.offset = offset;
1696    let payload_size = usize::try_from(block.add_size.unwrap_or(0))
1697        .map_err(|_| Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
1698    let total_size = 8usize
1699        .checked_add(encrypted_header_size)
1700        .and_then(|size| size.checked_add(payload_size))
1701        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
1702    Ok(EncryptedHeader {
1703        block,
1704        header,
1705        total_size,
1706    })
1707}
1708
1709fn read_encrypted_header_at(
1710    file: &mut File,
1711    file_len: u64,
1712    archive_offset: usize,
1713    offset: usize,
1714    password: &[u8],
1715    cipher_cache: &mut EncryptedHeaderCipherCache,
1716) -> Result<EncryptedHeader> {
1717    let absolute = archive_offset
1718        .checked_add(offset)
1719        .ok_or(Error::InvalidHeader("RAR 1.5 block offset overflows usize"))?;
1720    if absolute as u64 + 24 > file_len {
1721        return Err(Error::TooShort);
1722    }
1723    let first = read_exact_at(file, absolute, 24)?;
1724    let salt = read_header_salt(&first, 0)?;
1725    let mut cipher = cipher_cache.cipher(password, salt)?;
1726    let mut first_block = [0u8; 16];
1727    first_block.copy_from_slice(&first[8..24]);
1728    cipher
1729        .decrypt_in_place(&mut first_block)
1730        .map_err(map_rar30_crypto_error)?;
1731    let head_size = read_u16(&first_block, 5)? as usize;
1732    if head_size < 7 {
1733        return Err(Error::InvalidHeader("RAR 1.5 block header is too short"));
1734    }
1735    let encrypted_header_size = checked_align16(head_size, "RAR 1.5 block size overflows usize")?;
1736    let encrypted_start = absolute
1737        .checked_add(8)
1738        .ok_or(Error::InvalidHeader("RAR 1.5 block offset overflows usize"))?;
1739    if encrypted_start as u64 + encrypted_header_size as u64 > file_len {
1740        return Err(Error::TooShort);
1741    }
1742    let encrypted_rest = read_exact_at(file, encrypted_start + 16, encrypted_header_size - 16)?;
1743    let mut header = Vec::with_capacity(encrypted_header_size);
1744    header.extend_from_slice(&first_block);
1745    header.extend_from_slice(&encrypted_rest);
1746    cipher
1747        .decrypt_in_place(&mut header[16..])
1748        .map_err(map_rar30_crypto_error)?;
1749    header.truncate(head_size);
1750
1751    let mut block = parse_block_header(&header, 0)?;
1752    block.offset = offset;
1753    let payload_size = usize::try_from(block.add_size.unwrap_or(0))
1754        .map_err(|_| Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
1755    let total_size = 8usize
1756        .checked_add(encrypted_header_size)
1757        .and_then(|size| size.checked_add(payload_size))
1758        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
1759    Ok(EncryptedHeader {
1760        block,
1761        header,
1762        total_size,
1763    })
1764}
1765
1766fn read_header_salt(input: &[u8], offset: usize) -> Result<[u8; 8]> {
1767    input
1768        .get(offset..offset + 8)
1769        .ok_or(Error::TooShort)
1770        .map(|salt| salt.try_into().expect("RAR 3 salt size"))
1771}
1772
1773fn parse_file_like_header(
1774    input: &[u8],
1775    block: BlockHeader,
1776    archive_offset: usize,
1777) -> Result<FileHeader> {
1778    if block.head_size < 32 {
1779        return Err(Error::InvalidHeader("RAR 1.5 file header is too short"));
1780    }
1781    if block.flags & LONG_BLOCK == 0 {
1782        return Err(Error::InvalidHeader(
1783            "RAR 1.5 file header is missing packed data size",
1784        ));
1785    }
1786
1787    let start = block.offset;
1788    let head_end = start + block.head_size as usize;
1789    if head_end > input.len() {
1790        return Err(Error::TooShort);
1791    }
1792
1793    let pack_low = read_u32(input, start + 7)? as u64;
1794    let unp_low = read_u32(input, start + 11)? as u64;
1795    let host_os = input[start + 15];
1796    let file_crc = read_u32(input, start + 16)?;
1797    let file_time = read_u32(input, start + 20)?;
1798    let unp_ver = input[start + 24];
1799    let method = input[start + 25];
1800    let name_size = read_u16(input, start + 26)? as usize;
1801    let attr = read_u32(input, start + 28)?;
1802    let mut pos = start + 32;
1803
1804    let (pack_size, unp_size) = if block.flags & FHD_LARGE != 0 {
1805        let high_pack = read_u32(input, pos)? as u64;
1806        let high_unp = read_u32(input, pos + 4)? as u64;
1807        pos += 8;
1808        ((high_pack << 32) | pack_low, (high_unp << 32) | unp_low)
1809    } else {
1810        (pack_low, unp_low)
1811    };
1812
1813    let name_end = pos
1814        .checked_add(name_size)
1815        .ok_or(Error::InvalidHeader("RAR 1.5 file name size overflows"))?;
1816    if name_end > head_end {
1817        return Err(Error::InvalidHeader(
1818            "RAR 1.5 file name extends beyond header",
1819        ));
1820    }
1821    let name = decode_file_name(&input[pos..name_end], block.flags);
1822    pos = name_end;
1823
1824    let salt = if block.flags & FHD_SALT != 0 {
1825        let salt_end = pos
1826            .checked_add(8)
1827            .ok_or(Error::InvalidHeader("RAR 1.5 salt size overflows"))?;
1828        if salt_end > head_end {
1829            return Err(Error::InvalidHeader(
1830                "RAR 1.5 salt extends beyond file header",
1831            ));
1832        }
1833        let salt_bytes = input.get(pos..salt_end).ok_or(Error::TooShort)?;
1834        pos = salt_end;
1835        Some(
1836            salt_bytes
1837                .try_into()
1838                .expect("RAR 1.5 salt slice has fixed length"),
1839        )
1840    } else {
1841        None
1842    };
1843
1844    let file_comment = if block.flags & FHD_COMMENT != 0 {
1845        if pos + 2 <= head_end {
1846            let comment_len = read_u16(input, pos)? as usize;
1847            let comment_total = comment_len
1848                .checked_add(2)
1849                .ok_or(Error::InvalidHeader("RAR 1.5 file comment size overflows"))?;
1850            let comment_end = pos
1851                .checked_add(comment_total)
1852                .ok_or(Error::InvalidHeader("RAR 1.5 file comment size overflows"))?;
1853            if comment_end <= head_end {
1854                let comment = input[pos..comment_end].to_vec();
1855                pos = comment_end;
1856                comment
1857            } else {
1858                Vec::new()
1859            }
1860        } else {
1861            Vec::new()
1862        }
1863    } else {
1864        Vec::new()
1865    };
1866
1867    let ext_time = if block.flags & FHD_EXTTIME != 0 {
1868        input[pos..head_end].to_vec()
1869    } else {
1870        Vec::new()
1871    };
1872    let data_start = head_end;
1873    let data_len = usize::try_from(pack_size)
1874        .map_err(|_| Error::InvalidHeader("RAR 1.5 packed file size overflows usize"))?;
1875    let data_end = data_start
1876        .checked_add(data_len)
1877        .ok_or(Error::InvalidHeader(
1878            "RAR 1.5 packed file size overflows usize",
1879        ))?;
1880    Ok(FileHeader {
1881        block,
1882        pack_size,
1883        unp_size,
1884        host_os,
1885        file_crc,
1886        file_time,
1887        unp_ver,
1888        method,
1889        name,
1890        attr,
1891        salt,
1892        file_comment,
1893        ext_time,
1894        packed_range: archive_offset + data_start..archive_offset + data_end,
1895    })
1896}
1897
1898fn decode_file_name(raw: &[u8], flags: u16) -> Vec<u8> {
1899    if flags & FHD_UNICODE == 0 {
1900        return raw.to_vec();
1901    }
1902
1903    let Some(zero_pos) = raw.iter().position(|byte| *byte == 0) else {
1904        return raw.to_vec();
1905    };
1906    if zero_pos + 1 >= raw.len() {
1907        return raw[..zero_pos].to_vec();
1908    }
1909
1910    let fallback = &raw[..zero_pos];
1911    let high_byte = raw[zero_pos + 1];
1912    let encoded = &raw[zero_pos + 2..];
1913    let mut pos = 0usize;
1914    let mut flag_byte = 0u8;
1915    let mut flag_bits = 0u8;
1916    let mut dst_pos = 0usize;
1917    let mut units = Vec::new();
1918
1919    while pos < encoded.len() {
1920        if flag_bits == 0 {
1921            flag_byte = encoded[pos];
1922            pos += 1;
1923            flag_bits = 8;
1924        }
1925        let mode = flag_byte >> 6;
1926        flag_byte <<= 2;
1927        flag_bits -= 2;
1928
1929        match mode {
1930            0 => {
1931                let Some(&low) = encoded.get(pos) else {
1932                    return raw.to_vec();
1933                };
1934                pos += 1;
1935                units.push(u16::from(low));
1936                dst_pos += 1;
1937            }
1938            1 => {
1939                let Some(&low) = encoded.get(pos) else {
1940                    return raw.to_vec();
1941                };
1942                pos += 1;
1943                units.push((u16::from(high_byte) << 8) | u16::from(low));
1944                dst_pos += 1;
1945            }
1946            2 => {
1947                let Some((&low, &high)) = encoded.get(pos).zip(encoded.get(pos + 1)) else {
1948                    return raw.to_vec();
1949                };
1950                pos += 2;
1951                units.push((u16::from(high) << 8) | u16::from(low));
1952                dst_pos += 1;
1953            }
1954            3 => {
1955                let Some(&length_byte) = encoded.get(pos) else {
1956                    return raw.to_vec();
1957                };
1958                pos += 1;
1959                let (count, correction, high) = if length_byte & 0x80 != 0 {
1960                    let Some(&correction) = encoded.get(pos) else {
1961                        return raw.to_vec();
1962                    };
1963                    pos += 1;
1964                    ((length_byte & 0x7f) as usize + 2, correction, high_byte)
1965                } else {
1966                    (length_byte as usize + 2, 0, 0)
1967                };
1968                for _ in 0..count {
1969                    let low = fallback
1970                        .get(dst_pos)
1971                        .copied()
1972                        .unwrap_or(b'?')
1973                        .wrapping_add(correction);
1974                    units.push((u16::from(high) << 8) | u16::from(low));
1975                    dst_pos += 1;
1976                }
1977            }
1978            _ => unreachable!("2-bit filename mode"),
1979        }
1980    }
1981
1982    char::decode_utf16(units)
1983        .map(|unit| unit.unwrap_or(char::REPLACEMENT_CHARACTER))
1984        .collect::<String>()
1985        .into_bytes()
1986}
1987
1988fn read_block_header_at(
1989    file: &mut File,
1990    file_len: u64,
1991    archive_offset: usize,
1992    offset: usize,
1993) -> Result<BlockHeader> {
1994    let absolute = archive_offset
1995        .checked_add(offset)
1996        .ok_or(Error::InvalidHeader("RAR 1.5 block offset overflows usize"))?;
1997    if absolute as u64 + 7 > file_len {
1998        return Err(Error::TooShort);
1999    }
2000    let base = read_exact_at(file, absolute, 7)?;
2001    let head_size = read_u16(&base, 5)? as usize;
2002    if head_size < 7 {
2003        return Err(Error::InvalidHeader("RAR 1.5 block header is too short"));
2004    }
2005    if absolute as u64 + head_size as u64 > file_len {
2006        return Err(Error::TooShort);
2007    }
2008    let header = if head_size == 7 {
2009        base
2010    } else {
2011        read_exact_at(file, absolute, head_size)?
2012    };
2013    let mut block = parse_block_header(&header, 0)?;
2014    block.offset = offset;
2015    Ok(block)
2016}
2017
2018fn relative_block(block: &BlockHeader) -> BlockHeader {
2019    let mut relative = block.clone();
2020    relative.offset = 0;
2021    relative
2022}
2023
2024struct CrcWriter<'a, W: Write + ?Sized> {
2025    inner: &'a mut W,
2026    crc: &'a mut Crc32,
2027}
2028
2029impl<W: Write + ?Sized> Write for CrcWriter<'_, W> {
2030    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2031        let written = self.inner.write(buf)?;
2032        self.crc.update(&buf[..written]);
2033        Ok(written)
2034    }
2035
2036    fn flush(&mut self) -> std::io::Result<()> {
2037        self.inner.flush()
2038    }
2039}
2040
2041fn parse_block_header(input: &[u8], offset: usize) -> Result<BlockHeader> {
2042    if input.len() < offset + 7 {
2043        return Err(Error::TooShort);
2044    }
2045    let head_crc = read_u16(input, offset)?;
2046    let head_type = input[offset + 2];
2047    let flags = read_u16(input, offset + 3)?;
2048    let head_size = read_u16(input, offset + 5)?;
2049    if head_size < 7 {
2050        return Err(Error::InvalidHeader("RAR 1.5 block header is too short"));
2051    }
2052    let add_size = if flags & LONG_BLOCK != 0 {
2053        Some(read_u32(input, offset + 7)? as u64)
2054    } else {
2055        None
2056    };
2057    if offset + head_size as usize > input.len() {
2058        return Err(Error::TooShort);
2059    }
2060    if head_type != MARK_HEAD && should_validate_header_crc(head_type) {
2061        let header_end = header_crc_end(input, offset, head_type, flags, head_size)?;
2062        let actual = (crc32(&input[offset + 2..header_end]) & 0xffff) as u16;
2063        if actual != head_crc {
2064            return Err(Error::CrcMismatch {
2065                expected: head_crc,
2066                actual,
2067            });
2068        }
2069    }
2070    validate_legacy_auth_block_size(head_type, head_size)?;
2071
2072    Ok(BlockHeader {
2073        head_crc,
2074        head_type,
2075        flags,
2076        head_size,
2077        add_size,
2078        offset,
2079    })
2080}
2081
2082fn header_crc_end(
2083    input: &[u8],
2084    offset: usize,
2085    head_type: u8,
2086    flags: u16,
2087    head_size: u16,
2088) -> Result<usize> {
2089    let full_end = offset + head_size as usize;
2090    let fixed_end = match head_type {
2091        MAIN_HEAD if flags & MHD_COMMENT != 0 => Some(offset + 13),
2092        COMM_HEAD => Some(offset + 13),
2093        FILE_HEAD if flags & FHD_COMMENT != 0 => Some(file_header_comment_crc_end(input, offset)?),
2094        _ => None,
2095    };
2096    Ok(fixed_end.unwrap_or(full_end).min(full_end))
2097}
2098
2099fn file_header_comment_crc_end(input: &[u8], offset: usize) -> Result<usize> {
2100    if input.len() < offset + 32 {
2101        return Err(Error::TooShort);
2102    }
2103    let flags = read_u16(input, offset + 3)?;
2104    let name_size = read_u16(input, offset + 26)? as usize;
2105    let mut end = offset + 32;
2106    if flags & FHD_LARGE != 0 {
2107        end = end
2108            .checked_add(8)
2109            .ok_or(Error::InvalidHeader("RAR 1.5 file header size overflows"))?;
2110    }
2111    end = end
2112        .checked_add(name_size)
2113        .ok_or(Error::InvalidHeader("RAR 1.5 file header size overflows"))?;
2114    if flags & FHD_SALT != 0 {
2115        end = end
2116            .checked_add(8)
2117            .ok_or(Error::InvalidHeader("RAR 1.5 file header size overflows"))?;
2118    }
2119    Ok(end)
2120}
2121
2122fn should_validate_header_crc(head_type: u8) -> bool {
2123    // Historical AV/SIGN blocks are documented with inconsistent CRC fields in
2124    // real archives, so readers must not reject them solely on HEAD_CRC.
2125    !matches!(head_type, 0x76 | 0x79)
2126}
2127
2128fn validate_legacy_auth_block_size(head_type: u8, head_size: u16) -> Result<()> {
2129    let minimum = match head_type {
2130        0x76 => 21,
2131        0x79 => 182,
2132        _ => return Ok(()),
2133    };
2134    if head_size < minimum {
2135        return Err(Error::InvalidHeader(
2136            "RAR legacy authenticity block is too short",
2137        ));
2138    }
2139    Ok(())
2140}
2141
2142fn block_total_size(block: &BlockHeader) -> Result<usize> {
2143    let total = block.head_size as u64 + block.add_size.unwrap_or(0);
2144    usize::try_from(total).map_err(|_| Error::InvalidHeader("RAR 1.5 block size overflows usize"))
2145}
2146
2147fn file_block_total_size(
2148    block: &BlockHeader,
2149    default_total: usize,
2150    pack_size: u64,
2151) -> Result<usize> {
2152    let low_payload_size = usize::try_from(block.add_size.unwrap_or(0))
2153        .map_err(|_| Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2154    let header_prefix = default_total
2155        .checked_sub(low_payload_size)
2156        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2157    let pack_size = usize::try_from(pack_size)
2158        .map_err(|_| Error::InvalidHeader("RAR 1.5 packed file size overflows usize"))?;
2159    header_prefix
2160        .checked_add(pack_size)
2161        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))
2162}
2163
2164fn checked_block_next(block: &BlockHeader, total: usize, archive_len: usize) -> Result<usize> {
2165    let next = block
2166        .offset
2167        .checked_add(total)
2168        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2169    if next > archive_len {
2170        return Err(Error::TooShort);
2171    }
2172    Ok(next)
2173}
2174
2175fn checked_file_block_next(
2176    sfx_offset: usize,
2177    block: &BlockHeader,
2178    total: usize,
2179    file_len: u64,
2180) -> Result<usize> {
2181    let next = block
2182        .offset
2183        .checked_add(total)
2184        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2185    let absolute_next = sfx_offset
2186        .checked_add(next)
2187        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2188    if absolute_next as u64 > file_len {
2189        return Err(Error::TooShort);
2190    }
2191    Ok(next)
2192}
2193
2194fn packed_range(
2195    archive_offset: usize,
2196    block_offset: usize,
2197    total: usize,
2198    pack_size: u64,
2199) -> Result<Range<usize>> {
2200    let pack_size = usize::try_from(pack_size)
2201        .map_err(|_| Error::InvalidHeader("RAR 1.5 packed file size overflows usize"))?;
2202    let block_end = archive_offset
2203        .checked_add(block_offset)
2204        .and_then(|start| start.checked_add(total))
2205        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2206    let block_start = block_end
2207        .checked_sub(pack_size)
2208        .ok_or(Error::InvalidHeader("RAR 1.5 block size overflows usize"))?;
2209    Ok(block_start..block_end)
2210}
2211
2212#[cfg(test)]
2213mod tests {
2214    use super::*;
2215
2216    fn test_write_main_header(out: &mut Vec<u8>, flags: u16) {
2217        let start = out.len();
2218        out.extend_from_slice(&0u16.to_le_bytes());
2219        out.push(MAIN_HEAD);
2220        out.extend_from_slice(&flags.to_le_bytes());
2221        out.extend_from_slice(&13u16.to_le_bytes());
2222        out.extend_from_slice(&0u16.to_le_bytes());
2223        out.extend_from_slice(&0u32.to_le_bytes());
2224        test_write_header_crc(out, start);
2225    }
2226
2227    fn test_write_header_crc(out: &mut [u8], start: usize) {
2228        let crc = (crc32(&out[start + 2..]) & 0xffff) as u16;
2229        out[start..start + 2].copy_from_slice(&crc.to_le_bytes());
2230    }
2231
2232    fn legacy_auth_block(head_type: u8, head_size: u16) -> Vec<u8> {
2233        let mut block = vec![0; head_size as usize];
2234        block[2] = head_type;
2235        block[5..7].copy_from_slice(&head_size.to_le_bytes());
2236        block
2237    }
2238
2239    #[test]
2240    fn rejects_too_short_legacy_auth_blocks_even_without_crc_check() {
2241        assert!(matches!(
2242            parse_block_header(&legacy_auth_block(0x76, 20), 0),
2243            Err(Error::InvalidHeader(
2244                "RAR legacy authenticity block is too short"
2245            ))
2246        ));
2247        assert!(matches!(
2248            parse_block_header(&legacy_auth_block(0x79, 181), 0),
2249            Err(Error::InvalidHeader(
2250                "RAR legacy authenticity block is too short"
2251            ))
2252        ));
2253    }
2254
2255    #[test]
2256    fn accepts_minimum_legacy_auth_block_sizes_with_bad_crc() {
2257        assert_eq!(
2258            parse_block_header(&legacy_auth_block(0x76, 21), 0)
2259                .unwrap()
2260                .head_size,
2261            21
2262        );
2263        assert_eq!(
2264            parse_block_header(&legacy_auth_block(0x79, 182), 0)
2265                .unwrap()
2266                .head_size,
2267            182
2268        );
2269    }
2270
2271    #[test]
2272    fn parses_fhd_large_high_size_fields_from_file_header() {
2273        let name = b"large.bin";
2274        let head_size = 32 + 8 + name.len();
2275        let mut header = Vec::new();
2276        header.extend_from_slice(&0u16.to_le_bytes());
2277        header.push(FILE_HEAD);
2278        header.extend_from_slice(&(LONG_BLOCK | FHD_LARGE).to_le_bytes());
2279        header.extend_from_slice(&(head_size as u16).to_le_bytes());
2280        header.extend_from_slice(&0x89ab_cdefu32.to_le_bytes());
2281        header.extend_from_slice(&0x7654_3210u32.to_le_bytes());
2282        header.push(3);
2283        header.extend_from_slice(&0x1234_5678u32.to_le_bytes());
2284        header.extend_from_slice(&0x5a21_0000u32.to_le_bytes());
2285        header.push(29);
2286        header.push(0x35);
2287        header.extend_from_slice(&(name.len() as u16).to_le_bytes());
2288        header.extend_from_slice(&0x20u32.to_le_bytes());
2289        header.extend_from_slice(&1u32.to_le_bytes());
2290        header.extend_from_slice(&2u32.to_le_bytes());
2291        header.extend_from_slice(name);
2292
2293        let block = BlockHeader {
2294            head_crc: 0,
2295            head_type: FILE_HEAD,
2296            flags: LONG_BLOCK | FHD_LARGE,
2297            head_size: head_size as u16,
2298            add_size: Some(0x89ab_cdef),
2299            offset: 0,
2300        };
2301        let file = parse_file_like_header(&header, block, 0).unwrap();
2302
2303        assert_eq!(file.pack_size, 0x0000_0001_89ab_cdef);
2304        assert_eq!(file.unp_size, 0x0000_0002_7654_3210);
2305        assert_eq!(file.name, name);
2306        assert_eq!(
2307            file.packed_range,
2308            head_size..head_size + 0x0000_0001_89ab_cdefusize
2309        );
2310    }
2311
2312    #[test]
2313    fn fhd_large_archive_extent_uses_high_packed_size_without_underflowing() {
2314        let name = b"large-zero-low.bin";
2315        let head_size = 32 + 8 + name.len();
2316        let mut archive = Vec::from(RAR15_SIGNATURE);
2317        test_write_main_header(&mut archive, 0);
2318
2319        let start = archive.len();
2320        archive.extend_from_slice(&0u16.to_le_bytes());
2321        archive.push(FILE_HEAD);
2322        archive.extend_from_slice(&(LONG_BLOCK | FHD_LARGE).to_le_bytes());
2323        archive.extend_from_slice(&(head_size as u16).to_le_bytes());
2324        archive.extend_from_slice(&0u32.to_le_bytes());
2325        archive.extend_from_slice(&0u32.to_le_bytes());
2326        archive.push(3);
2327        archive.extend_from_slice(&0u32.to_le_bytes());
2328        archive.extend_from_slice(&0x5a21_0000u32.to_le_bytes());
2329        archive.push(29);
2330        archive.push(0x35);
2331        archive.extend_from_slice(&(name.len() as u16).to_le_bytes());
2332        archive.extend_from_slice(&0x20u32.to_le_bytes());
2333        archive.extend_from_slice(&1u32.to_le_bytes());
2334        archive.extend_from_slice(&1u32.to_le_bytes());
2335        archive.extend_from_slice(name);
2336        test_write_header_crc(&mut archive, start);
2337
2338        assert!(matches!(Archive::parse(&archive), Err(Error::TooShort)));
2339    }
2340
2341    fn block_header_with(flags: u16) -> BlockHeader {
2342        BlockHeader {
2343            head_crc: 0,
2344            head_type: FILE_HEAD,
2345            flags,
2346            head_size: 32,
2347            add_size: None,
2348            offset: 0,
2349        }
2350    }
2351
2352    fn file_header_with(flags: u16) -> FileHeader {
2353        FileHeader {
2354            block: block_header_with(flags),
2355            pack_size: 0,
2356            unp_size: 0,
2357            host_os: 0,
2358            file_crc: 0,
2359            file_time: 0,
2360            unp_ver: 29,
2361            method: 0x30,
2362            name: b"entry".to_vec(),
2363            attr: 0,
2364            salt: None,
2365            file_comment: Vec::new(),
2366            ext_time: Vec::new(),
2367            packed_range: 0..0,
2368        }
2369    }
2370
2371    fn main_header_with(flags: u16) -> MainHeader {
2372        MainHeader {
2373            head_crc: 0,
2374            flags,
2375            head_size: 13,
2376            reserved1: 0,
2377            reserved2: 0,
2378            encrypt_version: None,
2379        }
2380    }
2381
2382    #[test]
2383    fn main_header_predicates_match_each_flag_bit() {
2384        let cases = [
2385            (MHD_VOLUME, MainHeader::is_volume as fn(&MainHeader) -> bool),
2386            (MHD_COMMENT, MainHeader::has_archive_comment),
2387            (MHD_SOLID, MainHeader::is_solid),
2388            (MHD_NEWNUMBERING, MainHeader::uses_new_numbering),
2389            (MHD_PROTECT, MainHeader::has_recovery_record),
2390            (MHD_PASSWORD, MainHeader::has_encrypted_headers),
2391            (MHD_FIRSTVOLUME, MainHeader::is_first_volume),
2392        ];
2393        let zero = main_header_with(0);
2394        for (bit, predicate) in cases {
2395            assert!(
2396                !predicate(&zero),
2397                "expected false when bit {bit:#x} is clear"
2398            );
2399            let one = main_header_with(bit);
2400            assert!(predicate(&one), "expected true when bit {bit:#x} is set");
2401        }
2402    }
2403
2404    #[test]
2405    fn file_header_flag_predicates_track_each_bit() {
2406        let cases = [
2407            (
2408                FHD_SPLIT_BEFORE,
2409                FileHeader::is_split_before as fn(&FileHeader) -> bool,
2410            ),
2411            (FHD_SPLIT_AFTER, FileHeader::is_split_after),
2412            (FHD_PASSWORD, FileHeader::is_encrypted),
2413            (FHD_SOLID, FileHeader::is_solid),
2414            (FHD_EXTTIME, FileHeader::has_ext_time),
2415        ];
2416        let zero = file_header_with(0);
2417        for (bit, predicate) in cases {
2418            assert!(
2419                !predicate(&zero),
2420                "expected false on FileHeader for bit {bit:#x}"
2421            );
2422            let one = file_header_with(bit);
2423            assert!(
2424                predicate(&one),
2425                "expected true on FileHeader for bit {bit:#x}"
2426            );
2427        }
2428
2429        let directory = file_header_with(FHD_DIRECTORY_MASK);
2430        assert!(directory.is_directory());
2431        assert!(!file_header_with(0).is_directory());
2432
2433        let stored = file_header_with(0);
2434        assert!(stored.is_stored());
2435        let mut packed = file_header_with(0);
2436        packed.method = 0x33;
2437        assert!(!packed.is_stored());
2438    }
2439
2440    #[test]
2441    fn file_header_comment_extracts_payload_after_two_byte_size_prefix() {
2442        let mut without_flag = file_header_with(0);
2443        without_flag.file_comment = vec![3, 0, b'a', b'b', b'c'];
2444        assert!(!without_flag.has_file_comment());
2445        assert_eq!(without_flag.file_comment().unwrap(), None);
2446
2447        let mut with_flag = file_header_with(FHD_COMMENT);
2448        with_flag.file_comment = vec![3, 0, b'h', b'e', b'y'];
2449        assert!(with_flag.has_file_comment());
2450        assert_eq!(with_flag.file_comment().unwrap().unwrap(), b"hey");
2451
2452        let mut empty_flagged = file_header_with(FHD_COMMENT);
2453        empty_flagged.file_comment.clear();
2454        assert!(!empty_flagged.has_file_comment());
2455
2456        let mut truncated = file_header_with(FHD_COMMENT);
2457        truncated.file_comment = vec![10, 0, b'a'];
2458        assert!(matches!(truncated.file_comment(), Err(Error::TooShort)));
2459    }
2460
2461    #[test]
2462    fn file_header_name_metadata_and_crc_helpers_describe_entry() {
2463        let mut header = file_header_with(0);
2464        header.name = b"r\xc3\xa9sum\xc3\xa9.txt".to_vec();
2465        header.file_crc = crc32(b"hello");
2466        header.attr = 0x20;
2467        header.host_os = 3;
2468        header.file_time = 0x5a21_0000;
2469
2470        assert_eq!(header.name_bytes(), b"r\xc3\xa9sum\xc3\xa9.txt");
2471        assert_eq!(header.name_lossy(), "résumé.txt");
2472        let meta = header.metadata();
2473        assert_eq!(meta.name, header.name);
2474        assert_eq!(meta.attr, 0x20);
2475        assert_eq!(meta.host_os, 3);
2476        assert_eq!(meta.file_time, 0x5a21_0000);
2477        assert!(!meta.is_directory);
2478
2479        header.verify_crc32(b"hello").unwrap();
2480        match header.verify_crc32(b"different") {
2481            Err(Error::Crc32Mismatch { expected, actual }) => {
2482                assert_eq!(expected, header.file_crc);
2483                assert_ne!(actual, expected);
2484            }
2485            other => panic!("expected Crc32Mismatch, got {other:?}"),
2486        }
2487
2488        let directory = file_header_with(FHD_DIRECTORY_MASK);
2489        assert!(directory.metadata().is_directory);
2490
2491        // Garbage bytes still produce a String through lossy decoding.
2492        let mut garbage = file_header_with(0);
2493        garbage.name = vec![0xff, 0xfe, b'/', 0x80, b'x'];
2494        let lossy = garbage.name_lossy();
2495        assert!(lossy.ends_with("/\u{fffd}x"), "got {lossy:?}");
2496    }
2497
2498    #[test]
2499    fn newsub_header_name_lossy_delegates_to_inner_file_header() {
2500        let mut file = file_header_with(0);
2501        file.name = b"CMT".to_vec();
2502        let sub = NewSubHeader {
2503            file,
2504            kind: NewSubKind::ArchiveComment,
2505        };
2506        assert_eq!(sub.name_lossy(), "CMT");
2507    }
2508
2509    #[test]
2510    fn writer_options_constructor_and_default_match_documented_targets() {
2511        let default = WriterOptions::default();
2512        assert_eq!(default.target, ArchiveVersion::Rar15);
2513        assert_eq!(default.features, FeatureSet::store_only());
2514
2515        let explicit = WriterOptions::new(ArchiveVersion::Rar20, FeatureSet::store_only());
2516        assert_eq!(explicit.target, ArchiveVersion::Rar20);
2517        assert_eq!(explicit.features, FeatureSet::store_only());
2518    }
2519
2520    fn stored_archive_bytes(name: &[u8], data: &[u8]) -> Vec<u8> {
2521        write_stored_archive(
2522            &[StoredEntry {
2523                name,
2524                data,
2525                file_time: 0,
2526                file_attr: 0x20,
2527                host_os: 3,
2528                password: None,
2529                file_comment: None,
2530            }],
2531            WriterOptions::default(),
2532        )
2533        .unwrap()
2534    }
2535
2536    #[test]
2537    fn archive_parse_owned_consumes_buffer_without_changing_dispatch() {
2538        let bytes = stored_archive_bytes(b"owned.txt", b"hello rar15 owned");
2539        let archive = Archive::parse_owned(bytes.clone()).unwrap();
2540        assert_eq!(archive.files().count(), 1);
2541        let file = archive.files().next().unwrap();
2542        assert_eq!(file.name, b"owned.txt");
2543
2544        // Default ArchiveReadOptions delegate also drives the same code path.
2545        let with_options =
2546            Archive::parse_owned_with_options(bytes.clone(), crate::ArchiveReadOptions::default())
2547                .unwrap();
2548        assert_eq!(with_options.files().count(), 1);
2549
2550        let no_password = Archive::parse_owned_with_password(bytes, None).unwrap();
2551        assert_eq!(no_password.files().count(), 1);
2552    }
2553
2554    #[test]
2555    fn archive_parse_owned_with_password_unlocks_encrypted_archive() {
2556        let mut features = FeatureSet::store_only();
2557        features.file_encryption = true;
2558        let bytes = write_stored_archive(
2559            &[StoredEntry {
2560                name: b"locked.txt",
2561                data: b"encrypted owned payload",
2562                file_time: 0,
2563                file_attr: 0x20,
2564                host_os: 3,
2565                password: Some(b"pw"),
2566                file_comment: None,
2567            }],
2568            WriterOptions {
2569                target: ArchiveVersion::Rar20,
2570                features,
2571                compression_level: None,
2572                dictionary_size: None,
2573            },
2574        )
2575        .unwrap();
2576
2577        let archive = Archive::parse_owned_with_password(bytes, Some(b"pw")).unwrap();
2578        let file = archive.files().next().unwrap();
2579        assert!(file.is_encrypted());
2580    }
2581
2582    #[test]
2583    fn file_header_write_packed_data_streams_through_writer() {
2584        let payload = b"write_packed_data direct dump";
2585        let bytes = stored_archive_bytes(b"dump.bin", payload);
2586        let archive = Archive::parse(&bytes).unwrap();
2587        let file = archive.files().next().unwrap();
2588
2589        let mut sink = Vec::new();
2590        file.write_packed_data(&archive, &mut sink).unwrap();
2591        assert_eq!(sink, payload);
2592    }
2593
2594    #[test]
2595    fn file_header_unsupported_compression_describes_method_and_unpack_version() {
2596        let mut header = file_header_with(0);
2597        header.method = 0x33;
2598        header.unp_ver = 26;
2599        let err = header.unsupported_compression();
2600        assert!(matches!(
2601            err,
2602            Error::UnsupportedCompression {
2603                family: "RAR 1.5-4.x",
2604                unpack_version: 26,
2605                method: 0x33,
2606            }
2607        ));
2608    }
2609
2610    #[test]
2611    fn file_header_unsupported_encryption_describes_unpack_version() {
2612        let mut header = file_header_with(FHD_PASSWORD);
2613        header.unp_ver = 36;
2614        let err = header.unsupported_encryption();
2615        assert!(matches!(
2616            err,
2617            Error::UnsupportedEncryption {
2618                family: "RAR 1.5-4.x",
2619                unpack_version: 36,
2620            }
2621        ));
2622    }
2623
2624    #[test]
2625    fn crc_writer_flush_propagates_to_inner_writer() {
2626        struct FlushSpy {
2627            data: Vec<u8>,
2628            flushed: usize,
2629        }
2630        impl Write for FlushSpy {
2631            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2632                self.data.extend_from_slice(buf);
2633                Ok(buf.len())
2634            }
2635            fn flush(&mut self) -> std::io::Result<()> {
2636                self.flushed += 1;
2637                Ok(())
2638            }
2639        }
2640        let mut inner = FlushSpy {
2641            data: Vec::new(),
2642            flushed: 0,
2643        };
2644        let mut crc = Crc32::new();
2645        let mut writer = CrcWriter {
2646            inner: &mut inner,
2647            crc: &mut crc,
2648        };
2649        writer.write_all(b"hi").unwrap();
2650        writer.flush().unwrap();
2651        assert_eq!(inner.data, b"hi");
2652        assert_eq!(inner.flushed, 1);
2653    }
2654
2655    #[test]
2656    fn parse_main_header_rejects_block_size_below_minimum() {
2657        let block = BlockHeader {
2658            head_crc: 0,
2659            head_type: MAIN_HEAD,
2660            flags: 0,
2661            head_size: 12,
2662            add_size: None,
2663            offset: 0,
2664        };
2665        let err = parse_main_header(&[0u8; 32], &block).unwrap_err();
2666        assert_eq!(
2667            err,
2668            Error::InvalidHeader("RAR 1.5 main header is too short")
2669        );
2670    }
2671
2672    #[test]
2673    fn parse_main_header_rejects_block_extending_past_input_buffer() {
2674        let block = BlockHeader {
2675            head_crc: 0,
2676            head_type: MAIN_HEAD,
2677            flags: 0,
2678            head_size: 13,
2679            add_size: None,
2680            offset: 8,
2681        };
2682        // Buffer is shorter than offset + head_size.
2683        let err = parse_main_header(&[0u8; 16], &block).unwrap_err();
2684        assert_eq!(err, Error::TooShort);
2685    }
2686
2687    #[test]
2688    fn parse_main_header_reads_encrypt_version_when_flag_is_set() {
2689        // Build a synthetic main header at offset 0: 13 bytes of base + 1 byte
2690        // for the encrypt_version field at position 13.
2691        let mut input = vec![0u8; 14];
2692        input[13] = 0x29;
2693        let block = BlockHeader {
2694            head_crc: 0,
2695            head_type: MAIN_HEAD,
2696            flags: MHD_ENCRYPTVER,
2697            head_size: 14,
2698            add_size: None,
2699            offset: 0,
2700        };
2701        let main = parse_main_header(&input, &block).unwrap();
2702        assert_eq!(main.encrypt_version, Some(0x29));
2703    }
2704
2705    #[test]
2706    fn decode_file_name_returns_raw_when_unicode_flag_is_clear() {
2707        let raw = b"plain.txt";
2708        let decoded = decode_file_name(raw, 0);
2709        assert_eq!(decoded, raw);
2710    }
2711
2712    #[test]
2713    fn decode_file_name_returns_raw_when_unicode_marker_is_missing() {
2714        // FHD_UNICODE set but no zero byte to split fallback from encoded
2715        // payload — the decoder falls back to the raw bytes.
2716        let raw = b"no-zero-marker";
2717        let decoded = decode_file_name(raw, FHD_UNICODE);
2718        assert_eq!(decoded, raw);
2719    }
2720
2721    #[test]
2722    fn decode_file_name_returns_fallback_when_no_encoded_payload_follows_zero() {
2723        // Zero byte present but nothing after it — the decoder returns just
2724        // the fallback prefix.
2725        let mut raw = b"fallback".to_vec();
2726        raw.push(0);
2727        let decoded = decode_file_name(&raw, FHD_UNICODE);
2728        assert_eq!(decoded, b"fallback");
2729    }
2730
2731    #[test]
2732    fn decode_file_name_decodes_mode_zero_low_byte_only_units() {
2733        // Mode 0 emits one ASCII byte per code unit. Encoded payload format:
2734        //   high_byte, flag_byte, byte0, byte1, ...
2735        // flag_byte = 0b00_00_00_00 → four mode-0 emits.
2736        let mut raw = b"orig".to_vec();
2737        raw.push(0); // separator
2738        raw.push(0); // high_byte (unused for mode 0)
2739        raw.push(0b00_00_00_00); // flag_byte: four mode-0 codes
2740        raw.extend_from_slice(b"abcd");
2741        let decoded = decode_file_name(&raw, FHD_UNICODE);
2742        assert_eq!(decoded, b"abcd");
2743    }
2744
2745    #[test]
2746    fn decode_file_name_decodes_mode_two_full_two_byte_units() {
2747        // Mode 2 reads (low, high) bytes for a full UTF-16 code unit.
2748        // Encode "Hi" via mode 2 twice: flag_byte = 0b10_10_00_00.
2749        let mut raw = b"".to_vec();
2750        raw.push(0); // separator (zero_pos = 0)
2751        raw.push(0); // high_byte placeholder
2752                     // Two mode-2 units, then two mode-0 units which we will not reach.
2753        raw.push(0b10_10_00_00);
2754        // First unit 'H' = U+0048: low=0x48, high=0x00
2755        raw.extend_from_slice(&[0x48, 0x00]);
2756        // Second unit 'i' = U+0069
2757        raw.extend_from_slice(&[0x69, 0x00]);
2758        let decoded = decode_file_name(&raw, FHD_UNICODE);
2759        // The decoder may emit further units from the trailing flag bits;
2760        // accept any output that begins with "Hi".
2761        assert!(decoded.starts_with(b"Hi"), "got {decoded:?}");
2762    }
2763}