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 = data_start + file_header.packed_size - 1;
405
406 let include = match &opts.filter {
408 Some(f) => f(&file_header.name, file_count),
409 None => true,
410 };
411
412 if include {
413 let chunk = RarFileChunk::new(rar_file.clone(), data_start, data_end);
414 let chunk_size = chunk.length();
415
416 #[cfg(feature = "crypto")]
418 let encryption = if file_header.is_encrypted {
419 file_header
420 .salt
421 .map(|salt| FileEncryptionInfo::Rar4 { salt })
422 } else {
423 None
424 };
425
426 chunks.push(ParsedChunk {
427 name: file_header.name.clone(),
428 chunk,
429 continues_in_next: file_header.continues_in_next,
430 unpacked_size: file_header.unpacked_size,
431 chunk_size,
432 method: file_header.method,
433 dict_size_log: 22, rar_version: RarVersion::Rar4,
435 is_solid,
436 #[cfg(feature = "crypto")]
437 encryption,
438 });
439 retrieved_count += 1;
440
441 if let Some(max) = opts.max_files {
443 if retrieved_count >= max {
444 break;
445 }
446 }
447 }
448
449 offset = data_end + 1;
450 file_count += 1;
451 }
452
453 Ok(chunks)
454 }
455
456 #[cfg(feature = "crypto")]
459 fn parse_encrypted_header<T, F>(
460 &self,
461 data: &[u8],
462 crypto: &crate::crypto::Rar5Crypto,
463 parser: F,
464 ) -> Result<(T, usize)>
465 where
466 F: FnOnce(&[u8]) -> Result<(T, usize)>,
467 {
468 use crate::parsing::rar5::VintReader;
469
470 if data.len() < 16 {
471 return Err(RarError::InvalidHeader);
472 }
473
474 let mut iv = [0u8; 16];
476 iv.copy_from_slice(&data[..16]);
477
478 let encrypted_start = 16;
482
483 let available = data.len().saturating_sub(encrypted_start);
485 if available < 16 {
486 return Err(RarError::InvalidHeader);
487 }
488
489 let decrypt_len = (available.min(512) / 16) * 16;
491 if decrypt_len == 0 {
492 return Err(RarError::InvalidHeader);
493 }
494
495 let mut decrypted = data[encrypted_start..encrypted_start + decrypt_len].to_vec();
496 crypto
497 .decrypt(&iv, &mut decrypted)
498 .map_err(|e| RarError::DecryptionFailed(e.to_string()))?;
499
500 let (result, _) = parser(&decrypted)?;
502
503 let mut reader = VintReader::new(&decrypted[4..]); let header_size = reader.read().ok_or(RarError::InvalidHeader)?;
507 let size_vint_len = reader.position();
508
509 let plaintext_size = 4 + size_vint_len + header_size as usize;
511 let encrypted_size = plaintext_size.div_ceil(16) * 16;
512
513 Ok((result, 16 + encrypted_size))
515 }
516
517 async fn parse_rar5_file(
519 &self,
520 rar_file: &Arc<dyn FileMedia>,
521 opts: &ParseOptions,
522 ) -> Result<Vec<ParsedChunk>> {
523 let mut chunks = Vec::new();
524 let mut offset = 8u64; let header_buf = rar_file
528 .read_range(ReadInterval {
529 start: offset,
530 end: (offset + 256 - 1).min(rar_file.length() - 1),
531 })
532 .await?;
533
534 #[cfg(feature = "crypto")]
536 let header_crypto: Option<crate::crypto::Rar5Crypto> =
537 if Rar5EncryptionHeaderParser::is_encryption_header(&header_buf) {
538 let (enc_header, consumed) = Rar5EncryptionHeaderParser::parse(&header_buf)?;
539 offset += consumed as u64;
540
541 let password = opts.password.as_ref().ok_or(RarError::PasswordRequired)?;
543
544 Some(crate::crypto::Rar5Crypto::derive_key(
545 password,
546 &enc_header.salt,
547 enc_header.lg2_count,
548 ))
549 } else {
550 None
551 };
552
553 #[cfg(not(feature = "crypto"))]
554 if Rar5EncryptionHeaderParser::is_encryption_header(&header_buf) {
555 return Err(RarError::PasswordRequired);
556 }
557
558 #[cfg(feature = "crypto")]
560 let (archive_header, consumed) = if let Some(ref crypto) = header_crypto {
561 let enc_buf = rar_file
563 .read_range(ReadInterval {
564 start: offset,
565 end: (offset + 512 - 1).min(rar_file.length() - 1),
566 })
567 .await?;
568
569 self.parse_encrypted_header(&enc_buf, crypto, |data| {
570 Rar5ArchiveHeaderParser::parse(data)
571 })?
572 } else {
573 Rar5ArchiveHeaderParser::parse(&header_buf)?
574 };
575
576 #[cfg(not(feature = "crypto"))]
577 let (archive_header, consumed) = Rar5ArchiveHeaderParser::parse(&header_buf)?;
578
579 let is_solid = archive_header.archive_flags.is_solid;
580 offset += consumed as u64;
581
582 let mut file_count = 0usize;
583 let mut retrieved_count = 0usize;
584
585 while offset < rar_file.length().saturating_sub(16) {
587 let bytes_available = rar_file.length().saturating_sub(offset);
589 let read_size = 512u64.min(bytes_available);
590
591 if read_size < 16 {
592 break;
593 }
594
595 let header_buf = rar_file
596 .read_range(ReadInterval {
597 start: offset,
598 end: offset + read_size - 1,
599 })
600 .await?;
601
602 #[cfg(feature = "crypto")]
604 let (file_header, header_consumed) = if let Some(ref crypto) = header_crypto {
605 match self.parse_encrypted_header(&header_buf, crypto, |data| {
606 Rar5FileHeaderParser::parse(data)
607 }) {
608 Ok(h) => h,
609 Err(_) => break,
610 }
611 } else {
612 match Rar5FileHeaderParser::parse(&header_buf) {
613 Ok(h) => h,
614 Err(_) => break,
615 }
616 };
617
618 #[cfg(not(feature = "crypto"))]
619 let (file_header, header_consumed) = match Rar5FileHeaderParser::parse(&header_buf) {
620 Ok(h) => h,
621 Err(_) => break,
622 };
623
624 let data_start = offset + header_consumed as u64;
625 let data_end = data_start + file_header.packed_size - 1;
626
627 let include = match &opts.filter {
629 Some(f) => f(&file_header.name, file_count),
630 None => true,
631 };
632
633 if include {
634 let chunk = RarFileChunk::new(rar_file.clone(), data_start, data_end);
635 let chunk_size = file_header.packed_size;
636
637 let method = file_header.compression.method;
641
642 #[cfg(feature = "crypto")]
644 let encryption = if file_header.is_encrypted() {
645 file_header.encryption_info().and_then(|data| {
646 crate::crypto::Rar5EncryptionInfo::parse(data)
647 .ok()
648 .map(|info| FileEncryptionInfo::Rar5 {
649 salt: info.salt,
650 init_v: info.init_v,
651 lg2_count: info.lg2_count,
652 })
653 })
654 } else {
655 None
656 };
657
658 chunks.push(ParsedChunk {
659 name: file_header.name.clone(),
660 chunk,
661 continues_in_next: file_header.continues_in_next(),
662 unpacked_size: file_header.unpacked_size,
663 chunk_size,
664 method,
665 dict_size_log: file_header.compression.dict_size_log,
666 rar_version: RarVersion::Rar5,
667 is_solid,
668 #[cfg(feature = "crypto")]
669 encryption,
670 });
671 retrieved_count += 1;
672
673 if let Some(max) = opts.max_files {
674 if retrieved_count >= max {
675 break;
676 }
677 }
678 }
679
680 offset = data_end + 1;
681 file_count += 1;
682 }
683
684 Ok(chunks)
685 }
686
687 pub async fn parse(&self, opts: ParseOptions) -> Result<Vec<InnerFile>> {
689 if self.files.is_empty() {
690 return Err(RarError::NoFilesFound);
691 }
692
693 let mut all_parsed: Vec<Vec<ParsedChunk>> = Vec::new();
694
695 let mut i = 0;
696 while i < self.files.len() {
697 let file = &self.files[i];
698 let chunks = self.parse_file(file, &opts).await?;
699
700 if chunks.is_empty() {
701 i += 1;
702 continue;
703 }
704
705 let last = chunks.last().unwrap();
707 let continues = last.continues_in_next;
708 let chunk_size = last.chunk_size;
709 let unpacked_size = last.unpacked_size;
710 let chunk_start = last.chunk.start_offset;
711 let chunk_end = last.chunk.end_offset;
712 let name = last.name.clone();
713 let rar_version = last.rar_version;
714 let is_solid = last.is_solid;
715
716 all_parsed.push(chunks);
717
718 if continues {
720 let mut remaining = unpacked_size.saturating_sub(chunk_size);
721 while remaining >= chunk_size && i + 1 < self.files.len() {
722 i += 1;
723 let next_file = &self.files[i];
724
725 let chunk = RarFileChunk::new(next_file.clone(), chunk_start, chunk_end);
727 all_parsed.push(vec![ParsedChunk {
728 name: name.clone(),
729 chunk,
730 continues_in_next: false,
731 unpacked_size,
732 chunk_size,
733 method: 0x30, dict_size_log: 22, rar_version,
736 is_solid,
737 #[cfg(feature = "crypto")]
738 encryption: None, }]);
740 remaining = remaining.saturating_sub(chunk_size);
741 }
742 }
743
744 i += 1;
745 }
746
747 let all_chunks: Vec<ParsedChunk> = all_parsed.into_iter().flatten().collect();
749
750 #[cfg(feature = "crypto")]
751 type GroupValue = (
752 Vec<RarFileChunk>,
753 u8,
754 u8, u64,
756 RarVersion,
757 bool, Option<FileEncryptionInfo>,
759 );
760 #[cfg(not(feature = "crypto"))]
761 type GroupValue = (Vec<RarFileChunk>, u8, u8, u64, RarVersion, bool);
762
763 let mut grouped: HashMap<String, GroupValue> = HashMap::new();
764 for chunk in all_chunks {
765 #[cfg(feature = "crypto")]
766 let entry = grouped.entry(chunk.name).or_insert_with(|| {
767 (
768 Vec::new(),
769 chunk.method,
770 chunk.dict_size_log,
771 chunk.unpacked_size,
772 chunk.rar_version,
773 chunk.is_solid,
774 chunk.encryption,
775 )
776 });
777 #[cfg(not(feature = "crypto"))]
778 let entry = grouped.entry(chunk.name).or_insert_with(|| {
779 (
780 Vec::new(),
781 chunk.method,
782 chunk.dict_size_log,
783 chunk.unpacked_size,
784 chunk.rar_version,
785 chunk.is_solid,
786 )
787 });
788 entry.0.push(chunk.chunk);
789 }
790
791 #[cfg(feature = "crypto")]
793 let password = opts.password.clone();
794
795 let inner_files: Vec<InnerFile> = grouped
796 .into_iter()
797 .map(|(name, value)| {
798 #[cfg(feature = "crypto")]
799 {
800 let (
801 chunks,
802 method,
803 dict_size_log,
804 unpacked_size,
805 rar_version,
806 is_solid,
807 encryption,
808 ) = value;
809 let enc_info = encryption.map(|e| match e {
810 FileEncryptionInfo::Rar5 {
811 salt,
812 init_v,
813 lg2_count,
814 } => crate::inner_file::EncryptionInfo::Rar5 {
815 salt,
816 init_v,
817 lg2_count,
818 },
819 FileEncryptionInfo::Rar4 { salt } => {
820 crate::inner_file::EncryptionInfo::Rar4 { salt }
821 }
822 });
823 InnerFile::new_encrypted_with_solid_dict(
824 name,
825 chunks,
826 method,
827 dict_size_log,
828 unpacked_size,
829 rar_version,
830 enc_info,
831 password.clone(),
832 is_solid,
833 )
834 }
835 #[cfg(not(feature = "crypto"))]
836 {
837 let (chunks, method, dict_size_log, unpacked_size, rar_version, is_solid) =
838 value;
839 InnerFile::new_with_solid_dict(
840 name,
841 chunks,
842 method,
843 dict_size_log,
844 unpacked_size,
845 rar_version,
846 is_solid,
847 )
848 }
849 })
850 .collect();
851
852 Ok(inner_files)
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use crate::file_media::{FileMedia, LocalFileMedia};
860
861 #[tokio::test]
862 #[cfg(feature = "async")]
863 async fn test_get_archive_info_rar5() {
864 let file: Arc<dyn FileMedia> =
865 Arc::new(LocalFileMedia::new("__fixtures__/rar5/test.rar").unwrap());
866 let package = RarFilesPackage::new(vec![file]);
867
868 let info = package.get_archive_info().await.unwrap();
869 assert_eq!(info.version, RarVersion::Rar5);
870 assert!(!info.is_multivolume);
871 }
872
873 #[tokio::test]
874 #[cfg(feature = "async")]
875 async fn test_get_archive_info_rar4() {
876 let file: Arc<dyn FileMedia> =
877 Arc::new(LocalFileMedia::new("__fixtures__/single/single.rar").unwrap());
878 let package = RarFilesPackage::new(vec![file]);
879
880 let info = package.get_archive_info().await.unwrap();
881 assert_eq!(info.version, RarVersion::Rar4);
882 assert!(!info.is_multivolume);
883 }
884
885 #[tokio::test]
886 #[cfg(feature = "async")]
887 async fn test_parse_rar5_stored() {
888 let file: Arc<dyn FileMedia> =
890 Arc::new(LocalFileMedia::new("__fixtures__/rar5/test.rar").unwrap());
891 let package = RarFilesPackage::new(vec![file]);
892
893 let files = package.parse(ParseOptions::default()).await.unwrap();
894
895 assert_eq!(files.len(), 1);
896 assert_eq!(files[0].name, "test.txt");
897 }
898
899 #[tokio::test]
900 #[cfg(feature = "async")]
901 async fn test_parse_rar5_compressed() {
902 let file: Arc<dyn FileMedia> =
904 Arc::new(LocalFileMedia::new("__fixtures__/rar5/compressed.rar").unwrap());
905 let package = RarFilesPackage::new(vec![file]);
906
907 let files = package.parse(ParseOptions::default()).await.unwrap();
908
909 assert_eq!(files.len(), 1);
910 assert_eq!(files[0].name, "compress_test.txt");
911 assert_eq!(files[0].length, 152); match files[0].read_to_end().await {
916 Ok(content) => {
917 eprintln!("Got {} bytes of output", content.len());
918 eprintln!("First 32 bytes: {:02x?}", &content[..32.min(content.len())]);
919
920 assert_eq!(
922 content.len(),
923 152,
924 "decompressed size should match unpacked size"
925 );
926
927 match std::str::from_utf8(&content) {
929 Ok(text) => {
930 assert!(
931 text.contains("This is a test file"),
932 "content should contain expected text"
933 );
934 assert!(
935 text.contains("hello hello"),
936 "content should contain repeated text"
937 );
938 }
939 Err(_) => {
940 eprintln!(
942 "RAR5 decompression output is not valid UTF-8 (work in progress)"
943 );
944 }
945 }
946 }
947 Err(e) => {
948 eprintln!("RAR5 decompression error: {:?}", e);
950 }
951 }
952 }
953
954 #[tokio::test]
955 #[cfg(feature = "async")]
956 async fn test_parse_rar5_multivolume() {
957 let fixture_dir = "__fixtures__/rar5-multivolume";
959
960 let mut volume_paths: Vec<String> = std::fs::read_dir(fixture_dir)
962 .unwrap()
963 .filter_map(|e| e.ok())
964 .map(|e| e.path())
965 .filter(|p| p.extension().map_or(false, |ext| ext == "rar"))
966 .map(|p| p.to_string_lossy().to_string())
967 .collect();
968
969 volume_paths.sort();
971
972 if volume_paths.is_empty() {
973 eprintln!("Skipping test - no multi-volume fixtures found");
975 return;
976 }
977
978 eprintln!("Found {} volumes: {:?}", volume_paths.len(), volume_paths);
979
980 let files: Vec<Arc<dyn FileMedia>> = volume_paths
982 .iter()
983 .map(|p| Arc::new(LocalFileMedia::new(p).unwrap()) as Arc<dyn FileMedia>)
984 .collect();
985
986 let package = RarFilesPackage::new(files);
987
988 let parsed = package.parse(ParseOptions::default()).await.unwrap();
989
990 assert_eq!(parsed.len(), 1, "should have 1 inner file");
991 assert_eq!(parsed[0].name, "testfile.txt");
992
993 eprintln!("Parsed length: {}", parsed[0].length);
996
997 let content = parsed[0].read_to_end().await.unwrap();
999 eprintln!("Read content length: {}", content.len());
1000
1001 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1003 assert!(text.contains("Line 1:"), "should contain first line");
1004 assert!(text.contains("Line 100:"), "should contain last line");
1005
1006 assert!(content.len() >= 11000, "should have at least 11000 bytes");
1008 }
1009
1010 #[tokio::test]
1011 #[cfg(all(feature = "async", feature = "crypto"))]
1012 async fn test_parse_rar5_encrypted_stored() {
1013 let fixture = "__fixtures__/encrypted/rar5-encrypted-stored.rar";
1015
1016 if !std::path::Path::new(fixture).exists() {
1017 eprintln!("Skipping test - encrypted fixtures not found");
1018 return;
1019 }
1020
1021 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1022 let package = RarFilesPackage::new(vec![file]);
1023
1024 let opts = ParseOptions {
1025 password: Some("testpass".to_string()),
1026 ..Default::default()
1027 };
1028
1029 let parsed = package.parse(opts).await.unwrap();
1030 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1031
1032 let inner_file = &parsed[0];
1033 assert_eq!(inner_file.name, "testfile.txt");
1034 assert!(inner_file.is_encrypted());
1035
1036 let content = inner_file.read_decompressed().await.unwrap();
1038 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1039
1040 assert!(text.starts_with("Hello, encrypted world!"));
1041 }
1042
1043 #[tokio::test]
1044 #[cfg(all(feature = "async", feature = "crypto"))]
1045 async fn test_parse_rar5_encrypted_no_password() {
1046 let fixture = "__fixtures__/encrypted/rar5-encrypted-stored.rar";
1047
1048 if !std::path::Path::new(fixture).exists() {
1049 eprintln!("Skipping test - encrypted fixtures not found");
1050 return;
1051 }
1052
1053 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1054 let package = RarFilesPackage::new(vec![file]);
1055
1056 let parsed = package.parse(ParseOptions::default()).await.unwrap();
1058 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1059
1060 let inner_file = &parsed[0];
1061 assert!(inner_file.is_encrypted());
1062
1063 let result = inner_file.read_decompressed().await;
1065 assert!(result.is_err());
1066 match result {
1067 Err(crate::RarError::PasswordRequired) => {
1068 }
1070 Err(e) => panic!("Expected PasswordRequired error, got: {:?}", e),
1071 Ok(_) => panic!("Expected error but got success"),
1072 }
1073 }
1074
1075 #[tokio::test]
1076 #[cfg(all(feature = "async", feature = "crypto"))]
1077 async fn test_parse_rar5_encrypted_headers() {
1078 let fixture = "__fixtures__/encrypted/rar5-encrypted-headers.rar";
1080
1081 if !std::path::Path::new(fixture).exists() {
1082 eprintln!("Skipping test - encrypted headers fixture not found");
1083 return;
1084 }
1085
1086 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1087 let package = RarFilesPackage::new(vec![file]);
1088
1089 let info = package.get_archive_info().await.unwrap();
1091 assert!(info.has_encrypted_headers, "should have encrypted headers");
1092 assert_eq!(info.version, RarVersion::Rar5);
1093
1094 let result = package.parse(ParseOptions::default()).await;
1096 assert!(
1097 matches!(result, Err(RarError::PasswordRequired)),
1098 "should require password for encrypted headers, got {:?}",
1099 result
1100 );
1101
1102 let opts = ParseOptions {
1104 password: Some("testpass".to_string()),
1105 ..Default::default()
1106 };
1107
1108 let parsed = package.parse(opts).await.unwrap();
1109 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1110 assert_eq!(parsed[0].name, "testfile.txt");
1111
1112 let content = parsed[0].read_decompressed().await.unwrap();
1114 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1115 assert!(
1116 text.starts_with("Hello, encrypted world!"),
1117 "content was: {:?}",
1118 text
1119 );
1120 }
1121
1122 #[tokio::test]
1123 #[cfg(all(feature = "async", feature = "crypto"))]
1124 async fn test_get_archive_info_encrypted_headers() {
1125 let fixture = "__fixtures__/encrypted/rar5-encrypted-headers.rar";
1127
1128 if !std::path::Path::new(fixture).exists() {
1129 eprintln!("Skipping test - encrypted headers fixture not found");
1130 return;
1131 }
1132
1133 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1134 let package = RarFilesPackage::new(vec![file]);
1135
1136 let info = package.get_archive_info().await.unwrap();
1137 assert!(info.has_encrypted_headers);
1138 assert_eq!(info.version, RarVersion::Rar5);
1139 }
1141
1142 #[tokio::test]
1143 #[cfg(all(feature = "async", feature = "crypto"))]
1144 async fn test_parse_rar4_encrypted_stored() {
1145 let fixture = "__fixtures__/encrypted/rar4-encrypted-stored.rar";
1147
1148 if !std::path::Path::new(fixture).exists() {
1149 eprintln!("Skipping test - RAR4 encrypted fixtures not found");
1150 return;
1151 }
1152
1153 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1154 let package = RarFilesPackage::new(vec![file]);
1155
1156 let info = package.get_archive_info().await.unwrap();
1158 assert_eq!(info.version, RarVersion::Rar4);
1159
1160 let opts = ParseOptions {
1161 password: Some("testpass".to_string()),
1162 ..Default::default()
1163 };
1164
1165 let parsed = package.parse(opts).await.unwrap();
1166 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1167
1168 let inner_file = &parsed[0];
1169 assert_eq!(inner_file.name, "testfile.txt");
1170 assert!(inner_file.is_encrypted());
1171
1172 let content = inner_file.read_decompressed().await.unwrap();
1174 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1175
1176 assert!(
1177 text.starts_with("Hello, encrypted world!"),
1178 "content was: {:?}",
1179 text
1180 );
1181 }
1182
1183 #[tokio::test]
1184 #[cfg(all(feature = "async", feature = "crypto"))]
1185 async fn test_parse_rar4_encrypted_compressed() {
1186 let fixture = "__fixtures__/encrypted/rar4-encrypted.rar";
1188
1189 if !std::path::Path::new(fixture).exists() {
1190 eprintln!("Skipping test - RAR4 encrypted fixtures not found");
1191 return;
1192 }
1193
1194 let file: Arc<dyn FileMedia> = Arc::new(LocalFileMedia::new(fixture).unwrap());
1195 let package = RarFilesPackage::new(vec![file]);
1196
1197 let info = package.get_archive_info().await.unwrap();
1199 assert_eq!(info.version, RarVersion::Rar4);
1200
1201 let opts = ParseOptions {
1202 password: Some("testpass".to_string()),
1203 ..Default::default()
1204 };
1205
1206 let parsed = package.parse(opts).await.unwrap();
1207 assert_eq!(parsed.len(), 1, "should have 1 inner file");
1208
1209 let inner_file = &parsed[0];
1210 assert_eq!(inner_file.name, "testfile.txt");
1211 assert!(inner_file.is_encrypted());
1212
1213 let content = inner_file.read_decompressed().await.unwrap();
1215 let text = std::str::from_utf8(&content).expect("should be valid UTF-8");
1216
1217 assert!(
1218 text.starts_with("Hello, encrypted world!"),
1219 "content was: {:?}",
1220 text
1221 );
1222 }
1223}