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
4 changes: 4 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 @@ -68,6 +68,8 @@ tar = { optional = true, version = "0.0.1", package = "uu_tar", path = "src/uu/t

[dev-dependencies]
chrono = { workspace = true }
flate2 = "1"
libc = { workspace = true }
pretty_assertions = "1"
rand = { workspace = true }
regex = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions src/uu/tar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ regex = { workspace = true }
tar = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
flate2 = "1"

[lib]
path = "src/tar.rs"
Expand Down
104 changes: 104 additions & 0 deletions src/uu/tar/src/compression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::Path;

const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];

pub(crate) fn open_archive_reader(
archive_path: &Path,
mode: CompressionMode,
) -> Result<Box<dyn Read>, TarError> {
let mut file =
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
let mode = match mode {
CompressionMode::Auto => detect_compression(&mut file)?,
other => other,
};

let reader: Box<dyn Read> = match mode {
CompressionMode::Auto | CompressionMode::None => Box::new(file),
CompressionMode::Gzip => Box::new(GzDecoder::new(file)),
};

Ok(reader)
}

pub(crate) struct ArchiveWriter {
inner: ArchiveWriterInner,
}

enum ArchiveWriterInner {
Plain(File),
Gzip(GzEncoder<File>),
}

impl ArchiveWriter {
pub(crate) fn create(archive_path: &Path, mode: CompressionMode) -> Result<Self, TarError> {
let file = File::create(archive_path).map_err(|e| TarError::CannotCreateArchive {
path: archive_path.to_path_buf(),
source: e,
})?;

let inner = match mode {
CompressionMode::Auto => {
return Err(TarError::TarOperationError(
"internal error: automatic compression is not valid for archive creation"
.to_string(),
));
}
CompressionMode::None => ArchiveWriterInner::Plain(file),
CompressionMode::Gzip => {
ArchiveWriterInner::Gzip(GzEncoder::new(file, flate2::Compression::default()))
}
};

Ok(Self { inner })
}

pub(crate) fn finish(self) -> Result<(), TarError> {
match self.inner {
ArchiveWriterInner::Plain(mut file) => file.flush().map_err(TarError::from),
ArchiveWriterInner::Gzip(encoder) => encoder
.finish()
.map(|_| ())
.map_err(TarError::CannotFinalizeArchive),
}
}
}

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::Gzip(encoder) => encoder.write(buf),
}
}

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

fn detect_compression(file: &mut File) -> Result<CompressionMode, TarError> {
let mut magic = [0u8; 2];
let n = file.read(&mut magic).map_err(TarError::Io)?;
file.seek(std::io::SeekFrom::Start(0))
.map_err(TarError::Io)?;

if n >= GZIP_MAGIC.len() && magic[..GZIP_MAGIC.len()] == GZIP_MAGIC {
return Ok(CompressionMode::Gzip);
}
Ok(CompressionMode::None)
}
8 changes: 8 additions & 0 deletions src/uu/tar/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ pub enum TarError {
#[error("tar: Cannot read entry path: {0}")]
CannotReadEntryPath(io::Error),

/// Invalid archive format or unsupported compression stream
#[error("tar: {0}")]
InvalidArchive(String),

/// File or directory not found
#[error("tar: {path}: Cannot open: No such file or directory")]
FileNotFound { path: PathBuf },
Expand All @@ -51,6 +55,10 @@ pub enum TarError {
#[error("tar: Cannot extract '{path}': {source}")]
CannotExtract { path: PathBuf, source: io::Error },

/// General tar operation error
#[error("tar: {0}")]
TarOperationError(String),

/// Cannot finalize the archive
#[error("tar: Cannot finalize archive: {0}")]
CannotFinalizeArchive(io::Error),
Expand Down
26 changes: 15 additions & 11 deletions src/uu/tar/src/operations/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::compression::ArchiveWriter;
use crate::errors::TarError;
use crate::CompressionMode;
use std::collections::VecDeque;
use std::fs::{self, File};
use std::fs;
use std::io::{self, BufWriter, Write};
use std::path::Component::{self, ParentDir, Prefix, RootDir};
use std::path::{self, Path, PathBuf};
Expand All @@ -26,15 +28,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::CannotCreateArchive {
path: archive_path.to_path_buf(),
source: 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);
let mut out = BufWriter::new(io::stdout().lock());

// Add each file or directory to the archive
Expand Down Expand Up @@ -106,7 +107,10 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR

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

Ok(())
}
Expand Down
16 changes: 9 additions & 7 deletions src/uu/tar/src/operations/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::compression::open_archive_reader;
use crate::errors::TarError;
use std::fs::File;
use crate::CompressionMode;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use tar::Archive;
Expand All @@ -23,12 +24,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);
let mut out = BufWriter::new(io::stdout().lock());

// Extract to current directory
Expand Down
14 changes: 9 additions & 5 deletions src/uu/tar/src/operations/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::compression::open_archive_reader;
use crate::errors::TarError;
use crate::CompressionMode;
use chrono::{TimeZone, Utc};
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use tar::Archive;
use uucore::error::UResult;
use uucore::fs::display_permissions_unix;

/// List the contents of a tar archive, printing one entry per line.
pub fn list_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
let file: File =
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
let mut archive = Archive::new(file);
pub fn list_archive(
archive_path: &Path,
verbose: bool,
compression: CompressionMode,
) -> UResult<()> {
let reader = open_archive_reader(archive_path, compression)?;
let mut archive = Archive::new(reader);
let mut out = BufWriter::new(io::stdout().lock());

for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
Expand Down
24 changes: 20 additions & 4 deletions src/uu/tar/src/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

pub mod compression;
pub mod errors;
mod operations;

Expand All @@ -14,6 +15,13 @@ use uucore::format_usage;
const ABOUT: &str = "an archiving utility";
const USAGE: &str = "tar key [FILE...]\n tar {-c|-t|-x} [-v] -f ARCHIVE [FILE...]";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CompressionMode {
Auto,
None,
Gzip,
}

/// Determines whether a string looks like a POSIX tar keystring.
///
/// A valid keystring must not start with '-', must contain at least one
Expand Down Expand Up @@ -131,14 +139,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};

let verbose = matches.get_flag("verbose");
let explicit_compression = if matches.get_flag("gzip") {
Some(CompressionMode::Gzip)
} else {
None
};

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

return operations::extract::extract_archive(archive_path, verbose);
let compression = explicit_compression.unwrap_or(CompressionMode::Auto);
return operations::extract::extract_archive(archive_path, verbose, compression);
}

// Handle create operation
Expand All @@ -159,7 +173,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
));
}

return operations::create::create_archive(archive_path, &files, verbose);
let compression = explicit_compression.unwrap_or(CompressionMode::None);
return operations::create::create_archive(archive_path, &files, verbose, compression);
}

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

return operations::list::list_archive(archive_path, verbose);
let compression = explicit_compression.unwrap_or(CompressionMode::Auto);
return operations::list::list_archive(archive_path, verbose, compression);
}

// If no operation specified, show error
Expand Down Expand Up @@ -200,7 +216,7 @@ pub fn uu_app() -> Command {
arg!(-f --file <ARCHIVE> "Use archive file or device ARCHIVE")
.value_parser(clap::value_parser!(PathBuf)),
// Compression options
// arg!(-z --gzip "Filter through gzip"),
arg!(-z --gzip "Filter through gzip"),
// arg!(-j --bzip2 "Filter through bzip2"),
// arg!(-J --xz "Filter through xz"),
// Common options
Expand Down
Loading
Loading