1use std::path::Path;
2
3use anyhow::{Context as _, Result};
4use async_zip::base::read;
5#[cfg(not(windows))]
6use futures::AsyncSeek;
7use futures::{AsyncRead, io::BufReader};
8
9#[cfg(windows)]
10pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
11 let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader));
12
13 let destination = &destination
14 .canonicalize()
15 .unwrap_or_else(|_| destination.to_path_buf());
16
17 while let Some(mut item) = reader.next_with_entry().await? {
18 let entry_reader = item.reader_mut();
19 let entry = entry_reader.entry();
20 let path = destination.join(
21 entry
22 .filename()
23 .as_str()
24 .context("reading zip entry file name")?,
25 );
26
27 if entry
28 .dir()
29 .with_context(|| format!("reading zip entry metadata for path {path:?}"))?
30 {
31 std::fs::create_dir_all(&path)
32 .with_context(|| format!("creating directory {path:?}"))?;
33 } else {
34 let parent_dir = path
35 .parent()
36 .with_context(|| format!("no parent directory for {path:?}"))?;
37 std::fs::create_dir_all(parent_dir)
38 .with_context(|| format!("creating parent directory {parent_dir:?}"))?;
39 let mut file = smol::fs::File::create(&path)
40 .await
41 .with_context(|| format!("creating file {path:?}"))?;
42 futures::io::copy(entry_reader, &mut file)
43 .await
44 .with_context(|| format!("extracting into file {path:?}"))?;
45 }
46
47 reader = item.skip().await.context("reading next zip entry")?;
48 }
49
50 Ok(())
51}
52
53#[cfg(not(windows))]
54pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
55 let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?);
64 futures::io::copy(&mut BufReader::new(reader), &mut file)
65 .await
66 .context("saving archive contents into the temporary file")?;
67 extract_seekable_zip(destination, file).await
68}
69
70#[cfg(not(windows))]
71pub async fn extract_seekable_zip<R: AsyncRead + AsyncSeek + Unpin>(
72 destination: &Path,
73 reader: R,
74) -> Result<()> {
75 let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader))
76 .await
77 .context("reading the zip archive")?;
78 let destination = &destination
79 .canonicalize()
80 .unwrap_or_else(|_| destination.to_path_buf());
81 for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() {
82 let path = destination.join(
83 entry
84 .filename()
85 .as_str()
86 .context("reading zip entry file name")?,
87 );
88
89 if entry
90 .dir()
91 .with_context(|| format!("reading zip entry metadata for path {path:?}"))?
92 {
93 std::fs::create_dir_all(&path)
94 .with_context(|| format!("creating directory {path:?}"))?;
95 } else {
96 let parent_dir = path
97 .parent()
98 .with_context(|| format!("no parent directory for {path:?}"))?;
99 std::fs::create_dir_all(parent_dir)
100 .with_context(|| format!("creating parent directory {parent_dir:?}"))?;
101 let mut file = smol::fs::File::create(&path)
102 .await
103 .with_context(|| format!("creating file {path:?}"))?;
104 let mut entry_reader = reader
105 .reader_with_entry(i)
106 .await
107 .with_context(|| format!("reading entry for path {path:?}"))?;
108 futures::io::copy(&mut entry_reader, &mut file)
109 .await
110 .with_context(|| format!("extracting into file {path:?}"))?;
111
112 if let Some(perms) = entry.unix_permissions() {
113 use std::os::unix::fs::PermissionsExt;
114 let permissions = std::fs::Permissions::from_mode(u32::from(perms));
115 file.set_permissions(permissions)
116 .await
117 .with_context(|| format!("setting permissions for file {path:?}"))?;
118 }
119 }
120 }
121
122 Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127 use async_zip::ZipEntryBuilder;
128 use async_zip::base::write::ZipFileWriter;
129 use futures::{AsyncSeek, AsyncWriteExt};
130 use smol::io::Cursor;
131 use tempfile::TempDir;
132
133 use super::*;
134
135 async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
136 let mut out = smol::fs::File::create(dst).await?;
137 let mut writer = ZipFileWriter::new(&mut out);
138
139 for entry in walkdir::WalkDir::new(src_dir) {
140 let entry = entry?;
141 let path = entry.path();
142
143 if path.is_dir() {
144 continue;
145 }
146
147 let relative_path = path.strip_prefix(src_dir)?;
148 let data = smol::fs::read(&path).await?;
149
150 let filename = relative_path.display().to_string();
151
152 #[cfg(unix)]
153 {
154 let mut builder =
155 ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
156 use std::os::unix::fs::PermissionsExt;
157 let metadata = std::fs::metadata(path)?;
158 let perms = metadata.permissions().mode() as u16;
159 builder = builder.unix_permissions(perms);
160 writer.write_entry_whole(builder, &data).await?;
161 }
162 #[cfg(not(unix))]
163 {
164 let builder =
165 ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
166 writer.write_entry_whole(builder, &data).await?;
167 }
168 }
169
170 writer.close().await?;
171 out.flush().await?;
172
173 Ok(())
174 }
175
176 #[track_caller]
177 fn assert_file_content(path: &Path, content: &str) {
178 assert!(path.exists(), "file not found: {:?}", path);
179 let actual = std::fs::read_to_string(path).unwrap();
180 assert_eq!(actual, content);
181 }
182
183 #[track_caller]
184 fn make_test_data() -> TempDir {
185 let dir = tempfile::tempdir().unwrap();
186 let dst = dir.path();
187
188 std::fs::write(dst.join("test"), "Hello world.").unwrap();
189 std::fs::create_dir_all(dst.join("foo/bar")).unwrap();
190 std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();
191 std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();
192 std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
193
194 dir
195 }
196
197 async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin {
198 let data = smol::fs::read(&path).await.unwrap();
199 Cursor::new(data)
200 }
201
202 #[test]
203 fn test_extract_zip() {
204 let test_dir = make_test_data();
205 let zip_file = test_dir.path().join("test.zip");
206
207 smol::block_on(async {
208 compress_zip(test_dir.path(), &zip_file).await.unwrap();
209 let reader = read_archive(&zip_file).await;
210
211 let dir = tempfile::tempdir().unwrap();
212 let dst = dir.path();
213 extract_zip(dst, reader).await.unwrap();
214
215 assert_file_content(&dst.join("test"), "Hello world.");
216 assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
217 assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
218 assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
219 });
220 }
221
222 #[cfg(unix)]
223 #[test]
224 fn test_extract_zip_preserves_executable_permissions() {
225 use std::os::unix::fs::PermissionsExt;
226
227 smol::block_on(async {
228 let test_dir = tempfile::tempdir().unwrap();
229 let executable_path = test_dir.path().join("my_script");
230
231 std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();
233 let mut perms = std::fs::metadata(&executable_path).unwrap().permissions();
234 perms.set_mode(0o755); std::fs::set_permissions(&executable_path, perms).unwrap();
236
237 let zip_file = test_dir.path().join("test.zip");
239 compress_zip(test_dir.path(), &zip_file).await.unwrap();
240
241 let extract_dir = tempfile::tempdir().unwrap();
243 let reader = read_archive(&zip_file).await;
244 extract_zip(extract_dir.path(), reader).await.unwrap();
245
246 let extracted_path = extract_dir.path().join("my_script");
248 assert!(extracted_path.exists());
249 let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
250 assert_eq!(extracted_perms.mode() & 0o777, 0o755);
251 });
252 }
253}