this repo has no description
1use crate::progress::{ProgressArgs, copy_with_progress};
2use crate::utils::*;
3use anyhow::bail;
4use clap::Args;
5use flate2::write::GzEncoder;
6use flate2::{Compression, read::GzDecoder};
7use std::fs::File;
8use std::io::{self, BufReader, BufWriter, Read, Write};
9
10#[derive(Args, Debug)]
11pub struct GzipArgs {
12 #[clap(flatten)]
13 pub common_args: CommonArgs,
14
15 #[clap(flatten)]
16 pub level_args: LevelArgs,
17
18 #[clap(flatten)]
19 pub progress_args: ProgressArgs,
20}
21
22pub struct Gzip {
23 pub compression_level: i32,
24 pub progress_args: ProgressArgs,
25}
26
27impl Default for Gzip {
28 fn default() -> Self {
29 let validator = DefaultCompressionValidator;
30 Gzip {
31 compression_level: validator.default_level(),
32 progress_args: ProgressArgs::default(),
33 }
34 }
35}
36
37impl Gzip {
38 pub fn new(args: &GzipArgs) -> Gzip {
39 let validator = DefaultCompressionValidator;
40 let level = args.level_args.level.level;
41
42 // Validate and clamp the level to gzip's valid range
43 let level = validator.validate_and_clamp_level(level);
44
45 Gzip {
46 compression_level: level,
47 progress_args: args.progress_args,
48 }
49 }
50}
51
52impl Compressor for Gzip {
53 /// The standard extension for the gzip format.
54 fn extension(&self) -> &str {
55 "gz"
56 }
57
58 /// Full name for gzip.
59 fn name(&self) -> &str {
60 "gzip"
61 }
62
63 /// Gzip extracts to a file by default
64 fn default_extracted_target(&self) -> ExtractedTarget {
65 ExtractedTarget::FILE
66 }
67
68 /// Compress an input file or pipe to a gzip archive
69 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
70 if let CmprssOutput::Path(out_path) = &output
71 && out_path.is_dir()
72 {
73 bail!(
74 "Gzip does not support compressing to a directory. Please specify an output file."
75 );
76 }
77 if let CmprssInput::Path(input_paths) = &input {
78 for x in input_paths {
79 if x.is_dir() {
80 bail!(
81 "Gzip does not support compressing a directory. Please specify only files."
82 );
83 }
84 }
85 }
86 let mut file_size = None;
87 let mut input_stream: Box<dyn Read + Send> = match input {
88 CmprssInput::Path(paths) => {
89 if paths.len() > 1 {
90 bail!("Multiple input files not supported for gzip");
91 }
92 let path = &paths[0];
93 file_size = Some(std::fs::metadata(path)?.len());
94 Box::new(BufReader::new(File::open(path)?))
95 }
96 CmprssInput::Pipe(stdin) => Box::new(BufReader::new(stdin)),
97 CmprssInput::Reader(reader) => reader.0,
98 };
99
100 if let CmprssOutput::Writer(writer) = output {
101 let mut encoder =
102 GzEncoder::new(writer, Compression::new(self.compression_level as u32));
103 io::copy(&mut input_stream, &mut encoder)?;
104 encoder.finish()?;
105 } else {
106 let output_stream: Box<dyn Write + Send> = match &output {
107 CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)),
108 CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)),
109 CmprssOutput::Writer(_) => unreachable!(),
110 };
111 let mut encoder = GzEncoder::new(
112 output_stream,
113 Compression::new(self.compression_level as u32),
114 );
115 copy_with_progress(
116 &mut input_stream,
117 &mut encoder,
118 self.progress_args.chunk_size.size_in_bytes,
119 file_size,
120 self.progress_args.progress,
121 &output,
122 )?;
123 encoder.finish()?;
124 }
125 Ok(())
126 }
127
128 /// Extract a gzip archive
129 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
130 let mut file_size = None;
131 let input_stream: Box<dyn Read + Send> = match input {
132 CmprssInput::Path(paths) => {
133 if paths.len() > 1 {
134 bail!("Multiple input files not supported for gzip extraction");
135 }
136 let path = &paths[0];
137 file_size = Some(std::fs::metadata(path)?.len());
138 Box::new(BufReader::new(File::open(path)?))
139 }
140 CmprssInput::Pipe(stdin) => Box::new(BufReader::new(stdin)),
141 CmprssInput::Reader(reader) => reader.0,
142 };
143
144 let mut decoder = GzDecoder::new(input_stream);
145
146 if let CmprssOutput::Writer(mut writer) = output {
147 io::copy(&mut decoder, &mut writer)?;
148 } else {
149 let mut output_stream: Box<dyn Write + Send> = match &output {
150 CmprssOutput::Path(path) => Box::new(BufWriter::new(File::create(path)?)),
151 CmprssOutput::Pipe(stdout) => Box::new(BufWriter::new(stdout)),
152 CmprssOutput::Writer(_) => unreachable!(),
153 };
154 copy_with_progress(
155 &mut decoder,
156 &mut output_stream,
157 self.progress_args.chunk_size.size_in_bytes,
158 file_size,
159 self.progress_args.progress,
160 &output,
161 )?;
162 }
163
164 Ok(())
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::test_utils::*;
172 use std::fs;
173 use std::io::{Read, Write};
174 use tempfile::tempdir;
175
176 /// Test the basic interface of the Gzip compressor
177 #[test]
178 fn test_gzip_interface() {
179 let compressor = Gzip::default();
180 test_compressor_interface(&compressor, "gzip", Some("gz"));
181 }
182
183 /// Test the default compression level
184 #[test]
185 fn test_gzip_default_compression() -> Result {
186 let compressor = Gzip::default();
187 test_compression(&compressor)
188 }
189
190 /// Test fast compression level
191 #[test]
192 fn test_gzip_fast_compression() -> Result {
193 let fast_compressor = Gzip {
194 compression_level: 1,
195 progress_args: ProgressArgs::default(),
196 };
197 test_compression(&fast_compressor)
198 }
199
200 /// Test best compression level
201 #[test]
202 fn test_gzip_best_compression() -> Result {
203 let best_compressor = Gzip {
204 compression_level: 9,
205 progress_args: ProgressArgs::default(),
206 };
207 test_compression(&best_compressor)
208 }
209
210 /// Test for gzip-specific behavior: handling of concatenated gzip archives
211 #[test]
212 fn test_concatenated_gzip() -> Result {
213 let compressor = Gzip::default();
214 let temp_dir = tempdir().expect("Failed to create temp dir");
215
216 // Create two test files
217 let input_path1 = temp_dir.path().join("input1.txt");
218 let input_path2 = temp_dir.path().join("input2.txt");
219 let test_data1 = "This is the first file";
220 let test_data2 = "This is the second file";
221 fs::write(&input_path1, test_data1)?;
222 fs::write(&input_path2, test_data2)?;
223
224 // Compress each file separately
225 let archive_path1 = temp_dir.path().join("archive1.gz");
226 let archive_path2 = temp_dir.path().join("archive2.gz");
227
228 compressor.compress(
229 CmprssInput::Path(vec![input_path1.clone()]),
230 CmprssOutput::Path(archive_path1.clone()),
231 )?;
232
233 compressor.compress(
234 CmprssInput::Path(vec![input_path2.clone()]),
235 CmprssOutput::Path(archive_path2.clone()),
236 )?;
237
238 // Create a concatenated archive
239 let concat_archive = temp_dir.path().join("concat.gz");
240 let mut concat_file = fs::File::create(&concat_archive)?;
241
242 // Concat the two gzip files
243 let mut archive1_data = Vec::new();
244 let mut archive2_data = Vec::new();
245 fs::File::open(&archive_path1)?.read_to_end(&mut archive1_data)?;
246 fs::File::open(&archive_path2)?.read_to_end(&mut archive2_data)?;
247
248 concat_file.write_all(&archive1_data)?;
249 concat_file.write_all(&archive2_data)?;
250 concat_file.flush()?;
251
252 // Extract the concatenated archive - this should yield the first file's contents
253 let output_path = temp_dir.path().join("output.txt");
254
255 compressor.extract(
256 CmprssInput::Path(vec![concat_archive]),
257 CmprssOutput::Path(output_path.clone()),
258 )?;
259
260 // Verify the result is the first file's content
261 let output_data = fs::read_to_string(output_path)?;
262 assert_eq!(output_data, test_data1);
263
264 Ok(())
265 }
266}