Skip to content

Commit 72fb270

Browse files
committed
tar: add explicit zstd compression support
Add an explicit --zstd flag for create, list, and extract so .tar.zst archives work end to end. Keep the scope narrow by requiring the flag instead of trying to autodetect compression. Add CLI coverage and end-to-end tests for creating, listing, and extracting zstd-compressed archives.
1 parent b7c0578 commit 72fb270

File tree

11 files changed

+316
-30
lines changed

11 files changed

+316
-30
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ tempfile = "3.10.1"
5050
textwrap = { version = "0.16.1", features = ["terminal_size"] }
5151
xattr = "1.3.1"
5252
zip = "8.0"
53+
zstd = "0.13.3"
5354

5455
[dependencies]
5556
clap = { workspace = true }
@@ -74,6 +75,7 @@ tar-rs-crate = { version = "0.4", package = "tar" }
7475
tempfile = { workspace = true }
7576
uucore = { workspace = true, features = ["entries", "process", "signals"] }
7677
uutests = { workspace = true }
78+
zstd = { workspace = true }
7779

7880
[target.'cfg(unix)'.dev-dependencies]
7981
xattr = { workspace = true }

src/uu/tar/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ clap = { workspace = true }
1818
regex = { workspace = true }
1919
tar = { workspace = true }
2020
chrono = { workspace = true }
21+
zstd = { workspace = true }
2122

2223
[lib]
2324
path = "src/tar.rs"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// This file is part of the uutils tar package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use crate::errors::TarError;
7+
use crate::CompressionMode;
8+
use std::fs::File;
9+
use std::io::{Read, Write};
10+
use std::path::Path;
11+
12+
pub fn open_archive_reader(
13+
archive_path: &Path,
14+
compression: CompressionMode,
15+
) -> Result<Box<dyn Read>, TarError> {
16+
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
17+
18+
match compression {
19+
CompressionMode::None => Ok(Box::new(file)),
20+
CompressionMode::Zstd => {
21+
let decoder = zstd::stream::read::Decoder::new(file).map_err(|e| {
22+
TarError::InvalidArchive(format!(
23+
"Failed to initialize zstd decoder for '{}': {}",
24+
archive_path.display(),
25+
e
26+
))
27+
})?;
28+
Ok(Box::new(decoder))
29+
}
30+
}
31+
}
32+
33+
pub struct ArchiveWriter {
34+
inner: ArchiveWriterInner,
35+
}
36+
37+
enum ArchiveWriterInner {
38+
Plain(File),
39+
Zstd(zstd::stream::write::Encoder<'static, File>),
40+
}
41+
42+
impl ArchiveWriter {
43+
pub fn create(archive_path: &Path, compression: CompressionMode) -> Result<Self, TarError> {
44+
let file = File::create(archive_path).map_err(|e| {
45+
TarError::TarOperationError(format!(
46+
"Cannot create archive '{}': {}",
47+
archive_path.display(),
48+
e
49+
))
50+
})?;
51+
52+
let inner = match compression {
53+
CompressionMode::None => ArchiveWriterInner::Plain(file),
54+
CompressionMode::Zstd => {
55+
let encoder = zstd::stream::write::Encoder::new(file, 0).map_err(|e| {
56+
TarError::TarOperationError(format!(
57+
"Failed to initialize zstd encoder for '{}': {}",
58+
archive_path.display(),
59+
e
60+
))
61+
})?;
62+
ArchiveWriterInner::Zstd(encoder)
63+
}
64+
};
65+
66+
Ok(Self { inner })
67+
}
68+
69+
pub fn finish(self) -> Result<(), TarError> {
70+
match self.inner {
71+
ArchiveWriterInner::Plain(mut file) => file.flush().map_err(TarError::from),
72+
ArchiveWriterInner::Zstd(encoder) => encoder.finish().map(|_| ()).map_err(|e| {
73+
TarError::TarOperationError(format!("Failed to finalize zstd archive: {e}"))
74+
}),
75+
}
76+
}
77+
}
78+
79+
impl Write for ArchiveWriter {
80+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
81+
match &mut self.inner {
82+
ArchiveWriterInner::Plain(file) => file.write(buf),
83+
ArchiveWriterInner::Zstd(encoder) => encoder.write(buf),
84+
}
85+
}
86+
87+
fn flush(&mut self) -> std::io::Result<()> {
88+
match &mut self.inner {
89+
ArchiveWriterInner::Plain(file) => file.flush(),
90+
ArchiveWriterInner::Zstd(encoder) => encoder.flush(),
91+
}
92+
}
93+
}

src/uu/tar/src/operations/create.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
// file that was distributed with this source code.
55

