1#![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
43pub use zip::result::ZipError;
45
46#[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#[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 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 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}