unrar_ng/open_archive.rs
1use super::error::*;
2use super::*;
3use std::fmt;
4use std::os::raw::{c_int, c_uint};
5use std::path::{Path, PathBuf};
6use std::ptr::NonNull;
7
8bitflags::bitflags! {
9 #[derive(Debug, Default)]
10 struct ArchiveFlags: u32 {
11 const VOLUME = native::ROADF_VOLUME;
12 const COMMENT = native::ROADF_COMMENT;
13 const LOCK = native::ROADF_LOCK;
14 const SOLID = native::ROADF_SOLID;
15 const NEW_NUMBERING = native::ROADF_NEWNUMBERING;
16 const SIGNED = native::ROADF_SIGNED;
17 const RECOVERY = native::ROADF_RECOVERY;
18 const ENC_HEADERS = native::ROADF_ENCHEADERS;
19 const FIRST_VOLUME = native::ROADF_FIRSTVOLUME;
20 }
21}
22
23/// Volume information on the file that was *initially* opened.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum VolumeInfo {
26 /// the *initially* opened file is a single-part archive
27 None,
28 /// the *initially* opened file is the first volume in a multipart archive
29 First,
30 /// the *initially* opened file is any volume but the first in a multipart archive
31 Subsequent,
32}
33
34/// Extraction progress event for callbacks during batch extraction.
35///
36/// This enum is used with [`OpenArchive::extract_all_with_callback`] to receive
37/// notifications about extraction progress.
38#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub enum ExtractEvent {
41 /// File extraction is starting.
42 Start {
43 /// The filename being extracted (relative path within the archive)
44 filename: PathBuf,
45 /// The uncompressed size of the file in bytes
46 size: u64,
47 },
48 /// File extraction completed successfully.
49 Ok {
50 /// The filename that was extracted
51 filename: PathBuf,
52 },
53 /// File extraction failed.
54 Err {
55 /// The filename that failed to extract
56 filename: PathBuf,
57 /// The error code from the extraction
58 error_code: i32,
59 },
60 /// The archive requires a dictionary larger than the build-time limit.
61 ///
62 /// Surfaced from the upstream `UCM_LARGEDICT` callback. Returning
63 /// `true` permits the DLL to proceed; returning `false` lets the DLL
64 /// fail the operation, which the caller then observes as
65 /// `Err(UnrarError { code:` [`Code::LargeDict`](crate::error::Code::LargeDict)`, when: When::Process })`.
66 LargeDictWarning {
67 /// The dictionary size required by the archive, in kilobytes.
68 dict_size_kb: u64,
69 /// The maximum dictionary size this build supports, in kilobytes.
70 max_dict_size_kb: u64,
71 },
72}
73
74/// Outcome of [`OpenArchive::extract_all_with_callback`].
75///
76/// The DLL maps a user-initiated cancel (the callback returning `false`
77/// from `Start`/`Ok`/`Err`) to `ERAR_SUCCESS`, so without this status
78/// the caller cannot tell whether the archive finished or was stopped
79/// early. Pattern-match to distinguish the two — `Completed` means the
80/// DLL exhausted the archive, `Cancelled` means the callback aborted
81/// the run.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83#[non_exhaustive]
84pub enum ExtractStatus {
85 /// Extraction ran to the end of the archive.
86 Completed,
87 /// The user callback returned `false` and aborted extraction early.
88 Cancelled,
89}
90
91#[derive(Debug)]
92struct Handle(NonNull<native::Handle>);
93
94impl Drop for Handle {
95 fn drop(&mut self) {
96 unsafe { native::RARCloseArchive(self.0.as_ptr() as *const _) };
97 }
98}
99
100/// An open RAR archive that can be read or processed.
101///
102/// See the [OpenArchive chapter](index.html#openarchive) for more information.
103#[derive(Debug)]
104pub struct OpenArchive<M: OpenMode, C: Cursor> {
105 handle: Handle,
106 flags: ArchiveFlags,
107 damaged: bool,
108 extra: C,
109 marker: std::marker::PhantomData<M>,
110}
111type Userdata<T> = (T, Option<widestring::WideCString>);
112
113mod private {
114 use super::native;
115 pub trait Sealed {}
116 impl Sealed for super::CursorBeforeHeader {}
117 impl Sealed for super::CursorBeforeFile {}
118 impl Sealed for super::List {}
119 impl Sealed for super::ListSplit {}
120 impl Sealed for super::Process {}
121
122 #[repr(i32)]
123 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
124 pub(crate) enum Operation {
125 Skip = native::RAR_SKIP,
126 Test = native::RAR_TEST,
127 Extract = native::RAR_EXTRACT,
128 }
129
130 #[repr(u32)]
131 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
132 pub enum OpenModeValue {
133 Extract = native::RAR_OM_EXTRACT,
134 List = native::RAR_OM_LIST,
135 ListIncSplit = native::RAR_OM_LIST_INCSPLIT,
136 }
137}
138
139/// Type parameter for OpenArchive denoting a `read_header` operation must follow next.
140///
141/// See the chapter [OpenArchive: Cursors](index.html#openarchive-cursors) for more information.
142#[derive(Debug)]
143pub struct CursorBeforeHeader;
144/// Type parameter for OpenArchive denoting a `process_file` operation must follow next.
145///
146/// See the chapter [OpenArchive: Cursors](index.html#openarchive-cursors) for more information.
147#[derive(Debug)]
148pub struct CursorBeforeFile {
149 header: FileHeader,
150}
151
152/// The Cursor trait enables archives to keep track of their state.
153///
154/// See the chapter [OpenArchive: Cursors](index.html#openarchive-cursors) for more information.
155pub trait Cursor: private::Sealed {}
156impl Cursor for CursorBeforeHeader {}
157impl Cursor for CursorBeforeFile {}
158
159/// An OpenMode for processing RAR archive entries.
160///
161/// Process allows more sophisticated operations in the `ProcessFile` step.
162#[derive(Debug)]
163pub struct Process;
164#[derive(Debug)]
165/// An OpenMode for listing RAR archive entries.
166///
167/// List mode will list all entries. The payload itself cannot be processed and instead can only
168/// be skipped over. This will yield one header per individual file, regardless of how many parts
169/// the file is split across.
170pub struct List;
171/// An OpenMode for listing RAR archive entries.
172///
173/// ListSplit mode will list all entries. The payload itself cannot be processed and instead can
174/// only be skipped over. This will yield one header per individual file per part if the file is
175/// split across multiple parts. The [`FileHeader::is_split`] method will return true in that case.
176#[derive(Debug)]
177pub struct ListSplit;
178
179/// Mode with which the archive should be opened.
180///
181/// Possible modes are:
182///
183/// - [`List`](struct.List.html)
184/// - [`ListSplit`](struct.ListSplit.html)
185/// - [`Process`](struct.Process.html)
186pub trait OpenMode: private::Sealed {
187 const VALUE: private::OpenModeValue;
188}
189impl OpenMode for Process {
190 const VALUE: private::OpenModeValue = private::OpenModeValue::Extract;
191}
192impl OpenMode for List {
193 const VALUE: private::OpenModeValue = private::OpenModeValue::List;
194}
195impl OpenMode for ListSplit {
196 const VALUE: private::OpenModeValue = private::OpenModeValue::ListIncSplit;
197}
198
199impl<Mode: OpenMode, C: Cursor> OpenArchive<Mode, C> {
200 /// is the archive locked
201 pub fn is_locked(&self) -> bool {
202 self.flags.contains(ArchiveFlags::LOCK)
203 }
204
205 /// are the archive headers encrypted
206 pub fn has_encrypted_headers(&self) -> bool {
207 self.flags.contains(ArchiveFlags::ENC_HEADERS)
208 }
209
210 /// does the archive have a recovery record
211 pub fn has_recovery_record(&self) -> bool {
212 self.flags.contains(ArchiveFlags::RECOVERY)
213 }
214
215 /// does the archive have comments
216 pub fn has_comment(&self) -> bool {
217 self.flags.contains(ArchiveFlags::COMMENT)
218 }
219
220 /// is the archive solid (all files in a single compressed block).
221 pub fn is_solid(&self) -> bool {
222 self.flags.contains(ArchiveFlags::SOLID)
223 }
224
225 /// Volume information on the file that was *initially* opened.
226 ///
227 /// returns
228 /// - `VolumeInfo::None` if the opened file is a single-part archive
229 /// - `VolumeInfo::First` if the opened file is the first volume in a multipart archive
230 /// - `VolumeInfo::Subsequent` if the opened file is any other volume in a multipart archive
231 ///
232 /// Note that this value *never* changes from `First` to `Subsequent` by advancing to a
233 /// different volume.
234 pub fn volume_info(&self) -> VolumeInfo {
235 if self.flags.contains(ArchiveFlags::FIRST_VOLUME) {
236 VolumeInfo::First
237 } else if self.flags.contains(ArchiveFlags::VOLUME) {
238 VolumeInfo::Subsequent
239 } else {
240 VolumeInfo::None
241 }
242 }
243
244 /// unsets the `damaged` flag so that `Iterator` will not refuse to yield elements.
245 ///
246 /// Normally, when an error is returned during iteration, the archive remembers this
247 /// so that subsequent calls to `next` return `None` immediately. This is to prevent
248 /// the same error from recurring over and over again, leading to endless loops in programs
249 /// that might not have considered this. However, maybe there are errors that can be recovered
250 /// from? That's where this method might come in handy if you really know what you're doing.
251 /// However, should that be the case, I urge you to submit an issue / PR with an archive where
252 /// the recoverable error can be reproduced so I can exclude that case from "irrecoverable
253 /// errors" (currently all errors).
254 ///
255 /// Use at your own risk. Might be removed in future releases if somehow it can be verified
256 /// which errors are recoverable and which are not.
257 ///
258 /// # Example how you *might* use this
259 ///
260 /// ```no_run
261 /// use unrar_ng::{Archive, error::{When, Code}};
262 ///
263 /// let mut archive = Archive::new("corrupt.rar").open_for_listing().expect("archive error");
264 /// loop {
265 /// let mut error = None;
266 /// for result in &mut archive {
267 /// match result {
268 /// Ok(entry) => println!("{entry}"),
269 /// Err(e) => error = Some(e),
270 /// }
271 /// }
272 /// match error {
273 /// // your special recoverable error, please submit a PR with reproducible archive
274 /// Some(e) if (e.when, e.code) == (When::Process, Code::BadData) => archive.force_heal(),
275 /// Some(e) => panic!("irrecoverable error: {e}"),
276 /// None => break,
277 /// }
278 /// }
279 /// ```
280 pub fn force_heal(&mut self) {
281 self.damaged = false;
282 }
283}
284
285impl<Mode: OpenMode> OpenArchive<Mode, CursorBeforeHeader> {
286 pub(crate) fn new(
287 filename: &Path,
288 password: Option<&[u8]>,
289 recover: Option<&mut Option<Self>>,
290 ) -> UnrarResult<Self> {
291 let filename = pathed::construct(filename);
292
293 let mut data =
294 native::OpenArchiveDataEx::new(filename.as_ptr() as *const _, Mode::VALUE as u32);
295 let handle =
296 NonNull::new(unsafe { native::RAROpenArchiveEx(&mut data as *mut _) } as *mut _);
297
298 let arc = handle.and_then(|handle| {
299 if let Some(pw) = password {
300 let cpw = std::ffi::CString::new(pw).unwrap();
301 unsafe { native::RARSetPassword(handle.as_ptr(), cpw.as_ptr() as *const _) }
302 }
303 Some(OpenArchive {
304 handle: Handle(handle),
305 damaged: false,
306 flags: ArchiveFlags::from_bits(data.flags).unwrap(),
307 extra: CursorBeforeHeader,
308 marker: std::marker::PhantomData,
309 })
310 });
311 let result = Code::from(data.open_result as i32);
312
313 match (arc, result) {
314 (Some(arc), Code::Success) => Ok(arc),
315 (arc, _) => {
316 recover.and_then(|recover| arc.and_then(|arc| recover.replace(arc)));
317 Err(UnrarError::from(result, When::Open))
318 }
319 }
320 }
321
322 /// reads the next header of the underlying archive. The resulting OpenArchive will
323 /// be in "ProcessFile" mode, i.e. the file corresponding to the header (that has just
324 /// been read via this method call) will have to be read. Also contains header data
325 /// via [`archive.entry()`](OpenArchive::entry).
326 ///
327 /// # Examples
328 ///
329 /// Basic usage:
330 ///
331 /// ```
332 /// let archive = unrar_ng::Archive::new("data/version.rar").open_for_listing().unwrap().read_header();
333 /// assert!(archive.as_ref().is_ok_and(Option::is_some));
334 /// let archive = archive.unwrap().unwrap();
335 /// assert_eq!(archive.entry().filename.as_os_str(), "VERSION");
336 /// ```
337 pub fn read_header(self) -> UnrarResult<Option<OpenArchive<Mode, CursorBeforeFile>>> {
338 Ok(read_header(&self.handle)?.map(|entry| OpenArchive {
339 extra: CursorBeforeFile { header: entry },
340 damaged: self.damaged,
341 handle: self.handle,
342 flags: self.flags,
343 marker: std::marker::PhantomData,
344 }))
345 }
346}
347
348impl OpenArchive<Process, CursorBeforeHeader> {
349 /// Extracts all files from the archive to the specified directory in a single operation.
350 ///
351 /// This method is significantly faster than iterating through files individually,
352 /// especially for archives containing many small files. It bypasses the per-file
353 /// FFI overhead by using a batch extraction function internally.
354 ///
355 /// # Arguments
356 ///
357 /// * `dest` - The destination directory path. If the directory doesn't exist,
358 /// it will be created. Pass an empty path or `"."` for current directory.
359 ///
360 /// # Example
361 ///
362 /// ```no_run
363 /// use unrar_ng::Archive;
364 ///
365 /// let archive = Archive::new("archive.rar")
366 /// .open_for_processing()
367 /// .expect("Failed to open archive");
368 ///
369 /// archive.extract_all("./output")
370 /// .expect("Failed to extract archive");
371 /// ```
372 ///
373 /// # Panics
374 ///
375 /// This function will panic if `dest` contains nul characters.
376 pub fn extract_all<P: AsRef<Path>>(self, dest: P) -> UnrarResult<()> {
377 let dest_path = pathed::construct(dest.as_ref());
378 let result = pathed::extract_all(self.handle.0.as_ptr(), &dest_path);
379 match Code::from(result) {
380 Code::Success => Ok(()),
381 code => Err(UnrarError::from(code, When::Process)),
382 }
383 }
384
385 /// Extracts all files from the archive with a progress callback.
386 ///
387 /// This method is similar to [`extract_all`](Self::extract_all) but allows you to
388 /// receive notifications about extraction progress through a callback function.
389 ///
390 /// # Arguments
391 ///
392 /// * `dest` - The destination directory path.
393 /// * `callback` - A closure that receives [`ExtractEvent`] notifications.
394 /// For `Start`, `Ok` and `Err`, returning `false` cancels the rest
395 /// of the extraction and the call returns `Ok(ExtractStatus::Cancelled)`.
396 /// For `LargeDictWarning`, `false` rejects the oversized dictionary
397 /// instead of cancelling — extraction then fails with
398 /// [`Code::LargeDict`](crate::error::Code::LargeDict).
399 ///
400 /// # Returns
401 ///
402 /// * `Ok(ExtractStatus::Completed)` — the DLL finished iterating the
403 /// archive without the callback ever returning `false` from a
404 /// cancellable event.
405 /// * `Ok(ExtractStatus::Cancelled)` — the callback aborted extraction
406 /// early on a `Start`/`Ok`/`Err` event.
407 /// * `Err(UnrarError { .. })` — the DLL surfaced an error (incl.
408 /// [`Code::LargeDict`](crate::error::Code::LargeDict) when the
409 /// callback rejected an oversized dictionary).
410 ///
411 /// # Example
412 ///
413 /// ```no_run
414 /// use unrar_ng::{Archive, ExtractEvent, ExtractStatus};
415 ///
416 /// let archive = Archive::new("archive.rar")
417 /// .open_for_processing()
418 /// .expect("Failed to open archive");
419 ///
420 /// let status = archive.extract_all_with_callback("./output", |event| {
421 /// match event {
422 /// ExtractEvent::Start { filename, size } => {
423 /// print!("extracting {}... ({} bytes) ", filename.display(), size);
424 /// true // continue extraction
425 /// }
426 /// ExtractEvent::Ok { .. } => {
427 /// println!("ok");
428 /// true
429 /// }
430 /// ExtractEvent::Err { filename, error_code } => {
431 /// println!("error (code: {})", error_code);
432 /// true // continue with other files
433 /// }
434 /// ExtractEvent::LargeDictWarning { dict_size_kb, max_dict_size_kb } => {
435 /// eprintln!("archive needs {dict_size_kb} KB dict, build supports {max_dict_size_kb} KB");
436 /// false // refuse oversized dict; extraction fails with Code::LargeDict
437 /// }
438 /// _ => true,
439 /// }
440 /// }).expect("Failed to extract archive");
441 /// match status {
442 /// ExtractStatus::Completed => println!("done"),
443 /// ExtractStatus::Cancelled => println!("user cancelled"),
444 /// _ => {}
445 /// }
446 /// ```
447 ///
448 /// # Panics
449 ///
450 /// This function will panic if `dest` contains nul characters.
451 pub fn extract_all_with_callback<P, F>(
452 self,
453 dest: P,
454 mut callback: F,
455 ) -> UnrarResult<ExtractStatus>
456 where
457 P: AsRef<Path>,
458 F: FnMut(ExtractEvent) -> bool,
459 {
460 // Userdata struct to pass to the C callback
461 struct CallbackData<'a, F> {
462 callback: &'a mut F,
463 cancelled: bool,
464 }
465
466 extern "C" fn extract_callback<F>(
467 msg: native::UINT,
468 user_data: native::LPARAM,
469 p1: native::LPARAM,
470 p2: native::LPARAM,
471 ) -> c_int
472 where
473 F: FnMut(ExtractEvent) -> bool,
474 {
475 if user_data == 0 {
476 return 0;
477 }
478 let data = unsafe { &mut *(user_data as *mut CallbackData<'_, F>) };
479
480 // Helper to read a wchar_t* string from p1.
481 // native::WCHAR is i32 on Unix, u16 on Windows.
482 //
483 // FIXME: the `char::from_u32 + filter_map` decode below silently
484 // drops unpaired surrogates on Windows (wchar_t = u16), so the
485 // filename reported via ExtractEvent can diverge from what
486 // pathed/all.rs writes to disk via lossless WideCString. The
487 // 2048-wchar truncation cap below is also a known gap — both
488 // problems disappear once this is rewritten with
489 // U16/U32CString::from_ptr_truncate -> OsString -> PathBuf.
490 fn read_filename(p1: native::LPARAM) -> Option<PathBuf> {
491 if p1 == 0 {
492 return None;
493 }
494
495 let ptr = p1 as *const native::WCHAR;
496 if ptr.is_null() {
497 return None;
498 }
499
500 // Find null terminator. 2048 mirrors the upstream maximum
501 // path length used by `WideCString::from_ptr_truncate` in
502 // `pathed/all.rs::construct` and elsewhere — see the comment
503 // on `Internal::callback`'s UCM_CHANGEVOLUMEW arm in this file.
504 let mut len = 0usize;
505 const MAX_LEN: usize = 2048;
506 unsafe {
507 while len < MAX_LEN && *ptr.add(len) != 0 {
508 len += 1;
509 }
510 }
511
512 if len == 0 {
513 return None;
514 }
515
516 // Convert wchar_t slice to PathBuf
517 let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
518
519 // wchar_t is i32 on Unix, u16 on Windows
520 // Convert to chars respecting the platform's wchar_t representation
521 let path_string: String = slice
522 .iter()
523 .filter_map(|&c| char::from_u32(c as u32))
524 .collect();
525
526 Some(PathBuf::from(path_string))
527 }
528
529 match msg {
530 native::UCM_EXTRACTFILE => {
531 // p1 = filename (wchar_t*), p2 = file size
532 if let Some(filename) = read_filename(p1) {
533 let event = ExtractEvent::Start {
534 filename,
535 size: p2 as u64,
536 };
537 if !(data.callback)(event) {
538 data.cancelled = true;
539 return -1; // Cancel extraction
540 }
541 }
542 0
543 }
544 native::UCM_EXTRACTFILE_OK => {
545 // p1 = filename (wchar_t*), p2 = 0
546 if let Some(filename) = read_filename(p1) {
547 let event = ExtractEvent::Ok { filename };
548 if !(data.callback)(event) {
549 data.cancelled = true;
550 return -1;
551 }
552 }
553 0
554 }
555 native::UCM_EXTRACTFILE_ERR => {
556 // p1 = filename (wchar_t*), p2 = error code
557 if let Some(filename) = read_filename(p1) {
558 let event = ExtractEvent::Err {
559 filename,
560 error_code: p2 as i32,
561 };
562 if !(data.callback)(event) {
563 data.cancelled = true;
564 return -1;
565 }
566 }
567 0
568 }
569 native::UCM_LARGEDICT => {
570 // Upstream `uiDictLimit` (vendor/unrar/uisilent.cpp): only
571 // a return of 1 lets extraction continue; any other value
572 // (including 0) makes the DLL fail with ERAR_LARGE_DICT.
573 // P1/P2 are the required and max dict sizes in KB.
574 //
575 // Rejecting an oversized dictionary is not a user cancel —
576 // it surfaces as `Err(Code::LargeDict)`, so we deliberately
577 // do NOT set `data.cancelled` here. That keeps
578 // `ExtractStatus::Cancelled` strictly meaning "callback
579 // returned false from Start/Ok/Err".
580 let event = ExtractEvent::LargeDictWarning {
581 dict_size_kb: p1 as u64,
582 max_dict_size_kb: p2 as u64,
583 };
584 if (data.callback)(event) { 1 } else { 0 }
585 }
586 native::UCM_CHANGEVOLUMEW => {
587 // Handle volume change: -1 means stop (volume not found)
588 match p2 {
589 native::RAR_VOL_ASK => -1,
590 _ => 0,
591 }
592 }
593 _ => 0,
594 }
595 }
596
597 let dest_path = pathed::construct(dest.as_ref());
598
599 let mut callback_data = CallbackData {
600 callback: &mut callback,
601 cancelled: false,
602 };
603
604 unsafe {
605 native::RARSetCallback(
606 self.handle.0.as_ptr(),
607 Some(extract_callback::<F>),
608 &mut callback_data as *mut _ as native::LPARAM,
609 );
610 }
611
612 let result = pathed::extract_all(self.handle.0.as_ptr(), &dest_path);
613
614 match Code::from(result) {
615 Code::Success if callback_data.cancelled => Ok(ExtractStatus::Cancelled),
616 Code::Success => Ok(ExtractStatus::Completed),
617 code => Err(UnrarError::from(code, When::Process)),
618 }
619 }
620}
621
622impl Iterator for OpenArchive<List, CursorBeforeHeader> {
623 type Item = Result<FileHeader, UnrarError>;
624
625 fn next(&mut self) -> Option<Self::Item> {
626 if self.damaged {
627 return None;
628 }
629 match read_header(&self.handle) {
630 Ok(Some(header)) => {
631 match Internal::<Skip>::process_file_raw(&self.handle, None, None) {
632 Ok(_) => Some(Ok(header)),
633 Err(s) => {
634 self.damaged = true;
635 Some(Err(s))
636 }
637 }
638 }
639 Ok(None) => None,
640 Err(s) => {
641 self.damaged = true;
642 Some(Err(s))
643 }
644 }
645 }
646}
647
648impl Iterator for OpenArchive<ListSplit, CursorBeforeHeader> {
649 type Item = Result<FileHeader, UnrarError>;
650
651 fn next(&mut self) -> Option<Self::Item> {
652 if self.damaged {
653 return None;
654 }
655 match read_header(&self.handle) {
656 Ok(Some(header)) => {
657 match Internal::<Skip>::process_file_raw(&self.handle, None, None) {
658 Ok(_) => Some(Ok(header)),
659 Err(s) => {
660 self.damaged = true;
661 Some(Err(s))
662 }
663 }
664 }
665 Ok(None) => None,
666 Err(s) => {
667 self.damaged = true;
668 Some(Err(s))
669 }
670 }
671 }
672}
673
674impl<M: OpenMode> OpenArchive<M, CursorBeforeFile> {
675 /// returns the file header for the file that follows which is to be processed next.
676 pub fn entry(&self) -> &FileHeader {
677 &self.extra.header
678 }
679
680 /// skips over the next file, not doing anything with it.
681 pub fn skip(self) -> UnrarResult<OpenArchive<M, CursorBeforeHeader>> {
682 self.process_file::<Skip>(None, None)
683 }
684
685 fn process_file<PM: ProcessMode>(
686 self,
687 path: Option<&pathed::RarStr>,
688 file: Option<&pathed::RarStr>,
689 ) -> UnrarResult<OpenArchive<M, CursorBeforeHeader>> {
690 Ok(self.process_file_x::<PM>(path, file)?.1)
691 }
692
693 fn process_file_x<PM: ProcessMode>(
694 self,
695 path: Option<&pathed::RarStr>,
696 file: Option<&pathed::RarStr>,
697 ) -> UnrarResult<(PM::Output, OpenArchive<M, CursorBeforeHeader>)> {
698 let result = Ok((
699 Internal::<PM>::process_file_raw(&self.handle, path, file)?,
700 OpenArchive {
701 extra: CursorBeforeHeader,
702 damaged: self.damaged,
703 handle: self.handle,
704 flags: self.flags,
705 marker: std::marker::PhantomData,
706 },
707 ));
708 result
709 }
710}
711
712impl OpenArchive<Process, CursorBeforeFile> {
713 /// Reads the underlying file into a `Vec<u8>`
714 /// Returns the data as well as the owned Archive that can be processed further.
715 pub fn read(self) -> UnrarResult<(Vec<u8>, OpenArchive<Process, CursorBeforeHeader>)> {
716 Ok(self.process_file_x::<ReadToVec>(None, None)?)
717 }
718
719 /// Test the file without extracting it
720 pub fn test(self) -> UnrarResult<OpenArchive<Process, CursorBeforeHeader>> {
721 Ok(self.process_file::<Test>(None, None)?)
722 }
723
724 /// Extracts the file into the current working directory
725 /// Returns the OpenArchive for further processing
726 pub fn extract(self) -> UnrarResult<OpenArchive<Process, CursorBeforeHeader>> {
727 self.dir_extract(None)
728 }
729
730 /// Extracts the file into the specified directory.
731 /// Returns the OpenArchive for further processing
732 ///
733 /// # Panics
734 ///
735 /// This function will panic if `base` contains nul characters.
736 pub fn extract_with_base<P: AsRef<Path>>(
737 self,
738 base: P,
739 ) -> UnrarResult<OpenArchive<Process, CursorBeforeHeader>> {
740 self.dir_extract(Some(base.as_ref()))
741 }
742
743 /// Extracts the file into the specified file.
744 /// Returns the OpenArchive for further processing
745 ///
746 /// # Panics
747 ///
748 /// This function will panic if `dest` contains nul characters.
749 pub fn extract_to<P: AsRef<Path>>(
750 self,
751 file: P,
752 ) -> UnrarResult<OpenArchive<Process, CursorBeforeHeader>> {
753 let dest = pathed::construct(file.as_ref());
754 self.process_file::<Extract>(None, Some(&dest))
755 }
756
757 /// extracting into a directory if the filename has unicode characters
758 /// does not work on Linux, so we must specify the full path for Linux
759 fn dir_extract(
760 self,
761 base: Option<&Path>,
762 ) -> UnrarResult<OpenArchive<Process, CursorBeforeHeader>> {
763 let (path, file) = pathed::preprocess_extract(base, &self.entry().filename);
764 self.process_file::<Extract>(path.as_deref(), file.as_deref())
765 }
766}
767
768fn read_header(handle: &Handle) -> UnrarResult<Option<FileHeader>> {
769 let mut userdata: Userdata<<Skip as ProcessMode>::Output> = Default::default();
770 unsafe {
771 native::RARSetCallback(
772 handle.0.as_ptr(),
773 Some(Internal::<Skip>::callback),
774 &mut userdata as *mut _ as native::LPARAM,
775 );
776 }
777 let mut header = native::HeaderDataEx::default();
778 let read_result = Code::from(unsafe {
779 native::RARReadHeaderEx(handle.0.as_ptr(), &mut header as *mut _)
780 });
781 match read_result {
782 Code::Success => Ok(Some(header.into())),
783 Code::EndArchive => Ok(None),
784 _ => Err(UnrarError::from(read_result, When::Read)),
785 }
786}
787
788#[derive(Debug)]
789struct Skip;
790#[derive(Debug)]
791struct ReadToVec;
792#[derive(Debug)]
793struct Extract;
794#[derive(Debug)]
795struct Test;
796
797trait ProcessMode: core::fmt::Debug {
798 const OPERATION: private::Operation;
799 type Output: core::fmt::Debug + std::default::Default;
800
801 fn process_data(data: &mut Self::Output, other: &[u8]);
802}
803impl ProcessMode for Skip {
804 const OPERATION: private::Operation = private::Operation::Skip;
805 type Output = ();
806
807 fn process_data(_: &mut Self::Output, _: &[u8]) {}
808}
809impl ProcessMode for ReadToVec {
810 const OPERATION: private::Operation = private::Operation::Test;
811 type Output = Vec<u8>;
812
813 fn process_data(my: &mut Self::Output, other: &[u8]) {
814 my.extend_from_slice(other);
815 }
816}
817impl ProcessMode for Extract {
818 const OPERATION: private::Operation = private::Operation::Extract;
819 type Output = ();
820
821 fn process_data(_: &mut Self::Output, _: &[u8]) {}
822}
823impl ProcessMode for Test {
824 const OPERATION: private::Operation = private::Operation::Test;
825 type Output = ();
826
827 fn process_data(_: &mut Self::Output, _: &[u8]) {}
828}
829
830struct Internal<M: ProcessMode> {
831 marker: std::marker::PhantomData<M>,
832}
833
834impl<M: ProcessMode> Internal<M> {
835 extern "C" fn callback(
836 msg: native::UINT,
837 user_data: native::LPARAM,
838 p1: native::LPARAM,
839 p2: native::LPARAM,
840 ) -> c_int {
841 if user_data == 0 {
842 return 0;
843 }
844 let user_data = unsafe { &mut *(user_data as *mut Userdata<M::Output>) };
845 match msg {
846 native::UCM_CHANGEVOLUMEW => {
847 // 2048 seems to be the buffer size in unrar,
848 // also it's the maximum path length since 5.00.
849 let next =
850 unsafe { widestring::WideCString::from_ptr_truncate(p1 as *const _, 2048) };
851 user_data.1 = Some(next);
852 match p2 {
853 // Next volume not found. -1 means stop
854 native::RAR_VOL_ASK => -1,
855 // Next volume found, 0 means continue
856 _ => 0,
857 }
858 }
859 native::UCM_PROCESSDATA => {
860 let raw_slice = std::ptr::slice_from_raw_parts(p1 as *const u8, p2 as _);
861 M::process_data(&mut user_data.0, unsafe { &*raw_slice as &_ });
862 0
863 }
864 _ => 0,
865 }
866 }
867
868 fn process_file_raw(
869 handle: &Handle,
870 path: Option<&pathed::RarStr>,
871 file: Option<&pathed::RarStr>,
872 ) -> UnrarResult<M::Output> {
873 let mut user_data: Userdata<M::Output> = Default::default();
874 unsafe {
875 native::RARSetCallback(
876 handle.0.as_ptr(),
877 Some(Self::callback),
878 &mut user_data as *mut _ as native::LPARAM,
879 );
880 }
881 let process_result = Code::from(pathed::process_file(
882 handle.0.as_ptr(),
883 M::OPERATION as i32,
884 path,
885 file,
886 ));
887 match process_result {
888 Code::Success => Ok(user_data.0),
889 _ => Err(UnrarError::from(process_result, When::Process)),
890 }
891 }
892}
893
894bitflags::bitflags! {
895 #[derive(Debug)]
896 struct EntryFlags: u32 {
897 const SPLIT_BEFORE = 0x1;
898 const SPLIT_AFTER = 0x2;
899 const ENCRYPTED = 0x4;
900 // const RESERVED = 0x8;
901 const SOLID = 0x10;
902 const DIRECTORY = 0x20;
903 }
904}
905
906/// Metadata for an entry in a RAR archive
907///
908/// Created using the read_header methods in an OpenArchive, contains
909/// information for the file that follows which is to be processed next.
910#[allow(missing_docs)]
911#[derive(Debug)]
912pub struct FileHeader {
913 pub filename: PathBuf,
914 flags: EntryFlags,
915 pub unpacked_size: u64,
916 pub file_crc: u32,
917 pub file_time: u32,
918 pub method: u32,
919 pub file_attr: u32,
920}
921
922impl FileHeader {
923 /// is this entry split across multiple volumes.
924 ///
925 /// Will also work in open mode [`List`]
926 pub fn is_split(&self) -> bool {
927 self.flags.contains(EntryFlags::SPLIT_BEFORE)
928 || self.flags.contains(EntryFlags::SPLIT_AFTER)
929 }
930
931 /// is this entry split across multiple volumes, starting here
932 ///
933 /// Will also work in open mode [`List`]
934 pub fn is_split_after(&self) -> bool {
935 self.flags.contains(EntryFlags::SPLIT_AFTER)
936 }
937
938 /// is this entry split across multiple volumes, starting here
939 ///
940 /// Will always return false in open mode [`List`][^1].
941 ///
942 /// [^1]: this claim is not proven, however, the DLL seems to always skip
943 /// files where this flag would have been set.
944 pub fn is_split_before(&self) -> bool {
945 self.flags.contains(EntryFlags::SPLIT_BEFORE)
946 }
947
948 /// is this entry a directory
949 pub fn is_directory(&self) -> bool {
950 self.flags.contains(EntryFlags::DIRECTORY)
951 }
952
953 /// is this entry encrypted
954 pub fn is_encrypted(&self) -> bool {
955 self.flags.contains(EntryFlags::ENCRYPTED)
956 }
957
958 /// is this entry a file
959 pub fn is_file(&self) -> bool {
960 !self.is_directory()
961 }
962}
963
964impl fmt::Display for FileHeader {
965 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
966 write!(f, "{:?}", self.filename)?;
967 if self.is_directory() {
968 write!(f, "/")?
969 }
970 if self.is_split() {
971 write!(f, " (partial)")?
972 }
973 Ok(())
974 }
975}
976
977impl From<native::HeaderDataEx> for FileHeader {
978 fn from(header: native::HeaderDataEx) -> Self {
979 // `native::HeaderDataEx` is `#[repr(C, packed(1))]` to match the C++
980 // `#pragma pack(push, 1)` layout, so taking `&header.filename_w` is
981 // forbidden. `&raw const` produces a raw pointer without creating a
982 // reference; the underlying wchar_t array happens to be 4-byte
983 // aligned at its real offset in memory, so the subsequent read is
984 // fine for `WideCString::from_ptr_truncate`.
985 let filename_w_ptr = &raw const header.filename_w as *const _;
986 let filename =
987 unsafe { widestring::WideCString::from_ptr_truncate(filename_w_ptr, 1024) };
988
989 // Packed-struct fields are `Copy` primitives here, so value reads
990 // are legal; we copy each scalar out into a local before passing it
991 // to the constructor so rustc can't be tempted into taking a
992 // reference to the packed field.
993 let flags = header.flags;
994 let unp_size = header.unp_size;
995 let unp_size_high = header.unp_size_high;
996 let file_crc = header.file_crc;
997 let file_time = header.file_time;
998 let method = header.method;
999 let file_attr = header.file_attr;
1000
1001 FileHeader {
1002 filename: PathBuf::from(filename.to_os_string()),
1003 flags: EntryFlags::from_bits(flags).unwrap(),
1004 unpacked_size: unpack_unp_size(unp_size, unp_size_high),
1005 file_crc,
1006 file_time,
1007 method,
1008 file_attr,
1009 }
1010 }
1011}
1012
1013fn unpack_unp_size(unp_size: c_uint, unp_size_high: c_uint) -> u64 {
1014 ((unp_size_high as u64) << (8 * std::mem::size_of::<c_uint>())) | (unp_size as u64)
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019 #[test]
1020 fn combine_size() {
1021 use super::unpack_unp_size;
1022 let (high, low) = (1u32, 1464303715u32);
1023 assert_eq!(unpack_unp_size(low, high), 5759271011);
1024 }
1025}