Skip to content
Draft
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: 2 additions & 0 deletions benches/bench_tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ fn extract_archive_20_files(bencher: divan::Bencher) {
&archive_path,
false,
CompressionMode::None,
tar::BackupControl::None,
"~",
)
.unwrap();
});
Expand Down
4 changes: 4 additions & 0 deletions src/uu/tar/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pub enum TarError {
#[error("tar: Cannot extract '{path}': {source}")]
CannotExtract { path: PathBuf, source: io::Error },

/// Cannot create backup of an existing destination file
#[error("tar: Cannot backup '{path}': {source}")]
CannotBackup { path: PathBuf, source: io::Error },

/// General tar operation error
#[error("tar: {0}")]
TarOperationError(String),
Expand Down
69 changes: 56 additions & 13 deletions src/uu/tar/src/operations/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,88 @@

use crate::compression::open_archive_reader;
use crate::errors::TarError;
use crate::CompressionMode;
use crate::{BackupControl, CompressionMode};
use std::io::Read;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::path::{Path, PathBuf};
use tar::Archive;
use uucore::error::UResult;

/// Extract files from a tar archive
fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
let mut s = path.as_os_str().to_owned();
s.push(suffix);
PathBuf::from(s)
}

fn numbered_backup_path(path: &Path) -> PathBuf {
for n in 1u64.. {
let mut s = path.as_os_str().to_owned();
s.push(format!(".~{n}~"));
let candidate = PathBuf::from(&s);
if !candidate.exists() {
return candidate;
}
}
unreachable!()
}

fn backup_file(path: &Path, control: BackupControl, suffix: &str) -> Result<(), TarError> {
if matches!(control, BackupControl::None) || !path.is_file() {
return Ok(());
}
let backup = match control {
BackupControl::None => unreachable!(),
BackupControl::Simple => simple_backup_path(path, suffix),
BackupControl::Numbered => numbered_backup_path(path),
BackupControl::Existing => {
// use numbered if a .~1~ backup already exists, otherwise simple
let first_numbered = simple_backup_path(path, ".~1~");
if first_numbered.exists() {
numbered_backup_path(path)
} else {
simple_backup_path(path, suffix)
}
}
};
std::fs::rename(path, &backup).map_err(|e| TarError::CannotBackup {
path: path.to_path_buf(),
source: e,
})
}

/// Extract files from a tar archive.
///
/// # Arguments
///
/// * `archive_path` - Path to the tar archive to extract
/// * `verbose` - Whether to print verbose output during extraction
/// * `input` - Readable source of the archive data
/// * `archive_path` - Path used for error messages and verbose header
/// * `verbose` - Print each extracted path to stdout
/// * `compression` - Compression mode to use when reading
/// * `backup_control` - Whether and how to back up existing files before overwriting
/// * `backup_suffix` - Suffix used for simple backups (default: `~`)
///
/// # Errors
///
/// Returns an error if:
/// - The archive file cannot be opened
/// - The archive format is invalid
/// - Files cannot be extracted due to I/O or permission errors
/// Returns an error if the archive cannot be read or entries cannot be extracted.
pub fn extract_archive(
input: impl Read,
archive_path: &Path,
verbose: bool,
compression: CompressionMode,
backup_control: BackupControl,
backup_suffix: &str,
) -> UResult<()> {
let reader = open_archive_reader(input, compression)?;
let mut archive = Archive::new(reader);
let mut out = BufWriter::new(io::stdout().lock());

// Extract to current directory
if verbose {
writeln!(out, "Extracting archive: {}", archive_path.display()).map_err(TarError::Io)?;
}

// Iterate through entries for verbose output and error handling
for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
let mut entry = entry_result.map_err(TarError::CannotReadEntry)?;

// Get the path before unpacking (clone it so we can use it after borrowing entry mutably)
let path = entry
.path()
.map_err(TarError::CannotReadEntryPath)?
Expand All @@ -54,7 +96,8 @@ pub fn extract_archive(
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
}

// Unpack the entry
backup_file(&path, backup_control, backup_suffix)?;

entry.unpack_in(".").map_err(|e| TarError::CannotExtract {
path: path.clone(),
source: e,
Expand Down
Loading
Loading