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
67 changes: 67 additions & 0 deletions src/uu/tar/src/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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 chrono::{TimeZone, Utc};
use std::io::Write;
use std::path::Path;
use uucore::fs::display_permissions_unix;

/// Print a verbose (ls -l style) line for an entry in a tar archive
pub fn print_entry_verbose<W: Write>(
mut out: W,
header: &tar::Header,
path: &Path,
) -> std::io::Result<()> {
let mode = header.mode().unwrap_or(0);
let entry_type = header.entry_type();
let owner = header
.username()
.ok()
.flatten()
.map(|s| s.to_owned())
.unwrap_or_else(|| header.uid().unwrap_or(0).to_string());
let group = header
.groupname()
.ok()
.flatten()
.map(|s| s.to_owned())
.unwrap_or_else(|| header.gid().unwrap_or(0).to_string());
let size = header.size().unwrap_or(0);
let mtime = header.mtime().unwrap_or(0);

let type_char = match entry_type {
tar::EntryType::Directory => 'd',
tar::EntryType::Symlink => 'l',
tar::EntryType::Char => 'c',
tar::EntryType::Block => 'b',
tar::EntryType::Fifo => 'p',
_ => '-',
};
// Tar headers store the type separately from the mode bits, so we get the
// 9-character rwx string from uucore and prepend our own type character.
let perm_str = display_permissions_unix(mode, false);
let permissions = format!("{type_char}{perm_str}");

// TODO: GNU tar displays mtime in the user's local timezone; we
// currently format in UTC. Convert to local time for compatibility.
let dt: chrono::DateTime<Utc> = Utc
.timestamp_opt(mtime as i64, 0)
.single()
.unwrap_or_else(Utc::now);
let date_str = dt.format("%Y-%m-%d %H:%M");

// TODO: use path.has_trailing_sep() when stable
let path_str = path.display().to_string();
let suffix = if entry_type.is_dir() && !path_str.ends_with("/") {
std::path::MAIN_SEPARATOR_STR
} else {
""
};

writeln!(
out,
"{permissions} {owner}/{group} {size:>8} {date_str} {path_str}{suffix}"
)
}
17 changes: 14 additions & 3 deletions src/uu/tar/src/operations/create.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.

