this repo has no description
1use crate::utils::*;
2use clap::Args;
3use std::fs::File;
4use std::io::{self, Seek, SeekFrom, Write};
5use std::path::Path;
6use tempfile::tempfile;
7use zip::read::ZipArchive;
8use zip::write::FileOptions;
9use zip::{CompressionMethod, ZipWriter};
10
11#[derive(Args, Debug)]
12pub struct ZipArgs {
13 #[clap(flatten)]
14 pub common_args: CommonArgs,
15}
16
17#[derive(Default)]
18pub struct Zip {}
19
20impl Zip {
21 pub fn new(_args: &ZipArgs) -> Zip {
22 Zip {}
23 }
24
25 fn compress_to_file<W: Write + Seek>(
26 &self,
27 input: CmprssInput,
28 writer: W,
29 ) -> Result<(), io::Error> {
30 let mut zip_writer = ZipWriter::new(writer);
31 let options = FileOptions::default().compression_method(CompressionMethod::Deflated);
32
33 match input {
34 CmprssInput::Path(paths) => {
35 for path in paths {
36 if path.is_file() {
37 let name = path.file_name().unwrap().to_string_lossy();
38 zip_writer.start_file(name, options)?;
39 let mut f = File::open(&path)?;
40 io::copy(&mut f, &mut zip_writer)?;
41 } else if path.is_dir() {
42 // Use the directory as the base and add its contents
43 let base = path.parent().unwrap_or(&path);
44 add_directory(&mut zip_writer, base, &path)?;
45 } else {
46 return cmprss_error("unsupported file type for zip compression");
47 }
48 }
49 }
50 CmprssInput::Pipe(mut pipe) => {
51 // For pipe input, we'll create a single file named "archive"
52 zip_writer.start_file("archive", options)?;
53 io::copy(&mut pipe, &mut zip_writer)?;
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<(), io::Error> {
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 }
91 }
92
93 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> {
94 match output {
95 CmprssOutput::Path(ref out_dir) => {
96 // Create the output directory if it doesn't exist
97 if !out_dir.exists() {
98 std::fs::create_dir_all(out_dir)?;
99 } else if !out_dir.is_dir() {
100 return cmprss_error("zip extraction output must be a directory");
101 }
102
103 match input {
104 CmprssInput::Path(paths) => {
105 if paths.len() != 1 {
106 return cmprss_error("zip extraction expects a single archive file");
107 }
108 let file = File::open(&paths[0])?;
109 let mut archive = ZipArchive::new(file)?;
110 archive
111 .extract(out_dir)
112 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
113 }
114 CmprssInput::Pipe(mut pipe) => {
115 // Create a temporary file to store the zip content
116 let mut temp_file = tempfile()?;
117
118 // Copy from pipe to temporary file
119 io::copy(&mut pipe, &mut temp_file)?;
120
121 // Reset the file position to the beginning
122 temp_file.seek(SeekFrom::Start(0))?;
123
124 // Extract from the temporary file
125 let mut archive = ZipArchive::new(temp_file)?;
126 archive
127 .extract(out_dir)
128 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
129 }
130 }
131 }
132 CmprssOutput::Pipe(_) => cmprss_error("zip extraction to stdout is not supported"),
133 }
134 }
135}
136
137fn add_directory<W: Write + Seek>(
138 zip: &mut ZipWriter<W>,
139 base: &Path,
140 path: &Path,
141) -> Result<(), io::Error> {
142 for entry in std::fs::read_dir(path)? {
143 let entry = entry?;
144 let entry_path = entry.path();
145 // Get relative path for archive entry
146 let name = entry_path
147 .strip_prefix(base)
148 .unwrap()
149 .to_string_lossy()
150 .replace('\\', "/");
151 if entry_path.is_file() {
152 let options = FileOptions::default().compression_method(CompressionMethod::Deflated);
153 zip.start_file(name, options)?;
154 let mut f = File::open(&entry_path)?;
155 io::copy(&mut f, zip)?;
156 } else if entry_path.is_dir() {
157 // Ensure directory entry ends with '/'
158 let dir_name = name.clone() + "/";
159 zip.add_directory(
160 dir_name,
161 FileOptions::default().compression_method(CompressionMethod::Deflated),
162 )?;
163 add_directory(zip, base, &entry_path)?;
164 }
165 }
166 Ok(())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::test_utils::*;
173 use assert_fs::prelude::*;
174 use predicates::prelude::*;
175 use std::path::PathBuf;
176
177 /// Test the basic interface of the Zip compressor
178 #[test]
179 fn test_zip_interface() {
180 let compressor = Zip::default();
181 test_compressor_interface(&compressor, "zip", Some("zip"));
182 }
183
184 /// Test the default compression level
185 #[test]
186 fn test_zip_default_compression() -> Result<(), io::Error> {
187 let compressor = Zip::default();
188 test_compression(&compressor)
189 }
190
191 /// Test zip-specific functionality: directory handling
192 #[test]
193 fn test_directory_handling() -> Result<(), Box<dyn std::error::Error>> {
194 let compressor = Zip::default();
195 let dir = assert_fs::TempDir::new()?;
196 let file_path = dir.child("file.txt");
197 file_path.write_str("directory test data")?;
198 let working_dir = assert_fs::TempDir::new()?;
199 let archive = working_dir.child("dir_archive.zip");
200 archive.assert(predicate::path::missing());
201
202 compressor.compress(
203 CmprssInput::Path(vec![dir.path().to_path_buf()]),
204 CmprssOutput::Path(archive.path().to_path_buf()),
205 )?;
206 archive.assert(predicate::path::is_file());
207
208 let extract_dir = working_dir.child("extracted");
209 std::fs::create_dir_all(extract_dir.path())?;
210 compressor.extract(
211 CmprssInput::Path(vec![archive.path().to_path_buf()]),
212 CmprssOutput::Path(extract_dir.path().to_path_buf()),
213 )?;
214 // When extracting a directory from a zip, the directory name is included in the path
215 // Since the archive stores the entire directory, the extracted file is contained in the directory
216 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
217 extract_dir
218 .child(dir_name)
219 .child("file.txt")
220 .assert(predicate::path::eq_file(file_path.path()));
221 Ok(())
222 }
223}