zip_extract/
lib.rs

1//!# Deprecated
2//!
3//! zip-extract was born out of frustration with the zip crate's tedious extraction methods.
4//! Things have changed: [`ZipArchive::extract`](https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract)
5//! and [`ZipArchive::extract_unwrapped_root_dir`](https://docs.rs/zip/latest/zip/read/struct.ZipArchive.html#method.extract_unwrapped_root_dir)
6//! provide the same functionality as zip-extract, without a wrapper crate. Please use them instead.
7//!
8//! ---
9//!
10//! # zip-extract
11//! zip-extract's primary goal is simple: Automate tedious zip extraction. Ever wanted to just unpack
12//! an archive somewhere? Well, here you go:
13//!
14//! ## Usage
15//! See `extract` for details.
16//!
17//! ```ignore
18//! let archive: Vec<u8> = download_my_archive()?;
19//! let target_dir = PathBuf::from("my_target_dir"); // Doesn't need to exist
20//!
21//! // The third parameter allows you to strip away toplevel directories.
22//! // If `archive` contained a single folder, that folder's contents would be extracted instead.
23//! zip_extract::extract(Cursor::new(archive), &target_dir, true)?;
24//! ```
25//!
26//! ## Features
27//! All features are passed through to `zip2`, refer to
28//! [the documentation](https://docs.rs/crate/zip/2/features) for defaults and a list of features.
29
30#![forbid(unsafe_code)]
31
32#[macro_use]
33extern crate log;
34
35#[cfg(unix)]
36use std::os::unix::fs::PermissionsExt;
37
38use std::io::{Read, Seek};
39use std::path::{Path, PathBuf, StripPrefixError};
40use std::{fs, io};
41use thiserror::Error;
42
43/// Re-export of zip's error type, for convenience.
44pub use zip::result::ZipError;
45
46/// zip-extract's error type
47#[derive(Error, Debug)]
48pub enum ZipExtractError {
49    #[error(transparent)]
50    Io(#[from] io::Error),
51    #[error(transparent)]
52    Zip(#[from] ZipError),
53    #[error("Failed to strip toplevel directory {} from {}: {error}", .toplevel.to_string_lossy(), .path.to_string_lossy())]
54    StripToplevel {
55        toplevel: PathBuf,
56        path: PathBuf,
57        error: StripPrefixError,
58    },
59}
60
61/// Extracts a zip archive into `target_dir`.
62///
63/// `target_dir` is created if it doesn't exist. Will error if `target_dir.parent()` doesn't exist.
64///
65/// If `strip_toplevel` is true, will strip away the topmost directory. `strip_toplevel` only applies
66/// if all files and directories within the archive are descendants of the toplevel directory.
67///
68/// If you want to read from a source that doesn't implement Seek, you can wrap it into a Cursor:
69/// ```
70/// use std::io::Cursor;
71/// use std::path::PathBuf;
72///
73/// let bytes: Vec<u8> = vec![];
74/// let target = PathBuf::from("/tmp/target-directory");
75/// zip_extract::extract(Cursor::new(bytes), &target, false);
76/// ```
77///
78/// If on unix, `extract` will preserve permissions while extracting.
79#[deprecated = "zip-extract will no longer be maintained, since zip>=2.4.0 supports all its features via ZipArchive."]
80pub fn extract<S: Read + Seek>(
81    source: S,
82    target_dir: &Path,
83    strip_toplevel: bool,
84) -> Result<(), ZipExtractError> {
85    if !target_dir.exists() {
86        fs::create_dir(target_dir)?;
87    }
88
89    let mut archive = zip::ZipArchive::new(source)?;
90
91    let do_strip_toplevel = strip_toplevel && has_toplevel(&mut archive)?;
92
93    debug!("Extracting to {}", target_dir.to_string_lossy());
94    for i in 0..archive.len() {
95        let mut file = archive.by_index(i)?;
96        let mut relative_path = file.mangled_name();
97
98        if do_strip_toplevel {
99            let base = relative_path
100                .components()
101                .take(1)
102                .fold(PathBuf::new(), |mut p, c| {
103                    p.push(c);
104                    p
105                });
106            relative_path = relative_path
107                .strip_prefix(&base)
108                .map_err(|error| ZipExtractError::StripToplevel {
109                    toplevel: base,
110                    path: relative_path.clone(),
111                    error,
112                })?
113                .to_path_buf()
114        }
115
116        if relative_path.to_string_lossy().is_empty() {
117            // Top-level directory
118            continue;
119        }
120
121        let mut outpath = target_dir.to_path_buf();
122        outpath.push(relative_path);
123
124        trace!(
125            "Extracting {} to {}",
126            file.name(),
127            outpath.to_string_lossy()
128        );
129        if file.name().ends_with('/') {
130            fs::create_dir_all(&outpath)?;
131        } else {
132            if let Some(p) = outpath.parent() {
133                if !p.exists() {
134                    fs::create_dir_all(p)?;
135                }
136            }
137            let mut outfile = fs::File::create(&outpath)?;
138            io::copy(&mut file, &mut outfile)?;
139        }
140
141        #[cfg(unix)]
142        set_unix_mode(&file, &outpath)?;
143    }
144
145    debug!("Extracted {} files", archive.len());
146    Ok(())
147}
148
149fn has_toplevel<S: Read + Seek>(
150    archive: &mut zip::ZipArchive<S>,
151) -> Result<bool, zip::result::ZipError> {
152    let mut toplevel_dir: Option<PathBuf> = None;
153    if archive.len() < 2 {
154        return Ok(false);
155    }
156
157    for i in 0..archive.len() {
158        let file = archive.by_index(i)?.mangled_name();
159        if let Some(toplevel_dir) = &toplevel_dir {
160            if !file.starts_with(toplevel_dir) {
161                trace!("Found different toplevel directory");
162                return Ok(false);
163            }
164        } else {
165            // First iteration
166            let comp: PathBuf = file.components().take(1).collect();
167            trace!(
168                "Checking if path component {} is the only toplevel directory",
169                comp.to_string_lossy()
170            );
171            toplevel_dir = Some(comp);
172        }
173    }
174    trace!("Found no other toplevel directory");
175    Ok(true)
176}
177
178#[cfg(unix)]
179fn set_unix_mode<R: Read>(file: &zip::read::ZipFile<R>, outpath: &Path) -> io::Result<()> {
180    if let Some(m) = file.unix_mode() {
181        fs::set_permissions(outpath, PermissionsExt::from_mode(m))?
182    }
183    Ok(())
184}