Skip to main content

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}