66
use crate::errors::TarError;
7+
use crate::operations::compression::ArchiveWriter;
8+
use crate::CompressionMode;
79
use std::collections::VecDeque;
8-
use std::fs::{self, File};
10+
use std::fs;
911
use std::path::Component::{self, ParentDir, Prefix, RootDir};
1012
use std::path::{self, Path, PathBuf};
1113
use tar::Builder;
@@ -25,18 +27,14 @@ use uucore::error::UResult;
2527
/// - The archive file cannot be created
2628
/// - Any input file cannot be read
2729
/// - Files cannot be added due to I/O or permission errors
28-
pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UResult<()> {
29-
// Create the output file
30-
let file = File::create(archive_path).map_err(|e| {
31-
TarError::TarOperationError(format!(
32-
"Cannot create archive '{}': {}",
33-
archive_path.display(),
34-
e
35-
))
36-
})?;
37-
38-
// Create Builder instance
39-
let mut builder = Builder::new(file);
30+
pub fn create_archive(
31+
archive_path: &Path,
32+
files: &[&Path],
33+
verbose: bool,
34+
compression: CompressionMode,
35+
) -> UResult<()> {
36+
let writer = ArchiveWriter::create(archive_path, compression)?;
37+
let mut builder = Builder::new(writer);
4038

4139
// Add each file or directory to the archive
4240
for &path in files {
@@ -102,9 +100,10 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
102100
}
103101

104102
// Finish writing the archive
105-
builder
106-
.finish()
103+
let writer = builder
104+
.into_inner()
107105
.map_err(|e| TarError::TarOperationError(format!("Failed to finalize archive: {e}")))?;
106+
writer.finish()?;
108107

109108
Ok(())
110109
}

src/uu/tar/src/operations/extract.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
// file that was distributed with this source code.
55

66
use crate::errors::TarError;
7-
use std::fs::File;
7+
use crate::operations::compression::open_archive_reader;
8+
use crate::CompressionMode;
89
use std::path::Path;
910
use tar::Archive;
1011
use uucore::error::UResult;
@@ -22,12 +23,13 @@ use uucore::error::UResult;
2223
/// - The archive file cannot be opened
2324
/// - The archive format is invalid
2425
/// - Files cannot be extracted due to I/O or permission errors
25-
pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
26-
// Open the archive file
27-
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
28-
29-
// Create Archive instance
30-
let mut archive = Archive::new(file);
26+
pub fn extract_archive(
27+
archive_path: &Path,
28+
verbose: bool,
29+
compression: CompressionMode,
30+
) -> UResult<()> {
31+
let reader = open_archive_reader(archive_path, compression)?;
32+
let mut archive = Archive::new(reader);
3133

3234
// Extract to current directory
3335
if verbose {

src/uu/tar/src/operations/list.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
// file that was distributed with this source code.
55

66
use crate::errors::TarError;
7+
use crate::operations::compression::open_archive_reader;
8+
use crate::CompressionMode;
79
use chrono::{TimeZone, Utc};
8-
use std::fs::File;
910
use std::path::Path;
1011
use tar::Archive;
1112
use uucore::error::UResult;
1213
use uucore::fs::display_permissions_unix;
1314

1415
/// List the contents of a tar archive, printing one entry per line.
15-
pub fn list_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
16-
let file: File =
17-
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
18-
let mut archive = Archive::new(file);
16+
pub fn list_archive(
17+
archive_path: &Path,
18+
verbose: bool,
19+
compression: CompressionMode,
20+
) -> UResult<()> {
21+
let reader = open_archive_reader(archive_path, compression)?;
22+
let mut archive = Archive::new(reader);
1923

2024
for entry_result in archive
2125
.entries()

src/uu/tar/src/operations/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
pub mod compression;
67
pub mod create;
78
pub mod extract;
89
pub mod list;

src/uu/tar/src/tar.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ use uucore::format_usage;
1414
const ABOUT: &str = "an archiving utility";
1515
const USAGE: &str = "tar key [FILE...]\n tar {-c|-t|-x} [-v] -f ARCHIVE [FILE...]";
1616

17+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18+
pub(crate) enum CompressionMode {
19+
None,
20+
Zstd,
21+
}
22+
1723
/// Determines whether a string looks like a POSIX tar keystring.
1824
///
1925
/// A valid keystring must not start with '-', must contain at least one
@@ -131,14 +137,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
131137
};
132138

133139
let verbose = matches.get_flag("verbose");
140+
let compression = if matches.get_flag("zstd") {
141+
CompressionMode::Zstd
142+
} else {
143+
CompressionMode::None
144+
};
134145

135146
// Handle extract operation
136147
if matches.get_flag("extract") {
137148
let archive_path = matches.get_one::<PathBuf>("file").ok_or_else(|| {
138149
uucore::error::USimpleError::new(64, "option requires an argument -- 'f'")
139150
})?;
140151

141-
return operations::extract::extract_archive(archive_path, verbose);
152+
return operations::extract::extract_archive(archive_path, verbose, compression);
142153
}
143154

144155
// Handle create operation
@@ -159,7 +170,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
159170
));
160171
}
161172

162-
return operations::create::create_archive(archive_path, &files, verbose);
173+
return operations::create::create_archive(archive_path, &files, verbose, compression);
163174
}
164175

165176
// Handle list operation
@@ -168,7 +179,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
168179
uucore::error::USimpleError::new(64, "option requires an argument -- 'f'")
169180
})?;
170181

171-
return operations::list::list_archive(archive_path, verbose);
182+
return operations::list::list_archive(archive_path, verbose, compression);
172183
}
173184

174185
// If no operation specified, show error
@@ -203,6 +214,7 @@ pub fn uu_app() -> Command {
203214
// arg!(-z --gzip "Filter through gzip"),
204215
// arg!(-j --bzip2 "Filter through bzip2"),
205216
// arg!(-J --xz "Filter through xz"),
217+
arg!(--zstd "Filter through zstd"),
206218
// Common options
207219
arg!(-v --verbose "Verbosely list files processed"),
208220
// arg!(-h --dereference "Follow symlinks"),

src/uu/tar/tests/test_cli.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,14 @@ fn test_verbose_flag_parsing() {
3232
assert!(matches.get_flag("verbose"));
3333
assert!(matches.get_flag("create"));
3434
}
35+
36+
#[test]
37+
fn test_zstd_flag_parsing() {
38+
let app = uu_app();
39+
let result =
40+
app.try_get_matches_from(vec!["tar", "--zstd", "-cf", "archive.tar.zst", "file.txt"]);
41+
assert!(result.is_ok());
42+
let matches = result.unwrap();
43+
assert!(matches.get_flag("zstd"));
44+
assert!(matches.get_flag("create"));
45+
}

0 commit comments

Comments
 (0)