use crate::display;
use crate::errors::TarError;
use std::collections::VecDeque;
use std::fs::{self, File};
Expand All @@ -18,15 +19,15 @@ use uucore::error::UResult;
///
/// * `archive_path` - Path where the tar archive should be created
/// * `files` - Slice of file paths to add to the archive
/// * `verbose` - Whether to print verbose output during creation
/// * `verbose` - Verbosity level during creation
///
/// # Errors
///
/// Returns an error if:
/// - 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<()> {
pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: u8) -> UResult<()> {
// Create the output file
let file = File::create(archive_path).map_err(|e| TarError::CannotCreateArchive {
path: archive_path.to_path_buf(),
Expand All @@ -47,7 +48,17 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
.into());
}

if verbose {
if verbose >= 2 {
for p in get_tree(path)? {
let metadata = p.metadata().map_err(|e| TarError::CannotAddFile {
path: p.clone(),
source: e,
})?;
let mut header = tar::Header::new_gnu();
header.set_metadata(&metadata);
display::print_entry_verbose(&mut out, &header, &p).map_err(TarError::Io)?;
}
} else if verbose == 1 {
let to_print = get_tree(path)?
.iter()
.map(|p| (p.is_dir(), p.display().to_string()))
Expand Down
11 changes: 7 additions & 4 deletions src/uu/tar/src/operations/extract.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.

use crate::display;
use crate::errors::TarError;
use std::fs::File;
use std::io::{self, BufWriter, Write};
Expand All @@ -15,15 +16,15 @@ use uucore::error::UResult;
/// # Arguments
///
/// * `archive_path` - Path to the tar archive to extract
/// * `verbose` - Whether to print verbose output during extraction
/// * `verbose` - Verbosity level during extraction
///
/// # 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
pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
pub fn extract_archive(archive_path: &Path, verbose: u8) -> UResult<()> {
// Open the archive file
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;

Expand All @@ -32,7 +33,7 @@ pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
let mut out = BufWriter::new(io::stdout().lock());

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

Expand All @@ -46,7 +47,9 @@ pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
.map_err(TarError::CannotReadEntryPath)?
.to_path_buf();

if verbose {
if verbose >= 2 {
display::print_entry_verbose(&mut out, entry.header(), &path).map_err(TarError::Io)?;
} else if verbose == 1 {
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
}

Expand Down
64 changes: 5 additions & 59 deletions src/uu/tar/src/operations/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,28 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use crate::display;
use crate::errors::TarError;
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<()> {
pub fn list_archive(archive_path: &Path, verbose: u8) -> UResult<()> {
let file: File =
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
let mut archive = Archive::new(file);
let mut out = BufWriter::new(io::stdout().lock());

for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
let entry = entry_result.map_err(TarError::CannotReadEntry)?;
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;

if verbose {
// Collect all header fields into owned values before borrowing entry for the path,
// since both header() and path() require a borrow of entry.
let (mode, entry_type, owner, group, size, mtime) = {
let header = entry.header();
(
header.mode().unwrap_or(0),
header.entry_type(),
header
.username()
.ok()
.flatten()
.unwrap_or_default()
.to_owned(),
header
.groupname()
.ok()
.flatten()
.unwrap_or_default()
.to_owned(),
header.size().unwrap_or(0),
header.mtime().unwrap_or(0),
)
};

let path = entry.path().map_err(TarError::CannotReadEntryPath)?;

let type_char = match entry_type {
tar::EntryType::Directory => 'd',
tar::EntryType::Symlink => 'l',
tar::EntryType::Char => 'c',
tar::EntryType::Block => 'b',
tar::EntryType::Fifo => 'p',
_ => '-',
};
// Tar headers store the type separately from the mode bits, so we get the
// 9-character rwx string from uucore and prepend our own type character.
let perm_str = display_permissions_unix(mode, false);
let permissions = format!("{type_char}{perm_str}");

// TODO: GNU tar displays mtime in the user's local timezone; we
// currently format in UTC. Convert to local time for compatibility.
let dt: chrono::DateTime<Utc> = Utc
.timestamp_opt(mtime as i64, 0)
.single()
.unwrap_or_else(Utc::now);
let date_str = dt.format("%Y-%m-%d %H:%M");

writeln!(
out,
"{permissions} {owner}/{group} {size:>8} {date_str} {}",
path.display()
)
.map_err(TarError::Io)?;
if verbose >= 1 {
display::print_entry_verbose(&mut out, entry.header(), &path).map_err(TarError::Io)?;
} else {
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;

writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
}
}
Expand Down
12 changes: 10 additions & 2 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.

mod display;
pub mod errors;
mod operations;

Expand Down Expand Up @@ -130,7 +131,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
};

let verbose = matches.get_flag("verbose");
let verbose = matches.get_count("verbose");

// Handle extract operation
if matches.get_flag("extract") {
Expand Down Expand Up @@ -204,7 +205,7 @@ pub fn uu_app() -> Command {
// arg!(-j --bzip2 "Filter through bzip2"),
// arg!(-J --xz "Filter through xz"),
// Common options
arg!(-v --verbose "Verbosely list files processed"),
arg!(-v --verbose "Verbosely list files processed").action(ArgAction::Count),
// arg!(-h --dereference "Follow symlinks"),
// arg!(-p --"preserve-permissions" "Extract information about file permissions"),
// arg!(-P --"absolute-names" "Don't strip leading '/' from file names"),
Expand Down Expand Up @@ -285,6 +286,13 @@ mod tests {
assert_eq!(expand_posix_keystring(input), expected);
}

#[test]
fn test_expand_cvvf() {
let input = osvec(&["tar", "cvvf", "archive.tar", "file.txt"]);
let expected = osvec(&["tar", "-c", "-v", "-v", "-f", "archive.tar", "file.txt"]);
assert_eq!(expand_posix_keystring(input), expected);
}

#[test]
fn test_expand_xf() {
let input = osvec(&["tar", "xf", "archive.tar"]);
Expand Down
12 changes: 11 additions & 1 deletion src/uu/tar/tests/test_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ fn test_verbose_flag_parsing() {
let result = app.try_get_matches_from(vec!["tar", "-cvf", "archive.tar", "file.txt"]);
assert!(result.is_ok());
let matches = result.unwrap();
assert!(matches.get_flag("verbose"));
assert_eq!(matches.get_count("verbose"), 1);
assert!(matches.get_flag("create"));
}

#[test]
fn test_double_verbose_flag_parsing() {
let app = uu_app();
let result = app.try_get_matches_from(vec!["tar", "-cvvf", "archive.tar", "file.txt"]);
assert!(result.is_ok());
let matches = result.unwrap();
assert_eq!(matches.get_count("verbose"), 2);
assert!(matches.get_flag("create"));
}
35 changes: 35 additions & 0 deletions tests/by-util/test_tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use std::path::{self, PathBuf};

use regex::Regex;
use uutests::{at_and_ucmd, new_ucmd};

/// Size of a single tar block in bytes (per POSIX specification).
Expand Down Expand Up @@ -145,6 +146,20 @@ fn test_create_verbose() {
assert!(at.file_exists("archive.tar"));
}

#[test]
fn test_create_double_verbose() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("file.txt", "content");
at.mkdir("dir");

let file_regex = Regex::new(r"-.{9} .* 7 \d{4}-\d{2}-\d{2} \d{2}:\d{2} file.txt\r?\n").unwrap();
let dir_regex = Regex::new(r"d.{9} .* 0 \d{4}-\d{2}-\d{2} \d{2}:\d{2} dir[/\\]+\r?\n").unwrap();
ucmd.args(&["-cvvf", "archive.tar", "file.txt", "dir"])
.succeeds()
.stdout_matches(&file_regex)
.stdout_matches(&dir_regex);
}

#[test]
fn test_create_empty_archive_fails() {
new_ucmd!()
Expand Down Expand Up @@ -239,6 +254,26 @@ fn test_extract_verbose() {
assert!(at.file_exists("file3.txt"));
}

#[test]
fn test_extract_double_verbose() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("file.txt", "content");
at.mkdir("dir");
ucmd.args(&["-cf", "archive.tar", "file.txt", "dir"])
.succeeds();
at.remove("file.txt");
at.rmdir("dir");

let file_regex = Regex::new(r"-.{9} .* 7 \d{4}-\d{2}-\d{2} \d{2}:\d{2} file.txt\r?\n").unwrap();
let dir_regex = Regex::new(r"d.{9} .* 0 \d{4}-\d{2}-\d{2} \d{2}:\d{2} dir[/\\]+\r?\n").unwrap();
new_ucmd!()
.args(&["-xvvf", "archive.tar"])
.current_dir(at.as_string())
.succeeds()
.stdout_matches(&file_regex)
.stdout_matches(&dir_regex);
}

#[test]
fn test_extract_multiple_files() {
let (at, mut ucmd) = at_and_ucmd!();
Expand Down
Loading