this repo has no description
1extern crate tar;
2
3use anyhow::bail;
4use clap::Args;
5use std::fs::File;
6use std::io::{self, Read, Seek, SeekFrom, Write};
7use tar::{Archive, Builder};
8use tempfile::tempfile;
9
10use crate::utils::*;
11
12#[derive(Args, Debug)]
13pub struct TarArgs {
14 #[clap(flatten)]
15 pub common_args: CommonArgs,
16}
17
18#[derive(Default)]
19pub struct Tar {}
20
21impl Tar {
22 pub fn new(_args: &TarArgs) -> Tar {
23 Tar {}
24 }
25}
26
27impl Compressor for Tar {
28 /// Full name for tar, also used for extension
29 fn name(&self) -> &str {
30 "tar"
31 }
32
33 /// Tar extracts to a directory by default
34 fn default_extracted_target(&self) -> ExtractedTarget {
35 ExtractedTarget::Directory
36 }
37
38 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
39 match output {
40 CmprssOutput::Path(path) => {
41 let file = File::create(path)?;
42 self.compress_internal(input, Builder::new(file))
43 }
44 CmprssOutput::Pipe(mut pipe) => {
45 // Create a temporary file to write the tar to
46 let mut temp_file = tempfile()?;
47 self.compress_internal(input, Builder::new(&mut temp_file))?;
48
49 // Reset the file position to the beginning
50 temp_file.seek(SeekFrom::Start(0))?;
51
52 // Copy the temporary file to the pipe
53 io::copy(&mut temp_file, &mut pipe)?;
54 Ok(())
55 }
56 CmprssOutput::Writer(mut writer) => {
57 let mut temp_file = tempfile()?;
58 self.compress_internal(input, Builder::new(&mut temp_file))?;
59 temp_file.seek(SeekFrom::Start(0))?;
60 io::copy(&mut temp_file, &mut writer)?;
61 Ok(())
62 }
63 }
64 }
65
66 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
67 match output {
68 CmprssOutput::Path(ref out_dir) => {
69 // Create the output directory if it doesn't exist
70 if !out_dir.exists() {
71 std::fs::create_dir_all(out_dir)?;
72 } else if !out_dir.is_dir() {
73 bail!("tar extraction output must be a directory");
74 }
75
76 match input {
77 CmprssInput::Path(paths) => {
78 if paths.len() != 1 {
79 bail!("tar extraction expects a single archive file");
80 }
81 let file = File::open(&paths[0])?;
82 let mut archive = Archive::new(file);
83 Ok(archive.unpack(out_dir)?)
84 }
85 CmprssInput::Pipe(mut pipe) => {
86 // Create a temporary file to store the tar content
87 let mut temp_file = tempfile()?;
88
89 // Copy from pipe to temporary file
90 io::copy(&mut pipe, &mut temp_file)?;
91
92 // Reset the file position to the beginning
93 temp_file.seek(SeekFrom::Start(0))?;
94
95 // Extract from the temporary file
96 let mut archive = Archive::new(temp_file);
97 Ok(archive.unpack(out_dir)?)
98 }
99 CmprssInput::Reader(reader) => {
100 let mut archive = Archive::new(reader.0);
101 archive.unpack(out_dir)?;
102 Ok(())
103 }
104 }
105 }
106 CmprssOutput::Pipe(_) => bail!("tar extraction to stdout is not supported"),
107 CmprssOutput::Writer(mut writer) => match input {
108 CmprssInput::Path(paths) => {
109 if paths.len() != 1 {
110 bail!("tar extraction expects a single archive file");
111 }
112 let mut file = File::open(&paths[0])?;
113 io::copy(&mut file, &mut writer)?;
114 Ok(())
115 }
116 CmprssInput::Pipe(mut pipe) => {
117 io::copy(&mut pipe, &mut writer)?;
118 Ok(())
119 }
120 CmprssInput::Reader(mut reader) => {
121 io::copy(&mut reader, &mut writer)?;
122 Ok(())
123 }
124 },
125 }
126 }
127
128 fn list(&self, input: CmprssInput) -> Result {
129 let reader: Box<dyn Read> = match input {
130 CmprssInput::Path(paths) => {
131 if paths.len() != 1 {
132 bail!("tar listing expects a single archive file");
133 }
134 Box::new(File::open(&paths[0])?)
135 }
136 CmprssInput::Pipe(stdin) => Box::new(stdin),
137 CmprssInput::Reader(reader) => reader.0,
138 };
139 let mut archive = Archive::new(reader);
140 let stdout = io::stdout();
141 let mut out = stdout.lock();
142 for entry in archive.entries()? {
143 let entry = entry?;
144 let path = entry.path()?;
145 writeln!(out, "{}", path.display())?;
146 }
147 Ok(())
148 }
149}
150
151impl Tar {
152 /// Internal compress helper
153 fn compress_internal<W: Write>(&self, input: CmprssInput, mut archive: Builder<W>) -> Result {
154 match input {
155 CmprssInput::Path(paths) => {
156 for path in paths {
157 if path.is_file() {
158 archive.append_file(
159 path.file_name().unwrap(),
160 &mut File::open(path.as_path())?,
161 )?;
162 } else if path.is_dir() {
163 archive.append_dir_all(path.file_name().unwrap(), path.as_path())?;
164 } else {
165 bail!("unsupported file type for tar compression");
166 }
167 }
168 }
169 CmprssInput::Pipe(mut pipe) => {
170 // For pipe input, we'll create a single file named "archive"
171 let mut temp_file = tempfile()?;
172 io::copy(&mut pipe, &mut temp_file)?;
173 temp_file.seek(SeekFrom::Start(0))?;
174 archive.append_file("archive", &mut temp_file)?;
175 }
176 CmprssInput::Reader(_) => {
177 bail!("Cannot tar a reader input directly");
178 }
179 }
180 Ok(archive.finish()?)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::test_utils::*;
188 use assert_fs::prelude::*;
189 use predicates::prelude::*;
190 use std::path::PathBuf;
191
192 /// Test the basic interface of the Tar compressor
193 #[test]
194 fn test_tar_interface() {
195 let compressor = Tar::default();
196 test_compressor_interface(&compressor, "tar", Some("tar"));
197 }
198
199 /// Test the default compression level
200 #[test]
201 fn test_tar_default_compression() -> Result {
202 let compressor = Tar::default();
203 test_compression(&compressor)
204 }
205
206 /// Test tar-specific functionality: directory handling
207 #[test]
208 fn test_directory_handling() -> Result {
209 let compressor = Tar::default();
210 let dir = assert_fs::TempDir::new()?;
211 let file_path = dir.child("file.txt");
212 file_path.write_str("garbage data for testing")?;
213 let working_dir = assert_fs::TempDir::new()?;
214 let archive = working_dir.child("dir_archive.tar");
215 archive.assert(predicate::path::missing());
216
217 compressor.compress(
218 CmprssInput::Path(vec![dir.path().to_path_buf()]),
219 CmprssOutput::Path(archive.path().to_path_buf()),
220 )?;
221 archive.assert(predicate::path::is_file());
222
223 let extract_dir = working_dir.child("extracted");
224 std::fs::create_dir_all(extract_dir.path())?;
225 compressor.extract(
226 CmprssInput::Path(vec![archive.path().to_path_buf()]),
227 CmprssOutput::Path(extract_dir.path().to_path_buf()),
228 )?;
229
230 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
231 extract_dir
232 .child(dir_name)
233 .child("file.txt")
234 .assert(predicate::path::eq_file(file_path.path()));
235 Ok(())
236 }
237}