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