Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
outputs CODECOV_FLAGS
- name: Test
run: cargo test --no-fail-fast
run: cargo test --all --no-fail-fast
env:
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ tempfile = "3.10.1"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
xattr = "1.3.1"
zip = "8.0"
zstd = "0.13.3"

[dependencies]
clap = { workspace = true }
Expand All @@ -74,6 +75,7 @@ tar-rs-crate = { version = "0.4", package = "tar" }
tempfile = { workspace = true }
uucore = { workspace = true, features = ["entries", "process", "signals"] }
uutests = { workspace = true }
zstd = { workspace = true }

[target.'cfg(unix)'.dev-dependencies]
xattr = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions src/uu/tar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ clap = { workspace = true }
regex = { workspace = true }
tar = { workspace = true }
chrono = { workspace = true }
zstd = { workspace = true }

[lib]
path = "src/tar.rs"

[[bin]]
name = "tar"
path = "src/main.rs"

[dev-dependencies]
tempfile = { workspace = true }
154 changes: 154 additions & 0 deletions src/uu/tar/src/operations/compression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// This file is part of the uutils tar package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::errors::TarError;
use crate::CompressionMode;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;

pub fn open_archive_reader(
archive_path: &Path,
compression: CompressionMode,
) -> Result<Box<dyn Read>, TarError> {
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;

match compression {
CompressionMode::None => Ok(Box::new(file)),
CompressionMode::Zstd => {
let decoder = zstd::stream::read::Decoder::new(file).map_err(|e| {
TarError::InvalidArchive(format!(
"Failed to initialize zstd decoder for '{}': {}",
archive_path.display(),
e
))
})?;
Ok(Box::new(decoder))
}
}
}

pub struct ArchiveWriter {
inner: ArchiveWriterInner,
}

enum ArchiveWriterInner {
Plain(File),
Zstd(zstd::stream::write::Encoder<'static, File>),
}

impl ArchiveWriter {
pub fn create(archive_path: &Path, compression: CompressionMode) -> Result<Self, TarError> {
let file = File::create(archive_path).map_err(|e| {
TarError::TarOperationError(format!(
"Cannot create archive '{}': {}",
archive_path.display(),
e
))
})?;

let inner = match compression {
CompressionMode::None => ArchiveWriterInner::Plain(file),
CompressionMode::Zstd => {
let encoder = zstd::stream::write::Encoder::new(file, 0).map_err(|e| {
TarError::TarOperationError(format!(
"Failed to initialize zstd encoder for '{}': {}",
archive_path.display(),
e
))
})?;
ArchiveWriterInner::Zstd(encoder)
}
};

Ok(Self { inner })
}

pub fn finish(self) -> Result<(), TarError> {
match self.inner {
ArchiveWriterInner::Plain(mut file) => file.flush().map_err(TarError::from),
ArchiveWriterInner::Zstd(encoder) => encoder.finish().map(|_| ()).map_err(|e| {
TarError::TarOperationError(format!("Failed to finalize zstd archive: {e}"))
}),
}
}
}

impl Write for ArchiveWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match &mut self.inner {
ArchiveWriterInner::Plain(file) => file.write(buf),
ArchiveWriterInner::Zstd(encoder) => encoder.write(buf),
}
}

