Skip to main content

compress_tools/
lib.rs

1// Copyright (C) 2019-2021 O.S. Systems Software LTDA
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! The `compress-tools` crate aims to provide a convenient and easy to use set
6//! of methods which builds on top of `libarchive` exposing a small set of it’s
7//! functionalities.
8//!
9//! | Platform | Build Status |
10//! | -------- | ------------ |
11//! | Linux - x86_64 | [![build status](https://github.com/OSSystems/compress-tools-rs/workflows/CI%20-%20Linux%20-%20x86_64/badge.svg)](https://github.com/OSSystems/compress-tools-rs/actions) |
12//! | macOS - x86_64 | [![build status](https://github.com/OSSystems/compress-tools-rs/workflows/CI%20-%20macOS%20-%20x86_64/badge.svg)](https://github.com/OSSystems/compress-tools-rs/actions) |
13//! | Windows - x86_64 | [![build status](https://github.com/OSSystems/compress-tools-rs/workflows/CI%20-%20Windows%20-%20x86_64/badge.svg)](https://github.com/OSSystems/compress-tools-rs/actions) |
14//!
15//! ---
16//!
17//! # Dependencies
18//!
19//! You must have `libarchive`, 3.2.0 or newer, properly installed on your
20//! system in order to use this. If building on *nix and Windows GNU
21//! systems, `pkg-config` is used to locate the `libarchive`; on Windows
22//! MSVC, `vcpkg` will be used to locating the `libarchive`.
23//!
24//! The minimum supported Rust version is 1.59.
25//!
26//! # Features
27//!
28//! This crate is capable of extracting:
29//!
30//! * compressed files
31//! * archive files
32//! * single file from an archive
33//!
34//! For example, to extract an archive file it is as simple as:
35//!
36//! ```no_run
37//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! use compress_tools::*;
39//! use std::fs::File;
40//! use std::path::Path;
41//!
42//! let mut source = File::open("tree.tar.gz")?;
43//! let dest = Path::new("/tmp/dest");
44//!
45//! uncompress_archive(&mut source, &dest, Ownership::Preserve)?;
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! # Strict archive parsing
51//!
52//! Archive-listing and archive-extraction entry points (`list_archive_files`,
53//! `list_archive_entries`, `uncompress_archive`, `uncompress_archive_file`,
54//! `ArchiveIterator`, and their async/`_with_encoding` siblings) no longer
55//! register libarchive's "raw" format handler, so input that isn't a real
56//! archive errors out instead of yielding a single `data` entry.
57//!
58//! Use [`uncompress_data`] for decompressing a single stream (gzip, xz, …)
59//! — it continues to support raw input because that is its purpose. For
60//! streaming iteration that should accept arbitrary bytes, opt back in
61//! with [`ArchiveIteratorBuilder::raw_format`]. libarchive's "mtree"
62//! handler remains enabled on all entry points; pass
63//! `ArchiveIteratorBuilder::mtree_format(false)` to the iterator if you
64//! need to reject mtree matches.
65
66#[cfg(feature = "async_support")]
67pub mod async_support;
68mod error;
69mod ffi;
70#[cfg(feature = "futures_support")]
71pub mod futures_support;
72mod iterator;
73#[cfg(feature = "tokio_support")]
74pub mod tokio_support;
75mod zip_preflight;
76
77use error::{archive_result, archive_result_strict};
78pub use error::{Error, Result};
79use io::{Seek, SeekFrom};
80pub use iterator::{ArchiveContents, ArchiveIterator, ArchiveIteratorBuilder, ArchivePassword};
81use std::{
82    ffi::{CStr, CString},
83    io::{self, Read, Write},
84    os::raw::{c_int, c_void},
85    path::{Component, Path},
86    slice,
87};
88
89const READER_BUFFER_SIZE: usize = 16384;
90
91/// Re-export of [`libc::stat`] so `crate::stat` resolves uniformly across
92/// platforms — Windows has its own layout declared below.
93#[cfg(not(target_os = "windows"))]
94pub use libc::stat;
95
96/// `stat` layout matching the one exposed by `libarchive` on Windows.
97///
98/// On Windows `libarchive`'s `archive_entry_stat()` returns a pointer to the
99/// struct declared in `<sys/stat.h>`, which differs from `libc::stat` (the
100/// latter is actually `stat64`). Using the wrong layout reads garbage for
101/// `st_size` and the three `st_*time` fields.
102#[cfg(target_os = "windows")]
103#[derive(Copy, Clone)]
104#[repr(C)]
105#[allow(non_camel_case_types)]
106pub struct stat {
107    pub st_dev: libc::dev_t,
108    pub st_ino: libc::ino_t,
109    pub st_mode: u16,
110    pub st_nlink: libc::c_short,
111    pub st_uid: libc::c_short,
112    pub st_gid: libc::c_short,
113    pub st_rdev: libc::dev_t,
114    pub st_size: i32,
115    pub st_atime: libc::time_t,
116    pub st_mtime: libc::time_t,
117    pub st_ctime: libc::time_t,
118}
119
120/// Path and uncompressed size for a single archive entry.
121///
122/// `size` comes from the archive header and may be `0` for formats that do
123/// not record it there (some raw compressed streams, ZIP entries using a
124/// data descriptor). Tar and standard ZIP populate it reliably.
125#[derive(Clone, Debug)]
126pub struct ArchiveEntryInfo {
127    pub path: String,
128    pub size: u64,
129}
130
131/// Determine the ownership behavior when unpacking the archive.
132#[derive(Clone, Copy, Debug)]
133pub enum Ownership {
134    /// Preserve the ownership of the files when uncompressing the archive.
135    Preserve,
136    /// Ignore the ownership information of the files when uncompressing the
137    /// archive.
138    Ignore,
139}
140
141struct ReaderPipe<'a> {
142    reader: &'a mut dyn Read,
143    buffer: &'a mut [u8],
144}
145
146trait ReadAndSeek: Read + Seek {}
147impl<T> ReadAndSeek for T where T: Read + Seek {}
148
149struct SeekableReaderPipe<'a> {
150    reader: &'a mut dyn ReadAndSeek,
151    buffer: &'a mut [u8],
152}
153
154pub type DecodeCallback = fn(&[u8]) -> Result<String>;
155
156pub(crate) fn decode_utf8(bytes: &[u8]) -> Result<String> {
157    Ok(std::str::from_utf8(bytes)?.to_owned())
158}
159
160/// Get all files in a archive using `source` as a reader.
161/// # Example
162///
163/// ```no_run
164/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
165/// use compress_tools::*;
166/// use std::fs::File;
167///
168/// let mut source = File::open("tree.tar")?;
169/// let decode_utf8 = |bytes: &[u8]| Ok(std::str::from_utf8(bytes)?.to_owned());
170///
171/// let file_list = list_archive_files_with_encoding(&mut source, decode_utf8)?;
172/// # Ok(())
173/// # }
174/// ```
175pub fn list_archive_files_with_encoding<R>(source: R, decode: DecodeCallback) -> Result<Vec<String>>
176where
177    R: Read + Seek,
178{
179    Ok(list_archive_entries_with_encoding(source, decode)?
180        .into_iter()
181        .map(|e| e.path)
182        .collect())
183}
184
185/// Get all files in a archive using `source` as a reader.
186/// # Example
187///
188/// ```no_run
189/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
190/// use compress_tools::*;
191/// use std::fs::File;
192///
193/// let mut source = File::open("tree.tar")?;
194///
195/// let file_list = list_archive_files(&mut source)?;
196/// # Ok(())
197/// # }
198/// ```
199pub fn list_archive_files<R>(source: R) -> Result<Vec<String>>
200where
201    R: Read + Seek,
202{
203    list_archive_files_with_encoding(source, decode_utf8)
204}
205
206/// Get entry metadata (path and uncompressed size) for every entry in an
207/// archive without extracting their contents.
208///
209/// See [`ArchiveEntryInfo`] for caveats on `size` reporting across formats.
210///
211/// # Example
212///
213/// ```no_run
214/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
215/// use compress_tools::*;
216/// use std::fs::File;
217///
218/// let mut source = File::open("tree.tar")?;
219/// let decode_utf8 = |bytes: &[u8]| Ok(std::str::from_utf8(bytes)?.to_owned());
220///
221/// for entry in list_archive_entries_with_encoding(&mut source, decode_utf8)? {
222///     println!("{}: {} bytes", entry.path, entry.size);
223/// }
224/// # Ok(())
225/// # }
226/// ```
227pub fn list_archive_entries_with_encoding<R>(
228    source: R,
229    decode: DecodeCallback,
230) -> Result<Vec<ArchiveEntryInfo>>
231where
232    R: Read + Seek,
233{
234    let _utf8_guard = ffi::UTF8LocaleGuard::new();
235    run_with_archive(
236        Ownership::Ignore,
237        source,
238        |archive_reader, _, mut entry| unsafe {
239            let mut entries = Vec::new();
240            loop {
241                match ffi::archive_read_next_header(archive_reader, &mut entry) {
242                    ffi::ARCHIVE_EOF => return Ok(entries),
243                    value => archive_result(value, archive_reader)?,
244                }
245
246                let _utf8_guard = ffi::WindowsUTF8LocaleGuard::new();
247                let cstr = libarchive_entry_pathname(entry)?;
248                let path = decode(cstr.to_bytes())?;
249                let size = libarchive_entry_size(entry);
250                entries.push(ArchiveEntryInfo { path, size });
251            }
252        },
253    )
254}
255
256/// Get entry metadata (path and uncompressed size) for every entry in an
257/// archive without extracting their contents.
258///
259/// See [`ArchiveEntryInfo`] for caveats on `size` reporting across formats.
260///
261/// # Example
262///
263/// ```no_run
264/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
265/// use compress_tools::*;
266/// use std::fs::File;
267///
268/// let mut source = File::open("tree.tar")?;
269///
270/// for entry in list_archive_entries(&mut source)? {
271///     println!("{}: {} bytes", entry.path, entry.size);
272/// }
273/// # Ok(())
274/// # }
275/// ```
276pub fn list_archive_entries<R>(source: R) -> Result<Vec<ArchiveEntryInfo>>
277where
278    R: Read + Seek,
279{
280    list_archive_entries_with_encoding(source, decode_utf8)
281}
282
283/// Uncompress a file using the `source` need as reader and the `target` as a
284/// writer.
285///
286/// # Example
287///
288/// ```no_run
289/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
290/// use compress_tools::*;
291/// use std::fs::File;
292///
293/// let mut source = File::open("file.txt.gz")?;
294/// let mut target = Vec::default();
295///
296/// uncompress_data(&mut source, &mut target)?;
297/// # Ok(())
298/// # }
299/// ```
300///
301/// Slices can be used if you know the exact length of the uncompressed data.
302/// ```no_run
303/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
304/// use compress_tools::*;
305/// use std::fs::File;
306///
307/// let mut source = File::open("file.txt.gz")?;
308/// let mut target = [0 as u8; 313];
309///
310/// uncompress_data(&mut source, &mut target as &mut [u8])?;
311/// # Ok(())
312/// # }
313/// ```
314pub fn uncompress_data<R, W>(source: R, target: W) -> Result<usize>
315where
316    R: Read,
317    W: Write,
318{
319    let _utf8_guard = ffi::UTF8LocaleGuard::new();
320    run_with_unseekable_archive(source, |archive_reader, _, mut entry| unsafe {
321        archive_result(
322            ffi::archive_read_next_header(archive_reader, &mut entry),
323            archive_reader,
324        )?;
325        libarchive_write_data_block(archive_reader, target)
326    })
327}
328
329/// Uncompress an archive using `source` as a reader and `dest` as the
330/// destination directory.
331///
332/// # Example
333///
334/// ```no_run
335/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
336/// use compress_tools::*;
337/// use std::fs::File;
338/// use std::path::Path;
339///
340/// let mut source = File::open("tree.tar.gz")?;
341/// let dest = Path::new("/tmp/dest");
342/// let decode_utf8 = |bytes: &[u8]| Ok(std::str::from_utf8(bytes)?.to_owned());
343///
344/// uncompress_archive_with_encoding(&mut source, &dest, Ownership::Preserve, decode_utf8)?;
345/// # Ok(())
346/// # }
347/// ```
348pub fn uncompress_archive_with_encoding<R>(
349    source: R,
350    dest: &Path,
351    ownership: Ownership,
352    decode: DecodeCallback,
353) -> Result<()>
354where
355    R: Read + Seek,
356{
357    let _utf8_guard = ffi::UTF8LocaleGuard::new();
358    run_with_archive(
359        ownership,
360        source,
361        |archive_reader, archive_writer, mut entry| unsafe {
362            loop {
363                match ffi::archive_read_next_header(archive_reader, &mut entry) {
364                    ffi::ARCHIVE_EOF => return Ok(()),
365                    value => archive_result(value, archive_reader)?,
366                }
367
368                let _utf8_guard = ffi::WindowsUTF8LocaleGuard::new();
369                let cstr = libarchive_entry_pathname(entry)?;
370                let target_path = CString::new(
371                    dest.join(sanitize_destination_path(Path::new(&decode(
372                        cstr.to_bytes(),
373                    )?))?)
374                    .to_str()
375                    .unwrap(),
376                )
377                .unwrap();
378
379                ffi::archive_entry_set_pathname(entry, target_path.as_ptr());
380
381                let link_name = ffi::archive_entry_hardlink(entry);
382                if !link_name.is_null() {
383                    let target_path = CString::new(
384                        dest.join(sanitize_destination_path(Path::new(&decode(
385                            CStr::from_ptr(link_name).to_bytes(),
386                        )?))?)
387                        .to_str()
388                        .unwrap(),
389                    )
390                    .unwrap();
391
392                    ffi::archive_entry_set_hardlink(entry, target_path.as_ptr());
393                }
394
395                archive_result_strict(
396                    ffi::archive_write_header(archive_writer, entry),
397                    archive_writer,
398                )?;
399                if !libarchive_entry_is_dir(entry) {
400                    libarchive_copy_data(archive_reader, archive_writer)?;
401                }
402
403                archive_result_strict(
404                    ffi::archive_write_finish_entry(archive_writer),
405                    archive_writer,
406                )?;
407            }
408        },
409    )
410}
411
412/// Uncompress an archive using `source` as a reader and `dest` as the
413/// destination directory.
414///
415/// # Example
416///
417/// ```no_run
418/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
419/// use compress_tools::*;
420/// use std::fs::File;
421/// use std::path::Path;
422///
423/// let mut source = File::open("tree.tar.gz")?;
424/// let dest = Path::new("/tmp/dest");
425///
426/// uncompress_archive(&mut source, &dest, Ownership::Preserve)?;
427/// # Ok(())
428/// # }
429/// ```
430pub fn uncompress_archive<R>(source: R, dest: &Path, ownership: Ownership) -> Result<()>
431where
432    R: Read + Seek,
433{
434    uncompress_archive_with_encoding(source, dest, ownership, decode_utf8)
435}
436
437/// Uncompress a specific file from an archive. The `source` is used as a
438/// reader, the `target` as a writer and the `path` is the relative path for
439/// the file to be extracted from the archive.
440///
441/// # Example
442///
443/// ```no_run
444/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
445/// use compress_tools::*;
446/// use std::fs::File;
447///
448/// let mut source = File::open("tree.tar.gz")?;
449/// let mut target = Vec::default();
450/// let decode_utf8 = |bytes: &[u8]| Ok(std::str::from_utf8(bytes)?.to_owned());
451///
452/// uncompress_archive_file_with_encoding(&mut source, &mut target, "file/path", decode_utf8)?;
453/// # Ok(())
454/// # }
455/// ```
456pub fn uncompress_archive_file_with_encoding<R, W>(
457    source: R,
458    target: W,
459    path: &str,
460    decode: DecodeCallback,
461) -> Result<usize>
462where
463    R: Read + Seek,
464    W: Write,
465{
466    let _utf8_guard = ffi::UTF8LocaleGuard::new();
467    run_with_archive(
468        Ownership::Ignore,
469        source,
470        |archive_reader, _, mut entry| unsafe {
471            loop {
472                match ffi::archive_read_next_header(archive_reader, &mut entry) {
473                    ffi::ARCHIVE_EOF => {
474                        return Err(io::Error::new(
475                            io::ErrorKind::NotFound,
476                            format!("path {} doesn't exist inside archive", path),
477                        )
478                        .into())
479                    }
480                    value => archive_result(value, archive_reader)?,
481                }
482
483                let _utf8_guard = ffi::WindowsUTF8LocaleGuard::new();
484                let cstr = libarchive_entry_pathname(entry)?;
485                let file_name = decode(cstr.to_bytes())?;
486                if file_name == path {
487                    break;
488                }
489            }
490
491            if libarchive_entry_is_dir(entry) {
492                return Ok(0);
493            }
494            libarchive_write_data_block(archive_reader, target)
495        },
496    )
497}
498
499/// Uncompress a specific file from an archive. The `source` is used as a
500/// reader, the `target` as a writer and the `path` is the relative path for
501/// the file to be extracted from the archive.
502///
503/// # Example
504///
505/// ```no_run
506/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
507/// use compress_tools::*;
508/// use std::fs::File;
509///
510/// let mut source = File::open("tree.tar.gz")?;
511/// let mut target = Vec::default();
512///
513/// uncompress_archive_file(&mut source, &mut target, "file/path")?;
514/// # Ok(())
515/// # }
516/// ```
517pub fn uncompress_archive_file<R, W>(source: R, target: W, path: &str) -> Result<usize>
518where
519    R: Read + Seek,
520    W: Write,
521{
522    uncompress_archive_file_with_encoding(source, target, path, decode_utf8)
523}
524
525fn run_with_archive<F, R, T>(ownership: Ownership, mut reader: R, f: F) -> Result<T>
526where
527    F: FnOnce(*mut ffi::archive, *mut ffi::archive, *mut ffi::archive_entry) -> Result<T>,
528    R: Read + Seek,
529{
530    let _utf8_guard = ffi::UTF8LocaleGuard::new();
531    // libarchive only sniffs the format from offset 0.
532    reader.seek(SeekFrom::Start(0))?;
533    zip_preflight::reject_unsupported_zip_methods(&mut reader)?;
534    unsafe {
535        let archive_entry: *mut ffi::archive_entry = std::ptr::null_mut();
536        let archive_reader = ffi::archive_read_new();
537        let archive_writer = ffi::archive_write_disk_new();
538
539        let res = (|| {
540            archive_result(
541                ffi::archive_read_support_filter_all(archive_reader),
542                archive_reader,
543            )?;
544
545            archive_result(
546                ffi::archive_read_set_seek_callback(archive_reader, Some(libarchive_seek_callback)),
547                archive_reader,
548            )?;
549
550            let mut writer_flags = ffi::ARCHIVE_EXTRACT_TIME
551                | ffi::ARCHIVE_EXTRACT_PERM
552                | ffi::ARCHIVE_EXTRACT_ACL
553                | ffi::ARCHIVE_EXTRACT_FFLAGS
554                | ffi::ARCHIVE_EXTRACT_XATTR;
555
556            if let Ownership::Preserve = ownership {
557                writer_flags |= ffi::ARCHIVE_EXTRACT_OWNER;
558            };
559
560            archive_result(
561                ffi::archive_write_disk_set_options(archive_writer, writer_flags as i32),
562                archive_writer,
563            )?;
564            archive_result(
565                ffi::archive_write_disk_set_standard_lookup(archive_writer),
566                archive_writer,
567            )?;
568            archive_result(
569                ffi::archive_read_support_format_all(archive_reader),
570                archive_reader,
571            )?;
572
573            if archive_reader.is_null() || archive_writer.is_null() {
574                return Err(Error::NullArchive);
575            }
576
577            let mut pipe = SeekableReaderPipe {
578                reader: &mut reader,
579                buffer: &mut [0; READER_BUFFER_SIZE],
580            };
581
582            archive_result(
583                ffi::archive_read_open(
584                    archive_reader,
585                    std::ptr::addr_of_mut!(pipe) as *mut c_void,
586                    None,
587                    Some(libarchive_seekable_read_callback),
588                    None,
589                ),
590                archive_reader,
591            )?;
592
593            f(archive_reader, archive_writer, archive_entry)
594        })();
595
596        archive_result(ffi::archive_read_close(archive_reader), archive_reader)?;
597        archive_result(ffi::archive_read_free(archive_reader), archive_reader)?;
598
599        archive_result(ffi::archive_write_close(archive_writer), archive_writer)?;
600        archive_result(ffi::archive_write_free(archive_writer), archive_writer)?;
601
602        ffi::archive_entry_free(archive_entry);
603
604        res
605    }
606}
607
608fn run_with_unseekable_archive<F, R, T>(mut reader: R, f: F) -> Result<T>
609where
610    F: FnOnce(*mut ffi::archive, *mut ffi::archive, *mut ffi::archive_entry) -> Result<T>,
611    R: Read,
612{
613    let _utf8_guard = ffi::UTF8LocaleGuard::new();
614    unsafe {
615        let archive_entry: *mut ffi::archive_entry = std::ptr::null_mut();
616        let archive_reader = ffi::archive_read_new();
617        let archive_writer = ffi::archive_write_disk_new();
618
619        let res = (|| {
620            archive_result(
621                ffi::archive_read_support_filter_all(archive_reader),
622                archive_reader,
623            )?;
624
625            archive_result(
626                ffi::archive_read_support_format_raw(archive_reader),
627                archive_reader,
628            )?;
629
630            if archive_reader.is_null() || archive_writer.is_null() {
631                return Err(Error::NullArchive);
632            }
633
634            let mut pipe = ReaderPipe {
635                reader: &mut reader,
636                buffer: &mut [0; READER_BUFFER_SIZE],
637            };
638
639            archive_result(
640                ffi::archive_read_open(
641                    archive_reader,
642                    std::ptr::addr_of_mut!(pipe) as *mut c_void,
643                    None,
644                    Some(libarchive_read_callback),
645                    None,
646                ),
647                archive_reader,
648            )?;
649
650            f(archive_reader, archive_writer, archive_entry)
651        })();
652
653        archive_result(ffi::archive_read_close(archive_reader), archive_reader)?;
654        archive_result(ffi::archive_read_free(archive_reader), archive_reader)?;
655
656        archive_result(ffi::archive_write_close(archive_writer), archive_writer)?;
657        archive_result(ffi::archive_write_free(archive_writer), archive_writer)?;
658
659        ffi::archive_entry_free(archive_entry);
660
661        res
662    }
663}
664
665// This ensures we're not affected by the zip-slip vulnerability. In summary, it
666// uses relative destination paths to unpack files in unexpected places. This
667// also handles absolute paths, where the leading '/' will be stripped, matching
668// behaviour from gnu tar and bsdtar.
669//
670// More details can be found at: http://snyk.io/research/zip-slip-vulnerability
671fn sanitize_destination_path(dest: &Path) -> Result<&Path> {
672    let dest = dest.strip_prefix("/").unwrap_or(dest);
673
674    dest.components()
675        .find(|c| c == &Component::ParentDir)
676        .map_or(Ok(dest), |_| {
677            Err(std::io::Error::new(
678                std::io::ErrorKind::InvalidData,
679                "cannot use relative destination directory",
680            )
681            .into())
682        })
683}
684
685fn libarchive_copy_data(
686    archive_reader: *mut ffi::archive,
687    archive_writer: *mut ffi::archive,
688) -> Result<()> {
689    let mut buffer = std::ptr::null();
690    let mut offset = 0;
691    let mut size = 0;
692
693    unsafe {
694        loop {
695            match ffi::archive_read_data_block(archive_reader, &mut buffer, &mut size, &mut offset)
696            {
697                ffi::ARCHIVE_EOF => return Ok(()),
698                value => archive_result(value, archive_reader)?,
699            }
700
701            archive_result_strict(
702                /* Might depending on the version of libarchive on success
703                 * return 0 or the number of bytes written,
704                 * see man:archive_write_data(3) */
705                match ffi::archive_write_data_block(archive_writer, buffer, size, offset) {
706                    x if x >= 0 => 0,
707                    x => i32::try_from(x).unwrap(),
708                },
709                archive_writer,
710            )?;
711        }
712    }
713}
714
715fn libarchive_entry_size(entry: *mut ffi::archive_entry) -> u64 {
716    // `st_size` is `i32` on Windows (see the `stat` struct above) and `i64`
717    // on Unix. Widen through `i64` to keep the cast platform-agnostic.
718    let size = unsafe { (*ffi::archive_entry_stat(entry)).st_size } as i64;
719    size.max(0) as u64
720}
721
722// Raw POSIX mode bits: `libc::S_IFDIR` is not exposed on Windows, where our
723// `stat` mirrors libarchive's own layout.
724pub(crate) fn libarchive_entry_is_dir(entry: *mut ffi::archive_entry) -> bool {
725    const S_IFMT: u32 = 0o170000;
726    const S_IFDIR: u32 = 0o040000;
727    let mode = unsafe { (*ffi::archive_entry_stat(entry)).st_mode } as u32;
728    (mode & S_IFMT) == S_IFDIR
729}
730
731fn libarchive_entry_pathname<'a>(entry: *mut ffi::archive_entry) -> Result<&'a CStr> {
732    let pathname = unsafe { ffi::archive_entry_pathname(entry) };
733    if pathname.is_null() {
734        return Err(io::Error::new(
735            io::ErrorKind::InvalidData,
736            "archive entry has unreadable filename.".to_string(),
737        )
738        .into());
739    }
740
741    Ok(unsafe { CStr::from_ptr(pathname) })
742}
743
744unsafe fn libarchive_write_data_block<W>(
745    archive_reader: *mut ffi::archive,
746    mut target: W,
747) -> Result<usize>
748where
749    W: Write,
750{
751    let mut buffer = std::ptr::null();
752    let mut offset = 0;
753    let mut size = 0;
754    let mut written = 0;
755
756    loop {
757        match ffi::archive_read_data_block(archive_reader, &mut buffer, &mut size, &mut offset) {
758            ffi::ARCHIVE_EOF => return Ok(written),
759            value => archive_result(value, archive_reader)?,
760        }
761
762        if size == 0 {
763            continue;
764        }
765
766        let content = slice::from_raw_parts(buffer as *const u8, size);
767        target.write_all(content)?;
768        written += size;
769    }
770}
771
772unsafe extern "C" fn libarchive_seek_callback(
773    _: *mut ffi::archive,
774    client_data: *mut c_void,
775    offset: ffi::la_int64_t,
776    whence: c_int,
777) -> i64 {
778    let pipe = (client_data as *mut SeekableReaderPipe).as_mut().unwrap();
779    let whence = match whence {
780        0 => SeekFrom::Start(offset as u64),
781        1 => SeekFrom::Current(offset),
782        2 => SeekFrom::End(offset),
783        _ => return -1,
784    };
785
786    match pipe.reader.seek(whence) {
787        Ok(offset) => offset as i64,
788        Err(_) => -1,
789    }
790}
791
792unsafe extern "C" fn libarchive_seekable_read_callback(
793    archive: *mut ffi::archive,
794    client_data: *mut c_void,
795    buffer: *mut *const c_void,
796) -> ffi::la_ssize_t {
797    let pipe = (client_data as *mut SeekableReaderPipe).as_mut().unwrap();
798
799    *buffer = pipe.buffer.as_ptr() as *const c_void;
800
801    match pipe.reader.read(pipe.buffer) {
802        Ok(size) => size as ffi::la_ssize_t,
803        Err(e) => {
804            let description = CString::new(e.to_string()).unwrap();
805
806            ffi::archive_set_error(archive, e.raw_os_error().unwrap_or(0), description.as_ptr());
807
808            -1
809        }
810    }
811}
812
813unsafe extern "C" fn libarchive_read_callback(
814    archive: *mut ffi::archive,
815    client_data: *mut c_void,
816    buffer: *mut *const c_void,
817) -> ffi::la_ssize_t {
818    let pipe = (client_data as *mut ReaderPipe).as_mut().unwrap();
819
820    *buffer = pipe.buffer.as_ptr() as *const c_void;
821
822    match pipe.reader.read(pipe.buffer) {
823        Ok(size) => size as ffi::la_ssize_t,
824        Err(e) => {
825            let description = CString::new(e.to_string()).unwrap();
826
827            ffi::archive_set_error(archive, e.raw_os_error().unwrap_or(0), description.as_ptr());
828
829            -1
830        }
831    }
832}