this repo has no description
1use crate::utils::*;
2use anyhow::bail;
3use clap::Args;
4use std::fs::File;
5use std::io::{self, Seek, SeekFrom, Write};
6use std::path::Path;
7use tempfile::tempfile;
8use zip::read::ZipArchive;
9use zip::write::FileOptions;
10use zip::{CompressionMethod, ZipWriter};
11
12#[derive(Args, Debug)]
13pub struct ZipArgs {
14 #[clap(flatten)]
15 pub common_args: CommonArgs,
16}
17
18#[derive(Default)]
19pub struct Zip {}
20
21impl Zip {
22 pub fn new(_args: &ZipArgs) -> Zip {
23 Zip {}
24 }
25
26 fn compress_to_file<W: Write + Seek>(&self, input: CmprssInput, writer: W) -> Result {
27 let mut zip_writer = ZipWriter::new(writer);
28 let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated);
29
30 match input {
31 CmprssInput::Path(paths) => {
32 for path in paths {
33 if path.is_file() {
34 let name = path.file_name().unwrap().to_string_lossy();
35 zip_writer.start_file(name, options)?;
36 let mut f = File::open(&path)?;
37 io::copy(&mut f, &mut zip_writer)?;
38 } else if path.is_dir() {
39 // Use the directory as the base and add its contents
40 let base = path.parent().unwrap_or(&path);
41 add_directory(&mut zip_writer, base, &path)?;
42 } else {
43 bail!("unsupported file type for zip compression");
44 }
45 }
46 }
47 CmprssInput::Pipe(mut pipe) => {
48 // For pipe input, we'll create a single file named "archive"
49 zip_writer.start_file("archive", options)?;
50 io::copy(&mut pipe, &mut zip_writer)?;
51 }
52 CmprssInput::Reader(_) => {
53 bail!("Cannot zip a reader input");
54 }
55 }
56
57 zip_writer.finish()?;
58 Ok(())
59 }
60}
61
62impl Compressor for Zip {
63 fn name(&self) -> &str {
64 "zip"
65 }
66
67 /// Zip extracts to a directory by default
68 fn default_extracted_target(&self) -> ExtractedTarget {
69 ExtractedTarget::Directory
70 }
71
72 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
73 match output {
74 CmprssOutput::Path(ref path) => {
75 let file = File::create(path)?;
76 self.compress_to_file(input, file)
77 }
78 CmprssOutput::Pipe(mut pipe) => {
79 // Create a temporary file to write the zip to
80 let mut temp_file = tempfile()?;
81 self.compress_to_file(input, &mut temp_file)?;
82
83 // Reset the file position to the beginning
84 temp_file.seek(SeekFrom::Start(0))?;
85
86 // Copy the temporary file to the pipe
87 io::copy(&mut temp_file, &mut pipe)?;
88 Ok(())
89 }
90 CmprssOutput::Writer(mut writer) => {
91 let mut temp_file = tempfile()?;
92 self.compress_to_file(input, &mut temp_file)?;
93 temp_file.seek(SeekFrom::Start(0))?;
94 io::copy(&mut temp_file, &mut writer)?;
95 Ok(())
96 }
97 }
98 }
99
100 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
101 match output {
102 CmprssOutput::Path(ref out_dir) => {
103 // Create the output directory if it doesn't exist
104 if !out_dir.exists() {
105 std::fs::create_dir_all(out_dir)?;
106 } else if !out_dir.is_dir() {
107 bail!("zip extraction output must be a directory");
108 }
109
110 match input {
111 CmprssInput::Path(paths) => {
112 if paths.len() != 1 {
113 bail!("zip extraction expects a single archive file");
114 }
115 let file = File::open(&paths[0])?;
116 let mut archive = ZipArchive::new(file)?;
117 Ok(archive.extract(out_dir)?)
118 }
119 CmprssInput::Pipe(mut pipe) => {
120 // Create a temporary file to store the zip content
121 let mut temp_file = tempfile()?;
122
123 // Copy from pipe to temporary file
124 io::copy(&mut pipe, &mut temp_file)?;
125
126 // Reset the file position to the beginning
127 temp_file.seek(SeekFrom::Start(0))?;
128
129 // Extract from the temporary file
130 let mut archive = ZipArchive::new(temp_file)?;
131 Ok(archive.extract(out_dir)?)
132 }
133 CmprssInput::Reader(_) => {
134 bail!(
135 "Cannot extract from a reader input for zip (requires seekable input)"
136 )
137 }
138 }
139 }
140 CmprssOutput::Pipe(_) => bail!("zip extraction to stdout is not supported"),
141 CmprssOutput::Writer(mut writer) => match input {
142 CmprssInput::Path(paths) => {
143 if paths.len() != 1 {
144 bail!("zip extraction expects a single archive file");
145 }
146 let mut file = File::open(&paths[0])?;
147 io::copy(&mut file, &mut writer)?;
148 Ok(())
149 }
150 CmprssInput::Pipe(mut pipe) => {
151 io::copy(&mut pipe, &mut writer)?;
152 Ok(())
153 }
154 CmprssInput::Reader(mut reader) => {
155 io::copy(&mut reader, &mut writer)?;
156 Ok(())
157 }
158 },
159 }
160 }
161
162 fn list(&self, input: CmprssInput) -> Result {
163 // ZipArchive requires a seekable reader. For non-path inputs we must
164 // buffer into a tempfile first.
165 let stdout = io::stdout();
166 let mut out = stdout.lock();
167 match input {
168 CmprssInput::Path(paths) => {
169 if paths.len() != 1 {
170 bail!("zip listing expects a single archive file");
171 }
172 let archive = ZipArchive::new(File::open(&paths[0])?)?;
173 for name in archive.file_names() {
174 writeln!(out, "{}", name)?;
175 }
176 }
177 CmprssInput::Pipe(mut pipe) => {
178 let mut temp = tempfile()?;
179 io::copy(&mut pipe, &mut temp)?;
180 temp.seek(SeekFrom::Start(0))?;
181 let archive = ZipArchive::new(temp)?;
182 for name in archive.file_names() {
183 writeln!(out, "{}", name)?;
184 }
185 }
186 CmprssInput::Reader(mut reader) => {
187 let mut temp = tempfile()?;
188 io::copy(&mut reader, &mut temp)?;
189 temp.seek(SeekFrom::Start(0))?;
190 let archive = ZipArchive::new(temp)?;
191 for name in archive.file_names() {
192 writeln!(out, "{}", name)?;
193 }
194 }
195 }
196 Ok(())
197 }
198}
199
200fn add_directory<W: Write + Seek>(zip: &mut ZipWriter<W>, base: &Path, path: &Path) -> Result {
201 for entry in std::fs::read_dir(path)? {
202 let entry = entry?;
203 let entry_path = entry.path();
204 // Get relative path for archive entry
205 let name = entry_path
206 .strip_prefix(base)
207 .unwrap()
208 .to_string_lossy()
209 .replace('\\', "/");
210 if entry_path.is_file() {
211 let options =
212 FileOptions::<()>::default().compression_method(CompressionMethod::Deflated);
213 zip.start_file(name, options)?;
214 let mut f = File::open(&entry_path)?;
215 io::copy(&mut f, zip)?;
216 } else if entry_path.is_dir() {
217 // Ensure directory entry ends with '/'
218 let dir_name = name.clone() + "/";
219 zip.add_directory(
220 dir_name,
221 FileOptions::<()>::default().compression_method(CompressionMethod::Deflated),
222 )?;
223 add_directory(zip, base, &entry_path)?;
224 }
225 }
226 Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::test_utils::*;
233 use assert_fs::prelude::*;
234 use predicates::prelude::*;
235 use std::path::PathBuf;
236
237 /// Test the basic interface of the Zip compressor
238 #[test]
239 fn test_zip_interface() {
240 let compressor = Zip::default();
241 test_compressor_interface(&compressor, "zip", Some("zip"));
242 }
243
244 /// Test the default compression level
245 #[test]
246 fn test_zip_default_compression() -> Result {
247 let compressor = Zip::default();
248 test_compression(&compressor)
249 }
250
251 /// Test zip-specific functionality: directory handling
252 #[test]
253 fn test_directory_handling() -> Result {
254 let compressor = Zip::default();
255 let dir = assert_fs::TempDir::new()?;
256 let file_path = dir.child("file.txt");
257 file_path.write_str("directory test data")?;
258 let working_dir = assert_fs::TempDir::new()?;
259 let archive = working_dir.child("dir_archive.zip");
260 archive.assert(predicate::path::missing());
261
262 compressor.compress(
263 CmprssInput::Path(vec![dir.path().to_path_buf()]),
264 CmprssOutput::Path(archive.path().to_path_buf()),
265 )?;
266 archive.assert(predicate::path::is_file());
267
268 let extract_dir = working_dir.child("extracted");
269 std::fs::create_dir_all(extract_dir.path())?;
270 compressor.extract(
271 CmprssInput::Path(vec![archive.path().to_path_buf()]),
272 CmprssOutput::Path(extract_dir.path().to_path_buf()),
273 )?;
274 // When extracting a directory from a zip, the directory name is included in the path
275 // Since the archive stores the entire directory, the extracted file is contained in the directory
276 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
277 extract_dir
278 .child(dir_name)
279 .child("file.txt")
280 .assert(predicate::path::eq_file(file_path.path()));
281 Ok(())
282 }
283}