1#![forbid(unsafe_code)]
22
23#[macro_use]
24extern crate log;
25
26#[cfg(unix)]
27use std::os::unix::fs::PermissionsExt;
28
29use std::io::{Read, Seek};
30use std::path::{Path, PathBuf, StripPrefixError};
31use std::{fs, io};
32use thiserror::Error;
33
34pub use zip::result::ZipError;
36
37#[derive(Error, Debug)]
39pub enum ZipExtractError {
40 #[error(transparent)]
41 Io(#[from] io::Error),
42 #[error(transparent)]
43 Zip(#[from] ZipError),
44 #[error("Failed to strip toplevel directory {} from {}: {error}", .toplevel.to_string_lossy(), .path.to_string_lossy())]
45 StripToplevel {
46 toplevel: PathBuf,
47 path: PathBuf,
48 error: StripPrefixError,
49 },
50}
51
52pub fn extract<S: Read + Seek>(
71 source: S,
72 target_dir: &Path,
73 strip_toplevel: bool,
74) -> Result<(), ZipExtractError> {
75 if !target_dir.exists() {
76 fs::create_dir(target_dir)?;
77 }
78
79 let mut archive = zip::ZipArchive::new(source)?;
80
81 let do_strip_toplevel = strip_toplevel && has_toplevel(&mut archive)?;
82
83 debug!("Extracting to {}", target_dir.to_string_lossy());
84 for i in 0..archive.len() {
85 let mut file = archive.by_index(i)?;
86 let mut relative_path = file.mangled_name();
87
88 if do_strip_toplevel {
89 let base = relative_path
90 .components()
91 .take(1)
92 .fold(PathBuf::new(), |mut p, c| {
93 p.push(c);
94 p
95 });
96 relative_path = relative_path
97 .strip_prefix(&base)
98 .map_err(|error| ZipExtractError::StripToplevel {
99 toplevel: base,
100 path: relative_path.clone(),
101 error,
102 })?
103 .to_path_buf()
104 }
105
106 if relative_path.to_string_lossy().is_empty() {
107 continue;
109 }
110
111 let mut outpath = target_dir.to_path_buf();
112 outpath.push(relative_path);
113
114 trace!(
115 "Extracting {} to {}",
116 file.name(),
117 outpath.to_string_lossy()
118 );
119 if file.name().ends_with('/') {
120 fs::create_dir_all(&outpath)?;
121 } else {
122 if let Some(p) = outpath.parent() {
123 if !p.exists() {
124 fs::create_dir_all(p)?;
125 }
126 }
127 let mut outfile = fs::File::create(&outpath)?;
128 io::copy(&mut file, &mut outfile)?;
129 }
130
131 #[cfg(unix)]
132 set_unix_mode(&file, &outpath)?;
133 }
134
135 debug!("Extracted {} files", archive.len());
136 Ok(())
137}
138
139fn has_toplevel<S: Read + Seek>(
140 archive: &mut zip::ZipArchive<S>,
141) -> Result<bool, zip::result::ZipError> {
142 let mut toplevel_dir: Option<PathBuf> = None;
143 if archive.len() < 2 {
144 return Ok(false);
145 }
146
147 for i in 0..archive.len() {
148 let file = archive.by_index(i)?.mangled_name();
149 if let Some(toplevel_dir) = &toplevel_dir {
150 if !file.starts_with(toplevel_dir) {
151 trace!("Found different toplevel directory");
152 return Ok(false);
153 }
154 } else {
155 let comp: PathBuf = file.components().take(1).collect();
157 trace!(
158 "Checking if path component {} is the only toplevel directory",
159 comp.to_string_lossy()
160 );
161 toplevel_dir = Some(comp);
162 }
163 }
164 trace!("Found no other toplevel directory");
165 Ok(true)
166}
167
168#[cfg(unix)]
169fn set_unix_mode<R: Read>(file: &zip::read::ZipFile<R>, outpath: &Path) -> io::Result<()> {
170 if let Some(m) = file.unix_mode() {
171 fs::set_permissions(outpath, PermissionsExt::from_mode(m))?
172 }
173 Ok(())
174}