fn flush(&mut self) -> std::io::Result<()> {
match &mut self.inner {
ArchiveWriterInner::Plain(file) => file.flush(),
ArchiveWriterInner::Zstd(encoder) => encoder.flush(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
use tempfile::tempdir;

#[test]
fn test_plain_archive_writer_and_reader() {
let tempdir = tempdir().unwrap();
let archive_path = tempdir.path().join("plain.tar");

let mut writer = ArchiveWriter::create(&archive_path, CompressionMode::None).unwrap();
writer.write_all(b"plain data").unwrap();
writer.flush().unwrap();
writer.finish().unwrap();

let mut reader = open_archive_reader(&archive_path, CompressionMode::None).unwrap();
let mut contents = Vec::new();
reader.read_to_end(&mut contents).unwrap();
assert_eq!(contents, b"plain data");
}

#[test]
fn test_zstd_archive_writer_and_reader() {
let tempdir = tempdir().unwrap();
let archive_path = tempdir.path().join("archive.tar.zst");

let mut writer = ArchiveWriter::create(&archive_path, CompressionMode::Zstd).unwrap();
writer.write_all(b"zstd data").unwrap();
writer.flush().unwrap();
writer.finish().unwrap();

let mut reader = open_archive_reader(&archive_path, CompressionMode::Zstd).unwrap();
let mut contents = Vec::new();
reader.read_to_end(&mut contents).unwrap();
assert_eq!(contents, b"zstd data");
}

#[test]
fn test_open_archive_reader_missing_file() {
let tempdir = tempdir().unwrap();
let archive_path = tempdir.path().join("missing.tar.zst");

let err = open_archive_reader(&archive_path, CompressionMode::Zstd)
.err()
.unwrap();
assert!(matches!(err, TarError::FileNotFound(_)));
}

#[test]
fn test_open_archive_reader_invalid_zstd_stream_fails_on_read() {
let tempdir = tempdir().unwrap();
let archive_path = tempdir.path().join("invalid.tar.zst");
std::fs::write(&archive_path, b"not zstd").unwrap();

let mut reader = open_archive_reader(&archive_path, CompressionMode::Zstd).unwrap();
let mut contents = Vec::new();
assert!(reader.read_to_end(&mut contents).is_err());
}
}
76 changes: 61 additions & 15 deletions src/uu/tar/src/operations/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
// file that was distributed with this source code.

use crate::errors::TarError;
use crate::operations::compression::ArchiveWriter;
use crate::CompressionMode;
use std::collections::VecDeque;
use std::fs::{self, File};
use std::fs;
use std::path::Component::{self, ParentDir, Prefix, RootDir};
use std::path::{self, Path, PathBuf};
use tar::Builder;
Expand All @@ -25,18 +27,14 @@ use uucore::error::UResult;
/// - The archive file cannot be created
/// - Any input file cannot be read
/// - Files cannot be added due to I/O or permission errors
pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UResult<()> {
// Create the output file
let file = File::create(archive_path).map_err(|e| {
TarError::TarOperationError(format!(
"Cannot create archive '{}': {}",
archive_path.display(),
e
))
})?;

// Create Builder instance
let mut builder = Builder::new(file);
pub fn create_archive(
archive_path: &Path,
files: &[&Path],
verbose: bool,
compression: CompressionMode,
) -> UResult<()> {
let writer = ArchiveWriter::create(archive_path, compression)?;
let mut builder = Builder::new(writer);

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

// Finish writing the archive
builder
.finish()
let writer = builder
.into_inner()
.map_err(|e| TarError::TarOperationError(format!("Failed to finalize archive: {e}")))?;
writer.finish()?;

Ok(())
}
Expand Down Expand Up @@ -138,3 +137,50 @@ fn normalize_path(path: &Path) -> Option<PathBuf> {
None
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::CompressionMode;
use std::fs;
use tar::Archive;
use tempfile::tempdir;

#[test]
fn test_create_archive_with_zstd() {
let tempdir = tempdir().unwrap();
let _guard = crate::operations::TestDirGuard::enter(tempdir.path());
fs::write("file.txt", "hello").unwrap();

create_archive(
Path::new("archive.tar.zst"),
&[Path::new("file.txt")],
false,
CompressionMode::Zstd,
)
.unwrap();

let decoder =
zstd::stream::read::Decoder::new(fs::File::open("archive.tar.zst").unwrap()).unwrap();
let mut archive = Archive::new(decoder);
let mut entries = archive.entries().unwrap();
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap().to_str(), Some("file.txt"));
}

#[test]
fn test_create_archive_missing_file_fails() {
let tempdir = tempdir().unwrap();
let archive_path = tempdir.path().join("archive.tar.zst");
let missing_path = tempdir.path().join("missing.txt");

let err = create_archive(
&archive_path,
&[missing_path.as_path()],
false,
CompressionMode::Zstd,
)
.unwrap_err();
assert!(err.to_string().contains("missing.txt"));
}
}
55 changes: 48 additions & 7 deletions src/uu/tar/src/operations/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// file that was distributed with this source code.

use crate::errors::TarError;
use std::fs::File;
use crate::operations::compression::open_archive_reader;
use crate::CompressionMode;
use std::path::Path;
use tar::Archive;
use uucore::error::UResult;
Expand All @@ -22,12 +23,13 @@ use uucore::error::UResult;
/// - The archive file cannot be opened
/// - The archive format is invalid
/// - Files cannot be extracted due to I/O or permission errors
pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
// Open the archive file
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;

// Create Archive instance
let mut archive = Archive::new(file);
pub fn extract_archive(
archive_path: &Path,
verbose: bool,
compression: CompressionMode,
) -> UResult<()> {
let reader = open_archive_reader(archive_path, compression)?;
let mut archive = Archive::new(reader);

// Extract to current directory
if verbose {
Expand Down Expand Up @@ -60,3 +62,42 @@ pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::CompressionMode;
use std::fs;
use tar::Builder;
use tempfile::tempdir;

#[test]
fn test_extract_archive_with_zstd() {
let tempdir = tempdir().unwrap();
let archive_path = tempdir.path().join("archive.tar.zst");

let mut tar_bytes = Vec::new();
{
let mut builder = Builder::new(&mut tar_bytes);
let mut header = tar::Header::new_gnu();
header.set_mode(0o644);
header.set_size("hello".len() as u64);
header.set_cksum();
builder
.append_data(&mut header, "extracted.txt", std::io::Cursor::new("hello"))
.unwrap();
builder.finish().unwrap();
}
let compressed = zstd::stream::encode_all(std::io::Cursor::new(tar_bytes), 0).unwrap();
fs::write(&archive_path, compressed).unwrap();

let _guard = crate::operations::TestDirGuard::enter(tempdir.path());
let result = extract_archive(&archive_path, true, CompressionMode::Zstd);

result.unwrap();
assert_eq!(
fs::read_to_string(tempdir.path().join("extracted.txt")).unwrap(),
"hello"
);
}
}
Loading
Loading