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
87#[derive(Debug, Clone, Default, PartialEq, Eq)]
104pub struct ArchiveInfo {
105 pub has_recovery_record: bool,
109
110 pub is_solid: bool,
115
116 pub is_locked: bool,
120
121 pub is_multivolume: bool,
125
126 pub has_encrypted_headers: bool,
131
132 pub version: RarVersion,
134}
135
136#[derive(Default)]
152pub struct ParseOptions {
153 pub filter: Option<Box<dyn Fn(&str, usize) -> bool + Send + Sync>>,
158
159 pub max_files: Option<usize>,
164
165 #[cfg(feature = "crypto")]
170 pub password: Option<String>,
171}
172
173#[cfg(feature = "crypto")]
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum FileEncryptionInfo {
177 Rar5 {
179 salt: [u8; 16],
181 init_v: [u8; 16],
183 lg2_count: u8,
185 },
186 Rar4 {
188 salt: [u8; 8],
190 },
191}
192
193struct ParsedChunk {
195 name: String,
196 chunk: RarFileChunk,
197 continues_in_next: bool,
198 unpacked_size: u64,
199 chunk_size: u64,
200 method: u8,
201 dict_size_log: u8,
203 rar_version: RarVersion,
204 is_solid: bool,
206 #[cfg(feature = "crypto")]
208 encryption: Option<FileEncryptionInfo>,
209}
210
211pub struct RarFilesPackage {
213 files: Vec<Arc<dyn FileMedia>>,
214}
215
216impl RarFilesPackage {
217 pub fn new(files: Vec<Arc<dyn FileMedia>>) -> Self {
218 let mut files = files;
220 files.sort_by(|a, b| Self::volume_order(a.name()).cmp(&Self::volume_order(b.name())));
221 Self { files }
222 }
223
224 fn volume_order(name: &str) -> (u32, String) {
226 let lower = name.to_lowercase();
227 if lower.ends_with(".rar") {
228 (0, lower) } else {
230 let ext = lower.rsplit('.').next().unwrap_or("");
232 if ext.starts_with('r') && ext.len() == 3 {
233 ext[1..]
234 .parse::<u32>()
235 .map(|n| (n + 1, lower.clone()))
236 .unwrap_or((1000, lower))
237 } else {
238 (1000, lower)
239 }
240 }
241 }
242
243 pub async fn get_archive_info(&self) -> Result<ArchiveInfo> {
245 use crate::parsing::rar5::Rar5EncryptionHeaderParser;
246
247 if self.files.is_empty() {
248 return Err(RarError::NoFilesFound);
249 }
250
251 let rar_file = &self.files[0];
252 let marker_buf = rar_file
253 .read_range(ReadInterval {
254 start: 0,
255 end: 7, })
257 .await?;
258
259 let marker = MarkerHeaderParser::parse(&marker_buf)?;
260
261 match marker.version {
262 RarVersion::Rar4 => {
263 let archive_buf = rar_file
264 .read_range(ReadInterval {
265 start: marker.size as u64,
266 end: marker.size as u64 + ArchiveHeaderParser::HEADER_SIZE as u64 - 1,
267 })
268 .await?;
269 let archive = ArchiveHeaderParser::parse(&archive_buf)?;
270
271 Ok(ArchiveInfo {
272 has_recovery_record: archive.has_recovery,
273 is_solid: archive.has_solid_attributes,
274 is_locked: archive.is_locked,
275 is_multivolume: archive.has_volume_attributes,
276 has_encrypted_headers: archive.is_block_encoded,
277 version: RarVersion::Rar4,
278 })
279 }
280 RarVersion::Rar5 => {
281 let header_buf = rar_file
283 .read_range(ReadInterval {
284 start: marker.size as u64,
285 end: (marker.size as u64 + 255).min(rar_file.length() - 1),
286 })
287 .await?;
288
289 let has_encrypted_headers =
290 Rar5EncryptionHeaderParser::is_encryption_header(&header_buf);
291
292 if has_encrypted_headers {
293 Ok(ArchiveInfo {
295 has_encrypted_headers: true,
296 version: RarVersion::Rar5,
297 ..Default::default()
298 })
299 } else {
300 let (archive, _) = Rar5ArchiveHeaderParser::parse(&header_buf)?;
301
302 Ok(ArchiveInfo {
303 has_recovery_record: archive.archive_flags.has_recovery_record,
304 is_solid: archive.archive_flags.is_solid,
305 is_locked: archive.archive_flags.is_locked,
306 is_multivolume: archive.archive_flags.is_volume,
307 has_encrypted_headers: false,
308 version: RarVersion::Rar5,
309 })
310 }
311 }
312 }
313 }
314
315 async fn parse_file(
317 &self,
318 rar_file: &Arc<dyn FileMedia>,
319 opts: &ParseOptions,
320 ) -> Result<Vec<ParsedChunk>> {
321 #[allow(unused_mut)]
322 let mut offset = 0u64;
323
324 let marker_buf = rar_file
326 .read_range(ReadInterval {
327 start: offset,
328 end: offset + 8 - 1, })
330 .await?;
331
332 let marker = MarkerHeaderParser::parse(&marker_buf)?;
333
334 match marker.version {
336 RarVersion::Rar4 => {
337 self.parse_rar4_file(rar_file, opts, marker.size as u64)
338 .await
339 }
340 RarVersion::Rar5 => self.parse_rar5_file(rar_file, opts).await,
341 }
342 }
343
344 async fn parse_rar4_file(
346 &self,
347 rar_file: &Arc<dyn FileMedia>,
348 opts: &ParseOptions,
349 marker_size: u64,
350 ) -> Result<Vec<ParsedChunk>> {
351 let mut chunks = Vec::new();
352 let mut offset = marker_size;
353
354 let archive_buf = rar_file
356 .read_range(ReadInterval {
357 start: offset,
358 end: offset + ArchiveHeaderParser::HEADER_SIZE as u64 - 1,
359 })
360 .await?;
361 let archive = ArchiveHeaderParser::parse(&archive_buf)?;
362 let is_solid = archive.has_solid_attributes;
363 offset += archive.size as u64;
364
365 let mut file_count = 0usize;
366 let mut retrieved_count = 0usize;
367 let terminator_size = TerminatorHeaderParser::HEADER_SIZE as u64;
368
369 while offset < rar_file.length().saturating_sub(terminator_size) {
371 let bytes_available = rar_file.length().saturating_sub(offset);
373 let read_size = (FileHeaderParser::HEADER_SIZE as u64).min(bytes_available);
374
375 if read_size < 32 {
376 break;
378 }
379
380 let header_buf = rar_file
381 .read_range(ReadInterval {
382 start: offset,
383 end: offset + read_size - 1,
384 })
385 .await?;
386
387 let file_header = match FileHeaderParser::parse(&header_buf) {
388 Ok(h) => h,
389 Err(_) => break,
390 };
391
392 if file_header.header_type != 0x74 {
394 break;
395 }
396
397 #[cfg(not(feature = "crypto"))]
399 if file_header.is_encrypted {
400 return Err(RarError::EncryptedNotSupported);
401 }
402
403 let data_start = offset + file_header.head_size as u64;
404 let data_end = if file_header.packed_size > 0 {
405 data_start + file_header.packed_size - 1
406 } else {
407 data_start
408 };
409
410 let include = match &opts.filter {
412 Some(f) => f(&file_header.name, file_count),
413 None => true,
414 };
415
416 if include {
417 let chunk = RarFileChunk::new(rar_file.clone(), data_start, data_end);
418 let chunk_size = chunk.length();
419
420 #[cfg(feature = "crypto")]
422 let encryption = if file_header.is_encrypted {
423 file_header
424 .salt
425 .map(|salt| FileEncryptionInfo::Rar4 { salt })
426 } else {
427 None
428 };
429
430 chunks.push(ParsedChunk {
431 name: file_header.name.clone(),
432 chunk,
433 continues_in_next: file_header.continues_in_next,
434 unpacked_size: file_header.unpacked_size,
435 chunk_size,
436 method: file_header.method,
437 dict_size_log: 22, rar_version: RarVersion::Rar4,
439 is_solid,
440 #[cfg(feature = "crypto")]
441 encryption,
442 });
443 retrieved_count += 1;
444
445 if let Some(max) = opts.max_files {
447 if retrieved_count >= max {
448 break;
449 }
450 }
451 }
452
453 offset = data_end + 1;
454 file_count += 1;
455 }
456
457 Ok(chunks)
458 }
459
460 #[cfg(feature = "crypto")]
463 fn parse_encrypted_header<T, F>(
464 &self,
465 data: &[u8],
466 crypto: &crate::crypto::Rar5Crypto,
467 parser: F,
468 ) -> Result<(T, usize)>
469 where
470 F: FnOnce(&[u8]) -> Result<(T, usize)>,
471 {
472 use crate::parsing::rar5::VintReader;
473
474 if data.len() < 16 {
475 return Err(RarError::InvalidHeader);
476 }
477
478 let mut iv = [0u8; 16];
480 iv.copy_from_slice(&data[..16]);
481
482 let encrypted_start = 16;
486
487 let available = data.len().saturating_sub(encrypted_start);
489 if available < 16 {
490 return Err(RarError::InvalidHeader);
491 }
492
493 let decrypt_len = (available.min(512) / 16) * 16;
495 if decrypt_len == 0 {
496 return Err(RarError::InvalidHeader);
497 }
498
499 let mut decrypted = data[encrypted_start..encrypted_start + decrypt_len].to_vec();
500 crypto
501 .decrypt(&iv, &mut decrypted)
502 .map_err(|e| RarError::DecryptionFailed(e.to_string()))?;
503
504 let (result, _) = parser(&decrypted)?;
506
507 let mut reader = VintReader::new(&decrypted[4..]); let header_size = reader.read().ok_or(RarError::InvalidHeader)?;
511 let size_vint_len = reader.position();
512
513 let plaintext_size = 4 + size_vint_len + header_size as usize;
515 let encrypted_size = plaintext_size.div_ceil(16) * 16;
516
517 Ok((result, 16 + encrypted_size))
519 }
520
521 async fn parse_rar5_file(
523 &self,
524 rar_file: &Arc<dyn FileMedia>,
525 opts: &ParseOptions,
526 ) -> Result<Vec<ParsedChunk>> {
527 let mut chunks = Vec::new();
528 let mut offset = 8u64; let header_buf = rar_file
532 .read_range(ReadInterval {
533 start: offset,
534 end: (offset + 256 - 1).min(rar_file.length() - 1),
535 })
536 .await?;
537
538 #[cfg(feature = "crypto")]
540 let header_crypto: Option<crate::crypto::Rar5Crypto> =
541 if Rar5EncryptionHeaderParser::is_encryption_header(&header_buf) {
542 let (enc_header, consumed) = Rar5EncryptionHeaderParser::parse(&header_buf)?;
543 offset += consumed as u64;
544
545 let password = opts.password.as_ref().ok_or(RarError::PasswordRequired)?;
547
548 Some(crate::crypto::Rar5Crypto::derive_key(
549 password,
550 &enc_header.salt,
551 enc_header.lg2_count,
552 ))
553 } else {
554 None
555 };
556
557 #[cfg(not(feature = "crypto"))]
558 if Rar5EncryptionHeaderParser::is_encryption_header(&header_buf) {
559 return Err(RarError::PasswordRequired);
560 }
561
562 #[cfg(feature = "crypto")]
564 let (archive_header, consumed) = if let Some(ref crypto) = header_crypto {
565 let enc_buf = rar_file
567 .read_range(ReadInterval {
568 start: offset,
569 end: (offset + 512 - 1).min(rar_file.length() - 1),
570 })
571 .await?;
572
573 self.parse_encrypted_header(&enc_buf, crypto, |data| {
574 Rar5ArchiveHeaderParser::parse(data)
575 })?
576 } else {
577 Rar5ArchiveHeaderParser::parse(&header_buf)?
578 };
579
580 #[cfg(not(feature = "crypto"))]
581 let (archive_header, consumed) = Rar5ArchiveHeaderParser::parse(&header_buf)?;
582
583 let is_solid = archive_header.archive_flags.is_solid;
584 offset += consumed as u64;
585
586 let mut file_count = 0usize;
587 let mut retrieved_count = 0usize;
588
589 while offset < rar_file.length().saturating_sub(16) {
591 let bytes_available = rar_file.length().saturating_sub(offset);
593 let read_size = 512u64.min(bytes_available);
594
595 if read_size < 16 {
596 break;
597 }
598
599 let header_buf = rar_file
600 .read_range(ReadInterval {
601 start: offset,
602 end: offset + read_size - 1,
603 })
604 .await?;
605
606 #[cfg(feature = "crypto")]
608 let (file_header, header_consumed) = if let Some(ref crypto) = header_crypto {
609 match self.parse_encrypted_header(&header_buf, crypto, |data| {
610 Rar5FileHeaderParser::parse(data)
611 }) {
612 Ok(h) => h,
613 Err(_) => break,
614 }
615 } else {
616 match Rar5FileHeaderParser::parse(&header_buf) {
617 Ok(h) => h,
618 Err(_) => break,
619 }
620 };
621
622 #[cfg(not(feature = "crypto"))]
623 let (file_header, header_consumed) = match Rar5FileHeaderParser::parse(&header_buf) {
624 Ok(h) => h,
625 Err(_) => break,
626 };
627
628 let data_start = offset + header_consumed as u64;
629 let data_end = if file_header.packed_size > 0 {
630 data_start + file_header.packed_size - 1
631 } else {
632 data_start
633 };
634
635 let include = match &opts.filter {
637 Some(f) => f(&file_header.name, file_count),
638 None => true,
639 };
640
641 if include {
642 let chunk = RarFileChunk::new(rar_file.clone(), data_start, data_end);
643 let chunk_size = file_header.packed_size;
644
645 let method = file_header.compression.method;
649
650 #[cfg(feature = "crypto")]
652 let encryption = if file_header.is_encrypted() {
653 file_header.encryption_info().and_then(|data| {
654 crate::crypto::Rar5EncryptionInfo::parse(data)
655 .ok()
656 .map(|info| FileEncryptionInfo::Rar5 {
657 salt: info.salt,
658 init_v: info.init_v,
659 lg2_count: info.lg2_count,
660 })
661 })
662 } else {
663 None
664 };
665
666 chunks.push(ParsedChunk {
667 name: file_header.name.clone(),
668 chunk,
669 continues_in_next: file_header.continues_in_next(),
670 unpacked_size: file_header.unpacked_size,
671 chunk_size,
672 method,
673 dict_size_log: file_header.compression.dict_size_log,
674 rar_version: RarVersion::Rar5,
675 is_solid,
676 #[cfg(feature = "crypto")]
677 encryption,
678 });
679 retrieved_count += 1;
680
681 if let Some(max) = opts.max_files {
682 if retrieved_count >= max {
683 break;
684 }
685 }
686 }
687
688 offset = data_end + 1;
689 file_count += 1;
690 }
691
692 Ok(chunks)
693 }
694
695 pub async fn parse(&self, opts: ParseOptions) -> Result<Vec<InnerFile>> {
697 if self.files.is_empty() {
698 return Err(RarError::NoFilesFound);
699 }
700
701 let mut all_parsed: Vec<Vec<ParsedChunk>> = Vec::new();
702
703 let mut i = 0;
704 while i < self.files.len() {
705 let file = &self.files[i];
706 let chunks = self.parse_file(file, &opts).await?;
707
708 if chunks.is_empty() {
709 i += 1;
710 continue;
711 }
712
713 let last = chunks.last().unwrap();
715 let continues = last.continues_in_next;
716 let chunk_size = last.chunk_size;
717 let unpacked_size = last.unpacked_size;
718 let chunk_start = last.chunk.start_offset;
719 let chunk_end = last.chunk.end_offset;
720 let name = last.name.clone();
721 let rar_version = last.rar_version;
722 let is_solid = last.is_solid;
723
724 all_parsed.push(chunks);
725
726 if continues {
728 let mut remaining = unpacked_size.saturating_sub(chunk_size);
729 while remaining >= chunk_size && i + 1 < self.files.len() {
730 i += 1;
731 let next_file = &self.files[i];
732
733 let chunk = RarFileChunk::new(next_file.clone(), chunk_start, chunk_end);
735 all_parsed.push(vec![ParsedChunk {
736 name: name.clone(),
737 chunk,
738 continues_in_next: false,
739 unpacked_size,
740 chunk_size,
741 method: 0x30, dict_size_log: 22, rar_version,
744 is_solid,
745 #[cfg(feature = "crypto")]
746 encryption: None, }]);
748 remaining = remaining.saturating_sub(chunk_size);
749 }
750 }
751
752 i += 1;
753 }
754
755 let all_chunks: Vec<ParsedChunk> = all_parsed.into_iter().flatten().collect();
757
758 #[cfg(feature = "crypto")]
759 type GroupValue = (
760 Vec<RarFileChunk>,
761 u8,
762 u8, u64,
764 RarVersion,
765 bool, Option<FileEncryptionInfo>,
767 );
768 #[cfg(not(feature = "crypto"))]
769 type GroupValue = (Vec<RarFileChunk>, u8, u8, u64, RarVersion, bool);
770
771 let mut grouped: HashMap<String, GroupValue> = HashMap::new();
772 for chunk in all_chunks {
773 #[cfg(feature = "crypto")]
774 let entry = grouped.entry(chunk.name).or_insert_with(|| {
775 (
776 Vec::new(),
777 chunk.method,
778 chunk.dict_size_log,
779 chunk.unpacked_size,
780 chunk.rar_version,
781 chunk.is_solid,
782 chunk.encryption,
783 )
784 });
785 #[cfg(not(feature = "crypto"))]
786 let entry = grouped.entry(chunk.name).or_insert_with(|| {
787 (
788 Vec::new(),
789 chunk.method,
790 chunk.dict_size_log,
791 chunk.unpacked_size,
792 chunk.rar_version,
793 chunk.is_solid,
794 )
795 });
796 entry.0.push(chunk.chunk);
797 }
798
799 #[cfg(feature = "crypto")]
801 let password = opts.password.clone();
802
803 let inner_files: Vec<InnerFile> = grouped
804 .into_iter()
805 .map(|(name, value)| {
806 #[cfg(feature = "crypto")]
807 {
808 let (
809 chunks,
810 method,
811 dict_size_log,
812 unpacked_size,
813 rar_version,
814 is_solid,
815 encryption,
816 ) = value;
817 let enc_info = encryption.map(|e| match e {
818 FileEncryptionInfo::Rar5 {
819 salt,
820 init_v,
821 lg2_count,
822 } => crate::inner_file::EncryptionInfo::Rar5 {
823 salt,
824 init_v,
825 lg2_count,
826 },
827 FileEncryptionInfo::Rar4 { salt } => {
828 crate::inner_file::EncryptionInfo::Rar4 { salt }
829 }
830 });
831 InnerFile::new_encrypted_with_solid_dict(
832 name,
833 chunks,
834 method,
835 dict_size_log,
836 unpacked_size,
837 rar_version,
838 enc_info,
839 password.clone(),
840 is_solid,
841 )
842 }
843 #[cfg(not(feature = "crypto"))]
844 {
845 let (chunks, method, dict_size_log, unpacked_size, rar_version, is_solid) =
846 value;
847 InnerFile::new_with_solid_dict(
848 name,
849 chunks,
850 method,
851 dict_size_log,
852 unpacked_size,
853 rar_version,
854 is_solid,
855 )
856 }
857 })
858 .collect();
859
860 Ok(inner_files)
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867 use crate::file_media::{FileMedia, LocalFileMedia};
868
869 #[tokio::test]
870 #[cfg(feature = "async")]
871 async fn test_get_archive_info_rar5() {
872 let file: Arc<dyn FileMedia> =
873 Arc::new(LocalFileMedia::new("__fixtures__/rar5/test.rar").unwrap());
874 let package = RarFilesPackage::new(vec![file]);
875
876 let info = package.get_archive_info().await.unwrap();
877 assert_eq!(info.version, RarVersion::Rar5);
878 assert!(!info.is_multivolume);
879 }
880
881 #[tokio::test]
882 #[cfg(feature = "async")]
883 async fn test_get_archive_info_rar4() {
884 let file: Arc<dyn FileMedia> =
885 Arc::new(LocalFileMedia::new("__fixtures__/single/single.rar").unwrap());
886 let package = RarFilesPackage::new(vec![file]);
887
888 let info = package.get_archive_info().await.unwrap();
889 assert_eq!(info.version, RarVersion::Rar4);
890 assert!(!info.is_multivolume);
891 }
892
893 #[tokio::test]
894 #[cfg(feature = "async")]
895 async fn test_parse_rar5_stored() {
896 let file: Arc<dyn FileMedia> =
898 Arc::new(LocalFileMedia::new("__fixtures__/rar5/test.rar").unwrap());
899 let package = RarFilesPackage::new(vec![file]);
900
901 let files = package.parse(ParseOptions::default()).await.unwrap();
902
903 assert_eq!(files.len(), 1);
904 assert_eq!(files[0].name, "test.txt");
905 }
906
907 #[tokio::test]
908 #[cfg(feature = "async")]
909 async fn test_parse_rar5_compressed() {
910 let file: Arc<dyn FileMedia> =
912 Arc::new(LocalFileMedia::new("__fixtures__/rar5/compressed.rar").unwrap());
913 let package = RarFilesPackage::new(vec![file]);
914
915 let files = package.parse(ParseOptions::default()).await.unwrap();
916
917 assert_eq!(files.len(), 1);
918 assert_eq!(files[0].name, "compress_test.txt");
919 assert_eq!(files[0].length, 152); match files[0].read_to_end().await {
924 Ok(content) => {
925 eprintln!("Got {} bytes of output", content.len());
926 eprintln!("First 32 bytes: {:02x?}", &content[..32.min(content.len())]);
927
928 assert_eq!(
930 content.len(),
931 152,
932 "decompressed size should match unpacked size"
933 );
934
935 match std::str::from_utf8(&content) {
937 Ok(text) => {
938 assert!(
939 text.contains("This is a test file"),
940 "content should contain expected text"
941 );
942 assert!(
943 text.contains("hello hello"),
944 "content should contain repeated text"
945 );
946 }
947 Err(_) => {
948 eprintln!(
950 "RAR5 decompression output is not valid UTF-8 (work in progress)"
951 );
952 }
953 }
954 }
955 Err(e) => {
956 eprintln!("RAR5 decompression error: {:?}", e);
958 }
959 }
960 }
961
962 #[tokio::test]
963 #[cfg(feature = "async")]
964 async fn test_parse_rar5_multivolume() {
965 let fixture_dir = "__fixtures__/rar5-multivolume";
967
968 let mut volume_paths: Vec<String> = std::fs::read_dir(fixture_dir)
970 .unwrap()
971 .filter_map(|e| e.ok())
972 .map(|e| e.path())
973 .filter(|p| p.extension().map_or(false, |ext| ext == "rar"))
974 .map(|p| p.to_string_lossy().to_string())
975 .collect();
976
977 volume_paths.sort();
979
980 if volume_paths.is_empty() {
981 eprintln!("Skipping test - no multi-volume fixtures found");
983 return;
984 }
985
986 eprintln!("Found {} volumes: {:?}", volume_paths.len(), volume_paths);
987
988 let files: Vec<Arc<dyn FileMedia>> = volume_paths
990 .iter()
991 .map(|p| Arc::new(LocalFileMedia::new(p).unwrap()) as Arc<dyn FileMedia>)
992 .collect();
993
994 let package = RarFilesPackage::new(files);
995
996 let parsed = package.parse(ParseOptions::default()).await.unwrap();
997
998 assert_eq!(parsed.len(), 1, "should have 1 inner file");
999 assert_eq!(parsed[0].name, "testfile.txt");
1000
1001 eprintln!("Parsed length: {}", parsed[0].length);
1004
1005 let content = parsed[0].read_to_end().await.unwrap();
1007 eprintln!("Read content length: {}", content.len());
1008
1009 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1011 assert!(text.contains("Line 1:"), "should contain first line");
1012 assert!(text.contains("Line 100:"), "should contain last line");
1013
1014 assert!(content.len() >= 11000, "should have at least 11000 bytes");
1016 }
1017
1018 #[tokio::test]
1019 #[cfg(all(feature = "async", feature = "crypto"))]
1020 async fn test_parse_rar5_encrypted_stored() {
1021 let fixture = "__fixtures__/encrypted/rar5-encrypted-stored.rar";
1023
1024 if !std::path::Path::new(fixture).exists() {
1025 eprintln!("Skipping test - encrypted fixtures not found");
1026 return;
1027 }
1028
1029 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1030 let package = RarFilesPackage::new(vec![file]);
1031
1032 let opts = ParseOptions {
1033 password: Some("testpass".to_string()),
1034 ..Default::default()
1035 };
1036
1037 let parsed = package.parse(opts).await.unwrap();
1038 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1039
1040 let inner_file = &parsed[0];
1041 assert_eq!(inner_file.name, "testfile.txt");
1042 assert!(inner_file.is_encrypted());
1043
1044 let content = inner_file.read_decompressed().await.unwrap();
1046 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1047
1048 assert!(text.starts_with("Hello, encrypted world!"));
1049 }
1050
1051 #[tokio::test]
1052 #[cfg(all(feature = "async", feature = "crypto"))]
1053 async fn test_parse_rar5_encrypted_no_password() {
1054 let fixture = "__fixtures__/encrypted/rar5-encrypted-stored.rar";
1055
1056 if !std::path::Path::new(fixture).exists() {
1057 eprintln!("Skipping test - encrypted fixtures not found");
1058 return;
1059 }
1060
1061 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1062 let package = RarFilesPackage::new(vec![file]);
1063
1064 let parsed = package.parse(ParseOptions::default()).await.unwrap();
1066 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1067
1068 let inner_file = &parsed[0];
1069 assert!(inner_file.is_encrypted());
1070
1071 let result = inner_file.read_decompressed().await;
1073 assert!(result.is_err());
1074 match result {
1075 Err(crate::RarError::PasswordRequired) => {
1076 }
1078 Err(e) => panic!("Expected PasswordRequired error, got: {:?}", e),
1079 Ok(_) => panic!("Expected error but got success"),
1080 }
1081 }
1082
1083 #[tokio::test]
1084 #[cfg(all(feature = "async", feature = "crypto"))]
1085 async fn test_parse_rar5_encrypted_headers() {
1086 let fixture = "__fixtures__/encrypted/rar5-encrypted-headers.rar";
1088
1089 if !std::path::Path::new(fixture).exists() {
1090 eprintln!("Skipping test - encrypted headers fixture not found");
1091 return;
1092 }
1093
1094 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1095 let package = RarFilesPackage::new(vec![file]);
1096
1097 let info = package.get_archive_info().await.unwrap();
1099 assert!(info.has_encrypted_headers, "should have encrypted headers");
1100 assert_eq!(info.version, RarVersion::Rar5);
1101
1102 let result = package.parse(ParseOptions::default()).await;
1104 assert!(
1105 matches!(result, Err(RarError::PasswordRequired)),
1106 "should require password for encrypted headers, got {:?}",
1107 result
1108 );
1109
1110 let opts = ParseOptions {
1112 password: Some("testpass".to_string()),
1113 ..Default::default()
1114 };
1115
1116 let parsed = package.parse(opts).await.unwrap();
1117 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1118 assert_eq!(parsed[0].name, "testfile.txt");
1119
1120 let content = parsed[0].read_decompressed().await.unwrap();
1122 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1123 assert!(
1124 text.starts_with("Hello, encrypted world!"),
1125 "content was: {:?}",
1126 text
1127 );
1128 }
1129
1130 #[tokio::test]
1131 #[cfg(all(feature = "async", feature = "crypto"))]
1132 async fn test_get_archive_info_encrypted_headers() {
1133 let fixture = "__fixtures__/encrypted/rar5-encrypted-headers.rar";
1135
1136 if !std::path::Path::new(fixture).exists() {
1137 eprintln!("Skipping test - encrypted headers fixture not found");
1138 return;
1139 }
1140
1141 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1142 let package = RarFilesPackage::new(vec![file]);
1143
1144 let info = package.get_archive_info().await.unwrap();
1145 assert!(info.has_encrypted_headers);
1146 assert_eq!(info.version, RarVersion::Rar5);
1147 }
1149
1150 #[tokio::test]
1151 #[cfg(all(feature = "async", feature = "crypto"))]
1152 async fn test_parse_rar4_encrypted_stored() {
1153 let fixture = "__fixtures__/encrypted/rar4-encrypted-stored.rar";
1155
1156 if !std::path::Path::new(fixture).exists() {
1157 eprintln!("Skipping test - RAR4 encrypted fixtures not found");
1158 return;
1159 }
1160
1161 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1162 let package = RarFilesPackage::new(vec![file]);
1163
1164 let info = package.get_archive_info().await.unwrap();
1166 assert_eq!(info.version, RarVersion::Rar4);
1167
1168 let opts = ParseOptions {
1169 password: Some("testpass".to_string()),
1170 ..Default::default()
1171 };
1172
1173 let parsed = package.parse(opts).await.unwrap();
1174 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1175
1176 let inner_file = &parsed[0];
1177 assert_eq!(inner_file.name, "testfile.txt");
1178 assert!(inner_file.is_encrypted());
1179
1180 let content = inner_file.read_decompressed().await.unwrap();
1182 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1183
1184 assert!(
1185 text.starts_with("Hello, encrypted world!"),
1186 "content was: {:?}",
1187 text
1188 );
1189 }
1190
1191 #[tokio::test]
1192 #[cfg(all(feature = "async", feature = "crypto"))]
1193 async fn test_parse_rar4_encrypted_compressed() {
1194 let fixture = "__fixtures__/encrypted/rar4-encrypted.rar";
1196
1197 if !std::path::Path::new(fixture).exists() {
1198 eprintln!("Skipping test - RAR4 encrypted fixtures not found");
1199 return;
1200 }
1201
1202 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1203 let package = RarFilesPackage::new(vec![file]);
1204
1205 let info = package.get_archive_info().await.unwrap();
1207 assert_eq!(info.version, RarVersion::Rar4);
1208
1209 let opts = ParseOptions {
1210 password: Some("testpass".to_string()),
1211 ..Default::default()
1212 };
1213
1214 let parsed = package.parse(opts).await.unwrap();
1215 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1216
1217 let inner_file = &parsed[0];
1218 assert_eq!(inner_file.name, "testfile.txt");
1219 assert!(inner_file.is_encrypted());
1220
1221 let content = inner_file.read_decompressed().await.unwrap();
1223 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1224
1225 assert!(
1226 text.starts_with("Hello, encrypted world!"),
1227 "content was: {:?}",
1228 text
1229 );
1230 }
1231}