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
163fn add_directory<W: Write + Seek>(zip: &mut ZipWriter<W>, base: &Path, path: &Path) -> Result {
164 for entry in std::fs::read_dir(path)? {
165 let entry = entry?;
166 let entry_path = entry.path();
167 // Get relative path for archive entry
168 let name = entry_path
169 .strip_prefix(base)
170 .unwrap()
171 .to_string_lossy()
172 .replace('\\', "/");
173 if entry_path.is_file() {
174 let options =
175 FileOptions::<()>::default().compression_method(CompressionMethod::Deflated);
176 zip.start_file(name, options)?;
177 let mut f = File::open(&entry_path)?;
178 io::copy(&mut f, zip)?;
179 } else if entry_path.is_dir() {
180 // Ensure directory entry ends with '/'
181 let dir_name = name.clone() + "/";
182 zip.add_directory(
183 dir_name,
184 FileOptions::<()>::default().compression_method(CompressionMethod::Deflated),
185 )?;
186 add_directory(zip, base, &entry_path)?;
187 }
188 }
189 Ok(())
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::test_utils::*;
196 use assert_fs::prelude::*;
197 use predicates::prelude::*;
198 use std::path::PathBuf;
199
200 /// Test the basic interface of the Zip compressor
201 #[test]
202 fn test_zip_interface() {
203 let compressor = Zip::default();
204 test_compressor_interface(&compressor, "zip", Some("zip"));
205 }
206
207 /// Test the default compression level
208 #[test]
209 fn test_zip_default_compression() -> Result {
210 let compressor = Zip::default();
211 test_compression(&compressor)
212 }
213
214 /// Test zip-specific functionality: directory handling
215 #[test]
216 fn test_directory_handling() -> Result {
217 let compressor = Zip::default();
218 let dir = assert_fs::TempDir::new()?;
219 let file_path = dir.child("file.txt");
220 file_path.write_str("directory test data")?;
221 let working_dir = assert_fs::TempDir::new()?;
222 let archive = working_dir.child("dir_archive.zip");
223 archive.assert(predicate::path::missing());
224
225 compressor.compress(
226 CmprssInput::Path(vec![dir.path().to_path_buf()]),
227 CmprssOutput::Path(archive.path().to_path_buf()),
228 )?;
229 archive.assert(predicate::path::is_file());
230
231 let extract_dir = working_dir.child("extracted");
232 std::fs::create_dir_all(extract_dir.path())?;
233 compressor.extract(
234 CmprssInput::Path(vec![archive.path().to_path_buf()]),
235 CmprssOutput::Path(extract_dir.path().to_path_buf()),
236 )?;
237 // When extracting a directory from a zip, the directory name is included in the path
238 // Since the archive stores the entire directory, the extracted file is contained in the directory
239 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
240 extract_dir
241 .child(dir_name)
242 .child("file.txt")
243 .assert(predicate::path::eq_file(file_path.path()));
244 Ok(())
245 }
246}