1use crate::error::{RarError, Result};
77use crate::file_media::{FileMedia, ReadInterval};
78use crate::inner_file::InnerFile;
79use crate::parsing::{
80 rar5::{Rar5ArchiveHeaderParser, Rar5EncryptionHeaderParser, Rar5FileHeaderParser},
81 ArchiveHeaderParser, FileHeaderParser, MarkerHeaderParser, RarVersion, TerminatorHeaderParser,
82};
83use crate::rar_file_chunk::RarFileChunk;
84use std::collections::HashMap;
85use std::sync::Arc;
86
87const HEADER_PREFETCH_SIZE: u64 = 32 * 1024;
95
96struct HeaderBuffer {
102 data: Vec<u8>,
103 file_offset: u64,
105 prefetch_size: u64,
107}
108
109impl HeaderBuffer {
110 async fn new(media: &Arc<dyn FileMedia>, start: u64, prefetch_size: u64) -> Result<Self> {
112 let end = (start + prefetch_size - 1).min(media.length().saturating_sub(1));
113 if start > end {
114 return Ok(Self {
115 data: Vec::new(),
116 file_offset: start,
117 prefetch_size,
118 });
119 }
120 let data = media.read_range(ReadInterval { start, end }).await?;
121 Ok(Self {
122 data,
123 file_offset: start,
124 prefetch_size,
125 })
126 }
127
128 async fn read(&mut self, media: &Arc<dyn FileMedia>, start: u64, end: u64) -> Result<Vec<u8>> {
130 if end < start {
131 return Ok(Vec::new());
132 }
133 let buf_start = self.file_offset;
134 let buf_end = buf_start + self.data.len() as u64;
135
136 if start >= buf_start && end < buf_end {
138 let local_start = (start - buf_start) as usize;
139 let local_end = (end - buf_start) as usize;
140 return Ok(self.data[local_start..=local_end].to_vec());
141 }
142
143 let prefetch_end = (start + self.prefetch_size - 1).min(media.length().saturating_sub(1));
145 let read_end = end.max(prefetch_end);
146 let data = media
147 .read_range(ReadInterval {
148 start,
149 end: read_end,
150 })
151 .await?;
152 let result = data[..(end - start + 1) as usize].to_vec();
153 self.data = data;
155 self.file_offset = start;
156 Ok(result)
157 }
158}
159
160#[derive(Debug, Clone, Default, PartialEq, Eq)]
177pub struct ArchiveInfo {
178 pub has_recovery_record: bool,
182
183 pub is_solid: bool,
188
189 pub is_locked: bool,
193
194 pub is_multivolume: bool,
198
199 pub has_encrypted_headers: bool,
204
205 pub version: RarVersion,
207}
208
209#[derive(Default)]
225pub struct ParseOptions {
226 pub filter: Option<Box<dyn Fn(&str, usize) -> bool + Send + Sync>>,
231
232 pub max_files: Option<usize>,
237
238 pub header_prefetch_size: Option<u64>,
245
246 #[cfg(feature = "crypto")]
251 pub password: Option<String>,
252}
253
254#[cfg(feature = "crypto")]
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub enum FileEncryptionInfo {
258 Rar5 {
260 salt: [u8; 16],
262 init_v: [u8; 16],
264 lg2_count: u8,
266 },
267 Rar4 {
269 salt: [u8; 8],
271 },
272}
273
274struct ParsedChunk {
276 name: String,
277 chunk: RarFileChunk,
278 continues_in_next: bool,
279 unpacked_size: u64,
280 method: u8,
281 dict_size_log: u8,
283 rar_version: RarVersion,
284 is_solid: bool,
286 #[cfg(feature = "crypto")]
288 encryption: Option<FileEncryptionInfo>,
289}
290
291pub struct RarFilesPackage {
293 files: Vec<Arc<dyn FileMedia>>,
294}
295
296impl RarFilesPackage {
297 pub fn new(files: Vec<Arc<dyn FileMedia>>) -> Self {
302 let mut files = files;
304 files.sort_by(|a, b| Self::volume_order(a.name()).cmp(&Self::volume_order(b.name())));
305 Self { files }
306 }
307
308 fn volume_order(name: &str) -> (u32, String) {
310 let lower = name.to_lowercase();
311 if lower.ends_with(".rar") {
312 if let Some(stem) = lower.strip_suffix(".rar") {
314 if let Some(part_pos) = stem.rfind(".part") {
315 if let Ok(n) = stem[part_pos + 5..].parse::<u32>() {
316 return (n, lower);
317 }
318 }
319 }
320 (0, lower) } else {
322 let ext = lower.rsplit('.').next().unwrap_or("");
324 if ext.starts_with('r') && ext.len() == 3 {
325 ext[1..]
326 .parse::<u32>()
327 .map(|n| (n + 1, lower.clone()))
328 .unwrap_or((1000, lower))
329 } else {
330 (1000, lower)
331 }
332 }
333 }
334
335 pub async fn get_archive_info(&self) -> Result<ArchiveInfo> {
339 use crate::parsing::rar5::Rar5EncryptionHeaderParser;
340
341 if self.files.is_empty() {
342 return Err(RarError::NoFilesFound);
343 }
344
345 let rar_file = &self.files[0];
346 let mut buf = HeaderBuffer::new(rar_file, 0, HEADER_PREFETCH_SIZE).await?;
347
348 let marker_buf = buf.read(rar_file, 0, 7).await?;
349 let marker = MarkerHeaderParser::parse(&marker_buf)?;
350
351 match marker.version {
352 RarVersion::Rar4 => {
353 let archive_buf = buf
354 .read(
355 rar_file,
356 marker.size as u64,
357 marker.size as u64 + ArchiveHeaderParser::HEADER_SIZE as u64 - 1,
358 )
359 .await?;
360 let archive = ArchiveHeaderParser::parse(&archive_buf)?;
361
362 Ok(ArchiveInfo {
363 has_recovery_record: archive.has_recovery,
364 is_solid: archive.has_solid_attributes,
365 is_locked: archive.is_locked,
366 is_multivolume: archive.has_volume_attributes,
367 has_encrypted_headers: archive.is_block_encoded,
368 version: RarVersion::Rar4,
369 })
370 }
371 RarVersion::Rar5 => {
372 let header_buf = buf
373 .read(
374 rar_file,
375 marker.size as u64,
376 (marker.size as u64 + 255).min(rar_file.length() - 1),
377 )
378 .await?;
379
380 let has_encrypted_headers =
381 Rar5EncryptionHeaderParser::is_encryption_header(&header_buf);
382
383 if has_encrypted_headers {
384 Ok(ArchiveInfo {
385 has_encrypted_headers: true,
386 version: RarVersion::Rar5,
387 ..Default::default()
388 })
389 } else {
390 let (archive, _) = Rar5ArchiveHeaderParser::parse(&header_buf)?;
391
392 Ok(ArchiveInfo {
393 has_recovery_record: archive.archive_flags.has_recovery_record,
394 is_solid: archive.archive_flags.is_solid,
395 is_locked: archive.archive_flags.is_locked,
396 is_multivolume: archive.archive_flags.is_volume,
397 has_encrypted_headers: false,
398 version: RarVersion::Rar5,
399 })
400 }
401 }
402 }
403 }
404
405 async fn parse_file(
411 &self,
412 rar_file: &Arc<dyn FileMedia>,
413 opts: &ParseOptions,
414 ) -> Result<Vec<ParsedChunk>> {
415 let prefetch = opts.header_prefetch_size.unwrap_or(HEADER_PREFETCH_SIZE);
418 let mut buf = HeaderBuffer::new(rar_file, 0, prefetch).await?;
419
420 let marker_buf = buf.read(rar_file, 0, 7).await?;
421 let marker = MarkerHeaderParser::parse(&marker_buf)?;
422
423 match marker.version {
424 RarVersion::Rar4 => {
425 self.parse_rar4_file(rar_file, opts, marker.size as u64, &mut buf)
426 .await
427 }
428 RarVersion::Rar5 => self.parse_rar5_file(rar_file, opts, &mut buf).await,
429 }
430 }
431
432 async fn parse_rar4_file(
434 &self,
435 rar_file: &Arc<dyn FileMedia>,
436 opts: &ParseOptions,
437 marker_size: u64,
438 buf: &mut HeaderBuffer,
439 ) -> Result<Vec<ParsedChunk>> {
440 let mut chunks = Vec::new();
441 let mut offset = marker_size;
442
443 let archive_buf = buf
445 .read(
446 rar_file,
447 offset,
448 offset + ArchiveHeaderParser::HEADER_SIZE as u64 - 1,
449 )
450 .await?;
451 let archive = ArchiveHeaderParser::parse(&archive_buf)?;
452 let is_solid = archive.has_solid_attributes;
453 offset += archive.size as u64;
454
455 let mut file_count = 0usize;
456 let mut retrieved_count = 0usize;
457 let terminator_size = TerminatorHeaderParser::HEADER_SIZE as u64;
458
459 while offset < rar_file.length().saturating_sub(terminator_size) {
461 let bytes_available = rar_file.length().saturating_sub(offset);
463 let read_size = (FileHeaderParser::HEADER_SIZE as u64).min(bytes_available);
464
465 if read_size < 32 {
466 break;
468 }
469
470 let header_buf = buf.read(rar_file, offset, offset + read_size - 1).await?;
471
472 let file_header = match FileHeaderParser::parse(&header_buf) {
473 Ok(h) => h,
474 Err(_) => break,
475 };
476
477 if file_header.header_type != 0x74 {
479 break;
480 }
481
482 #[cfg(not(feature = "crypto"))]
484 if file_header.is_encrypted {
485 return Err(RarError::EncryptedNotSupported);
486 }
487
488 let data_start = offset
489 .checked_add(file_header.head_size as u64)
490 .ok_or_else(|| RarError::InvalidOffset {
491 offset,
492 length: rar_file.length(),
493 })?;
494 let data_end = if file_header.packed_size > 0 {
495 data_start
496 .checked_add(file_header.packed_size - 1)
497 .ok_or_else(|| RarError::InvalidOffset {
498 offset: data_start,
499 length: rar_file.length(),
500 })?
501 } else {
502 data_start.saturating_sub(1)
504 };
505
506 let include = match &opts.filter {
508 Some(f) => f(&file_header.name, file_count),
509 None => true,
510 };
511
512 if include {
513 let chunk = RarFileChunk::new(rar_file.clone(), data_start, data_end);
514
515 #[cfg(feature = "crypto")]
517 let encryption = if file_header.is_encrypted {
518 file_header
519 .salt
520 .map(|salt| FileEncryptionInfo::Rar4 { salt })
521 } else {
522 None
523 };
524
525 chunks.push(ParsedChunk {
526 name: file_header.name.clone(),
527 chunk,
528 continues_in_next: file_header.continues_in_next,
529 unpacked_size: file_header.unpacked_size,
530 method: file_header.method,
531 dict_size_log: 22, rar_version: RarVersion::Rar4,
533 is_solid,
534 #[cfg(feature = "crypto")]
535 encryption,
536 });
537 retrieved_count += 1;
538
539 if let Some(max) = opts.max_files {
541 if retrieved_count >= max {
542 break;
543 }
544 }
545 }
546
547 offset = data_end + 1;
548 file_count += 1;
549 }
550
551 Ok(chunks)
552 }
553
554 #[cfg(feature = "crypto")]
557 fn parse_encrypted_header<T, F>(
558 &self,
559 data: &[u8],
560 crypto: &crate::crypto::Rar5Crypto,
561 parser: F,
562 ) -> Result<(T, usize)>
563 where
564 F: FnOnce(&[u8]) -> Result<(T, usize)>,
565 {
566 use crate::parsing::rar5::VintReader;
567
568 if data.len() < 16 {
569 return Err(RarError::InvalidHeader);
570 }
571
572 let mut iv = [0u8; 16];
574 iv.copy_from_slice(&data[..16]);
575
576 let encrypted_start = 16;
580
581 let available = data.len().saturating_sub(encrypted_start);
583 if available < 16 {
584 return Err(RarError::InvalidHeader);
585 }
586
587 let decrypt_len = (available.min(512) / 16) * 16;
589 if decrypt_len == 0 {
590 return Err(RarError::InvalidHeader);
591 }
592
593 let mut decrypted = data[encrypted_start..encrypted_start + decrypt_len].to_vec();
594 crypto
595 .decrypt(&iv, &mut decrypted)
596 .map_err(|e| RarError::DecryptionFailed(e.to_string()))?;
597
598 let (result, _) = parser(&decrypted)?;
600
601 let mut reader = VintReader::new(&decrypted[4..]); let header_size = reader.read().ok_or(RarError::InvalidHeader)?;
605 let size_vint_len = reader.position();
606
607 let plaintext_size = 4 + size_vint_len + header_size as usize;
609 let encrypted_size = plaintext_size.div_ceil(16) * 16;
610
611 Ok((result, 16 + encrypted_size))
613 }
614
615 async fn parse_rar5_file(
617 &self,
618 rar_file: &Arc<dyn FileMedia>,
619 opts: &ParseOptions,
620 buf: &mut HeaderBuffer,
621 ) -> Result<Vec<ParsedChunk>> {
622 let mut chunks = Vec::new();
623 let mut offset = 8u64; let header_buf = buf
627 .read(
628 rar_file,
629 offset,
630 (offset + 256 - 1).min(rar_file.length() - 1),
631 )
632 .await?;
633
634 #[cfg(feature = "crypto")]
636 let header_crypto: Option<crate::crypto::Rar5Crypto> =
637 if Rar5EncryptionHeaderParser::is_encryption_header(&header_buf) {
638 let (enc_header, consumed) = Rar5EncryptionHeaderParser::parse(&header_buf)?;
639 offset += consumed as u64;
640
641 let password = opts.password.as_ref().ok_or(RarError::PasswordRequired)?;
643
644 Some(crate::crypto::Rar5Crypto::derive_key(
645 password,
646 &enc_header.salt,
647 enc_header.lg2_count,
648 ))
649 } else {
650 None
651 };
652
653 #[cfg(not(feature = "crypto"))]
654 if Rar5EncryptionHeaderParser::is_encryption_header(&header_buf) {
655 return Err(RarError::PasswordRequired);
656 }
657
658 #[cfg(feature = "crypto")]
660 let (archive_header, consumed) = if let Some(ref crypto) = header_crypto {
661 let enc_buf = buf
663 .read(
664 rar_file,
665 offset,
666 (offset + 512 - 1).min(rar_file.length() - 1),
667 )
668 .await?;
669
670 self.parse_encrypted_header(&enc_buf, crypto, |data| {
671 Rar5ArchiveHeaderParser::parse(data)
672 })?
673 } else {
674 Rar5ArchiveHeaderParser::parse(&header_buf)?
675 };
676
677 #[cfg(not(feature = "crypto"))]
678 let (archive_header, consumed) = Rar5ArchiveHeaderParser::parse(&header_buf)?;
679
680 let is_solid = archive_header.archive_flags.is_solid;
681 offset += consumed as u64;
682
683 let mut file_count = 0usize;
684 let mut retrieved_count = 0usize;
685
686 while offset < rar_file.length().saturating_sub(16) {
688 let bytes_available = rar_file.length().saturating_sub(offset);
690 let read_size = 512u64.min(bytes_available);
691
692 if read_size < 16 {
693 break;
694 }
695
696 let header_buf = buf.read(rar_file, offset, offset + read_size - 1).await?;
697
698 #[cfg(feature = "crypto")]
700 let (file_header, header_consumed) = if let Some(ref crypto) = header_crypto {
701 match self.parse_encrypted_header(&header_buf, crypto, |data| {
702 Rar5FileHeaderParser::parse(data)
703 }) {
704 Ok(h) => h,
705 Err(_) => break,
706 }
707 } else {
708 match Rar5FileHeaderParser::parse(&header_buf) {
709 Ok(h) => h,
710 Err(_) => break,
711 }
712 };
713
714 #[cfg(not(feature = "crypto"))]
715 let (file_header, header_consumed) = match Rar5FileHeaderParser::parse(&header_buf) {
716 Ok(h) => h,
717 Err(_) => break,
718 };
719
720 let data_start = offset.checked_add(header_consumed as u64).ok_or_else(|| {
721 RarError::InvalidOffset {
722 offset,
723 length: rar_file.length(),
724 }
725 })?;
726 let data_end = if file_header.packed_size > 0 {
727 data_start
728 .checked_add(file_header.packed_size - 1)
729 .ok_or_else(|| RarError::InvalidOffset {
730 offset: data_start,
731 length: rar_file.length(),
732 })?
733 } else {
734 data_start.saturating_sub(1)
736 };
737
738 let include = match &opts.filter {
740 Some(f) => f(&file_header.name, file_count),
741 None => true,
742 };
743
744 if include {
745 let chunk = RarFileChunk::new(rar_file.clone(), data_start, data_end);
746
747 let method = file_header.compression.method;
751
752 #[cfg(feature = "crypto")]
754 let encryption = if file_header.is_encrypted() {
755 file_header.encryption_info().and_then(|data| {
756 crate::crypto::Rar5EncryptionInfo::parse(data)
757 .ok()
758 .map(|info| FileEncryptionInfo::Rar5 {
759 salt: info.salt,
760 init_v: info.init_v,
761 lg2_count: info.lg2_count,
762 })
763 })
764 } else {
765 None
766 };
767
768 chunks.push(ParsedChunk {
769 name: file_header.name.clone(),
770 chunk,
771 continues_in_next: file_header.continues_in_next(),
772 unpacked_size: file_header.unpacked_size,
773 method,
774 dict_size_log: file_header.compression.dict_size_log,
775 rar_version: RarVersion::Rar5,
776 is_solid,
777 #[cfg(feature = "crypto")]
778 encryption,
779 });
780 retrieved_count += 1;
781
782 if let Some(max) = opts.max_files {
783 if retrieved_count >= max {
784 break;
785 }
786 }
787 }
788
789 offset = data_end + 1;
790 file_count += 1;
791 }
792
793 Ok(chunks)
794 }
795
796 pub async fn parse(&self, opts: ParseOptions) -> Result<Vec<InnerFile>> {
798 if self.files.is_empty() {
799 return Err(RarError::NoFilesFound);
800 }
801
802 let mut all_parsed: Vec<Vec<ParsedChunk>> = Vec::new();
803
804 let mut i = 0;
805 while i < self.files.len() {
806 let file = &self.files[i];
807 let chunks = self.parse_file(file, &opts).await?;
808
809 if chunks.is_empty() {
810 i += 1;
811 continue;
812 }
813
814 let continues = chunks.last().unwrap().continues_in_next;
816
817 all_parsed.push(chunks);
818
819 if continues {
822 while i + 1 < self.files.len() {
823 i += 1;
824 let next_file = &self.files[i];
825
826 let cont_chunks = self.parse_file(next_file, &opts).await?;
827 if cont_chunks.is_empty() {
828 break;
829 }
830
831 let cont_continues = cont_chunks.last().unwrap().continues_in_next;
832 all_parsed.push(cont_chunks);
833
834 if !cont_continues {
835 break;
836 }
837 }
838 }
839
840 i += 1;
841 }
842
843 let all_chunks: Vec<ParsedChunk> = all_parsed.into_iter().flatten().collect();
845
846 #[cfg(feature = "crypto")]
847 type GroupValue = (
848 Vec<RarFileChunk>,
849 u8,
850 u8, u64,
852 RarVersion,
853 bool, Option<FileEncryptionInfo>,
855 );
856 #[cfg(not(feature = "crypto"))]
857 type GroupValue = (Vec<RarFileChunk>, u8, u8, u64, RarVersion, bool);
858
859 let mut grouped: HashMap<String, GroupValue> = HashMap::new();
860 for chunk in all_chunks {
861 #[cfg(feature = "crypto")]
862 let entry = grouped.entry(chunk.name).or_insert_with(|| {
863 (
864 Vec::new(),
865 chunk.method,
866 chunk.dict_size_log,
867 chunk.unpacked_size,
868 chunk.rar_version,
869 chunk.is_solid,
870 chunk.encryption,
871 )
872 });
873 #[cfg(not(feature = "crypto"))]
874 let entry = grouped.entry(chunk.name).or_insert_with(|| {
875 (
876 Vec::new(),
877 chunk.method,
878 chunk.dict_size_log,
879 chunk.unpacked_size,
880 chunk.rar_version,
881 chunk.is_solid,
882 )
883 });
884 entry.0.push(chunk.chunk);
885 }
886
887 #[cfg(feature = "crypto")]
889 let password = opts.password.clone();
890
891 let inner_files: Vec<InnerFile> = grouped
892 .into_iter()
893 .map(|(name, value)| {
894 #[cfg(feature = "crypto")]
895 {
896 let (
897 chunks,
898 method,
899 dict_size_log,
900 unpacked_size,
901 rar_version,
902 is_solid,
903 encryption,
904 ) = value;
905 let enc_info = encryption.map(|e| match e {
906 FileEncryptionInfo::Rar5 {
907 salt,
908 init_v,
909 lg2_count,
910 } => crate::inner_file::EncryptionInfo::Rar5 {
911 salt,
912 init_v,
913 lg2_count,
914 },
915 FileEncryptionInfo::Rar4 { salt } => {
916 crate::inner_file::EncryptionInfo::Rar4 { salt }
917 }
918 });
919 InnerFile::new_encrypted_with_solid_dict(
920 name,
921 chunks,
922 method,
923 dict_size_log,
924 unpacked_size,
925 rar_version,
926 enc_info,
927 password.clone(),
928 is_solid,
929 )
930 }
931 #[cfg(not(feature = "crypto"))]
932 {
933 let (chunks, method, dict_size_log, unpacked_size, rar_version, is_solid) =
934 value;
935 InnerFile::new_with_solid_dict(
936 name,
937 chunks,
938 method,
939 dict_size_log,
940 unpacked_size,
941 rar_version,
942 is_solid,
943 )
944 }
945 })
946 .collect();
947
948 Ok(inner_files)
949 }
950}
951
952#[cfg(test)]
953mod tests {
954 use super::*;
955 use crate::file_media::{FileMedia, LocalFileMedia};
956
957 #[test]
958 fn test_volume_order_old_style() {
959 assert_eq!(RarFilesPackage::volume_order("archive.rar").0, 0);
961 assert_eq!(RarFilesPackage::volume_order("archive.r00").0, 1);
963 assert_eq!(RarFilesPackage::volume_order("archive.r01").0, 2);
964 assert_eq!(RarFilesPackage::volume_order("archive.r99").0, 100);
965 }
966
967 #[test]
968 fn test_volume_order_part_naming() {
969 let mut names = vec![
971 "archive.part10.rar",
972 "archive.part2.rar",
973 "archive.part1.rar",
974 "archive.part3.rar",
975 ];
976 names.sort_by_key(|n| RarFilesPackage::volume_order(n));
977 assert_eq!(
978 names,
979 vec![
980 "archive.part1.rar",
981 "archive.part2.rar",
982 "archive.part3.rar",
983 "archive.part10.rar",
984 ]
985 );
986 }
987
988 #[test]
989 fn test_volume_order_unknown() {
990 assert_eq!(RarFilesPackage::volume_order("archive.zip").0, 1000);
992 }
993
994 #[tokio::test]
995 #[cfg(feature = "async")]
996 async fn test_get_archive_info_rar5() {
997 let file: Arc<dyn FileMedia> =
998 Arc::new(LocalFileMedia::new("__fixtures__/rar5/test.rar").unwrap());
999 let package = RarFilesPackage::new(vec![file]);
1000
1001 let info = package.get_archive_info().await.unwrap();
1002 assert_eq!(info.version, RarVersion::Rar5);
1003 assert!(!info.is_multivolume);
1004 }
1005
1006 #[tokio::test]
1007 #[cfg(feature = "async")]
1008 async fn test_get_archive_info_rar4() {
1009 let file: Arc<dyn FileMedia> =
1010 Arc::new(LocalFileMedia::new("__fixtures__/single/single.rar").unwrap());
1011 let package = RarFilesPackage::new(vec![file]);
1012
1013 let info = package.get_archive_info().await.unwrap();
1014 assert_eq!(info.version, RarVersion::Rar4);
1015 assert!(!info.is_multivolume);
1016 }
1017
1018 #[tokio::test]
1019 #[cfg(feature = "async")]
1020 async fn test_parse_rar5_stored() {
1021 let file: Arc<dyn FileMedia> =
1023 Arc::new(LocalFileMedia::new("__fixtures__/rar5/test.rar").unwrap());
1024 let package = RarFilesPackage::new(vec![file]);
1025
1026 let files = package.parse(ParseOptions::default()).await.unwrap();
1027
1028 assert_eq!(files.len(), 1);
1029 assert_eq!(files[0].name, "test.txt");
1030 }
1031
1032 #[tokio::test]
1033 #[cfg(feature = "async")]
1034 async fn test_parse_rar5_compressed() {
1035 let file: Arc<dyn FileMedia> =
1037 Arc::new(LocalFileMedia::new("__fixtures__/rar5/compressed.rar").unwrap());
1038 let package = RarFilesPackage::new(vec![file]);
1039
1040 let files = package.parse(ParseOptions::default()).await.unwrap();
1041
1042 assert_eq!(files.len(), 1);
1043 assert_eq!(files[0].name, "compress_test.txt");
1044 assert_eq!(files[0].length, 152); match files[0].read_to_end().await {
1049 Ok(content) => {
1050 eprintln!("Got {} bytes of output", content.len());
1051 eprintln!("First 32 bytes: {:02x?}", &content[..32.min(content.len())]);
1052
1053 assert_eq!(
1055 content.len(),
1056 152,
1057 "decompressed size should match unpacked size"
1058 );
1059
1060 match std::str::from_utf8(&content) {
1062 Ok(text) => {
1063 assert!(
1064 text.contains("This is a test file"),
1065 "content should contain expected text"
1066 );
1067 assert!(
1068 text.contains("hello hello"),
1069 "content should contain repeated text"
1070 );
1071 }
1072 Err(_) => {
1073 eprintln!(
1075 "RAR5 decompression output is not valid UTF-8 (work in progress)"
1076 );
1077 }
1078 }
1079 }
1080 Err(e) => {
1081 eprintln!("RAR5 decompression error: {:?}", e);
1083 }
1084 }
1085 }
1086
1087 #[tokio::test]
1088 #[cfg(feature = "async")]
1089 async fn test_parse_rar5_multivolume() {
1090 let fixture_dir = "__fixtures__/rar5-multivolume";
1092
1093 let mut volume_paths: Vec<String> = std::fs::read_dir(fixture_dir)
1095 .unwrap()
1096 .filter_map(|e| e.ok())
1097 .map(|e| e.path())
1098 .filter(|p| p.extension().map_or(false, |ext| ext == "rar"))
1099 .map(|p| p.to_string_lossy().to_string())
1100 .collect();
1101
1102 volume_paths.sort();
1104
1105 if volume_paths.is_empty() {
1106 eprintln!("Skipping test - no multi-volume fixtures found");
1108 return;
1109 }
1110
1111 eprintln!("Found {} volumes: {:?}", volume_paths.len(), volume_paths);
1112
1113 let files: Vec<Arc<dyn FileMedia>> = volume_paths
1115 .iter()
1116 .map(|p| Arc::new(LocalFileMedia::new(p).unwrap()) as Arc<dyn FileMedia>)
1117 .collect();
1118
1119 let package = RarFilesPackage::new(files);
1120
1121 let parsed = package.parse(ParseOptions::default()).await.unwrap();
1122
1123 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1124 assert_eq!(parsed[0].name, "testfile.txt");
1125
1126 eprintln!("Parsed length: {}", parsed[0].length);
1129
1130 let content = parsed[0].read_to_end().await.unwrap();
1132 eprintln!("Read content length: {}", content.len());
1133
1134 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1136 assert!(text.contains("Line 1:"), "should contain first line");
1137 assert!(text.contains("Line 100:"), "should contain last line");
1138
1139 assert!(content.len() >= 11000, "should have at least 11000 bytes");
1141 }
1142
1143 #[tokio::test]
1144 #[cfg(all(feature = "async", feature = "crypto"))]
1145 async fn test_parse_rar5_encrypted_stored() {
1146 let fixture = "__fixtures__/encrypted/rar5-encrypted-stored.rar";
1148
1149 if !std::path::Path::new(fixture).exists() {
1150 eprintln!("Skipping test - encrypted fixtures not found");
1151 return;
1152 }
1153
1154 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1155 let package = RarFilesPackage::new(vec![file]);
1156
1157 let opts = ParseOptions {
1158 password: Some("testpass".to_string()),
1159 ..Default::default()
1160 };
1161
1162 let parsed = package.parse(opts).await.unwrap();
1163 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1164
1165 let inner_file = &parsed[0];
1166 assert_eq!(inner_file.name, "testfile.txt");
1167 assert!(inner_file.is_encrypted());
1168
1169 let content = inner_file.read_decompressed().await.unwrap();
1171 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1172
1173 assert!(text.starts_with("Hello, encrypted world!"));
1174 }
1175
1176 #[tokio::test]
1177 #[cfg(all(feature = "async", feature = "crypto"))]
1178 async fn test_parse_rar5_encrypted_no_password() {
1179 let fixture = "__fixtures__/encrypted/rar5-encrypted-stored.rar";
1180
1181 if !std::path::Path::new(fixture).exists() {
1182 eprintln!("Skipping test - encrypted fixtures not found");
1183 return;
1184 }
1185
1186 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1187 let package = RarFilesPackage::new(vec![file]);
1188
1189 let parsed = package.parse(ParseOptions::default()).await.unwrap();
1191 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1192
1193 let inner_file = &parsed[0];
1194 assert!(inner_file.is_encrypted());
1195
1196 let result = inner_file.read_decompressed().await;
1198 assert!(result.is_err());
1199 match result {
1200 Err(crate::RarError::PasswordRequired) => {
1201 }
1203 Err(e) => panic!("Expected PasswordRequired error, got: {:?}", e),
1204 Ok(_) => panic!("Expected error but got success"),
1205 }
1206 }
1207
1208 #[tokio::test]
1209 #[cfg(all(feature = "async", feature = "crypto"))]
1210 async fn test_parse_rar5_encrypted_headers() {
1211 let fixture = "__fixtures__/encrypted/rar5-encrypted-headers.rar";
1213
1214 if !std::path::Path::new(fixture).exists() {
1215 eprintln!("Skipping test - encrypted headers fixture not found");
1216 return;
1217 }
1218
1219 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1220 let package = RarFilesPackage::new(vec![file]);
1221
1222 let info = package.get_archive_info().await.unwrap();
1224 assert!(info.has_encrypted_headers, "should have encrypted headers");
1225 assert_eq!(info.version, RarVersion::Rar5);
1226
1227 let result = package.parse(ParseOptions::default()).await;
1229 assert!(
1230 matches!(result, Err(RarError::PasswordRequired)),
1231 "should require password for encrypted headers, got {:?}",
1232 result
1233 );
1234
1235 let opts = ParseOptions {
1237 password: Some("testpass".to_string()),
1238 ..Default::default()
1239 };
1240
1241 let parsed = package.parse(opts).await.unwrap();
1242 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1243 assert_eq!(parsed[0].name, "testfile.txt");
1244
1245 let content = parsed[0].read_decompressed().await.unwrap();
1247 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1248 assert!(
1249 text.starts_with("Hello, encrypted world!"),
1250 "content was: {:?}",
1251 text
1252 );
1253 }
1254
1255 #[tokio::test]
1256 #[cfg(all(feature = "async", feature = "crypto"))]
1257 async fn test_get_archive_info_encrypted_headers() {
1258 let fixture = "__fixtures__/encrypted/rar5-encrypted-headers.rar";
1260
1261 if !std::path::Path::new(fixture).exists() {
1262 eprintln!("Skipping test - encrypted headers fixture not found");
1263 return;
1264 }
1265
1266 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1267 let package = RarFilesPackage::new(vec![file]);
1268
1269 let info = package.get_archive_info().await.unwrap();
1270 assert!(info.has_encrypted_headers);
1271 assert_eq!(info.version, RarVersion::Rar5);
1272 }
1274
1275 #[tokio::test]
1276 #[cfg(all(feature = "async", feature = "crypto"))]
1277 async fn test_parse_rar4_encrypted_stored() {
1278 let fixture = "__fixtures__/encrypted/rar4-encrypted-stored.rar";
1280
1281 if !std::path::Path::new(fixture).exists() {
1282 eprintln!("Skipping test - RAR4 encrypted fixtures not found");
1283 return;
1284 }
1285
1286 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1287 let package = RarFilesPackage::new(vec![file]);
1288
1289 let info = package.get_archive_info().await.unwrap();
1291 assert_eq!(info.version, RarVersion::Rar4);
1292
1293 let opts = ParseOptions {
1294 password: Some("testpass".to_string()),
1295 ..Default::default()
1296 };
1297
1298 let parsed = package.parse(opts).await.unwrap();
1299 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1300
1301 let inner_file = &parsed[0];
1302 assert_eq!(inner_file.name, "testfile.txt");
1303 assert!(inner_file.is_encrypted());
1304
1305 let content = inner_file.read_decompressed().await.unwrap();
1307 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1308
1309 assert!(
1310 text.starts_with("Hello, encrypted world!"),
1311 "content was: {:?}",
1312 text
1313 );
1314 }
1315
1316 #[tokio::test]
1317 #[cfg(all(feature = "async", feature = "crypto"))]
1318 async fn test_parse_rar4_encrypted_compressed() {
1319 let fixture = "__fixtures__/encrypted/rar4-encrypted.rar";
1321
1322 if !std::path::Path::new(fixture).exists() {
1323 eprintln!("Skipping test - RAR4 encrypted fixtures not found");
1324 return;
1325 }
1326
1327 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1328 let package = RarFilesPackage::new(vec![file]);
1329
1330 let info = package.get_archive_info().await.unwrap();
1332 assert_eq!(info.version, RarVersion::Rar4);
1333
1334 let opts = ParseOptions {
1335 password: Some("testpass".to_string()),
1336 ..Default::default()
1337 };
1338
1339 let parsed = package.parse(opts).await.unwrap();
1340 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1341
1342 let inner_file = &parsed[0];
1343 assert_eq!(inner_file.name, "testfile.txt");
1344 assert!(inner_file.is_encrypted());
1345
1346 let content = inner_file.read_decompressed().await.unwrap();
1348 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1349
1350 assert!(
1351 text.starts_with("Hello, encrypted world!"),
1352 "content was: {:?}",
1353 text
1354 );
1355 }
1356}