#![allow(dead_code)]
use clap::{Arg, Command};
use color_eyre::Result;
use color_eyre::eyre::eyre;
use std::fs::File;
use std::fs;
use std::path::Path;
use crate::lfs;
use std::io;
use std::cell::Cell;
use std::rc::Rc;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression as FlateCompression;
use bzip2::read::BzDecoder;
use bzip2::write::BzEncoder;
use bzip2::Compression as BzCompression;
use liblzma::read::XzDecoder;
use liblzma::write::XzEncoder;
use std::env;
const BLOCK_SIZE: usize = 512;
const SIZE_OFFSET: usize = 124;
const SIZE_LEN: usize = 12;
const CHECKSUM_OFFSET: usize = 148;
const CHECKSUM_LEN: usize = 8;
const TYPEFLAG_OFFSET: usize = 156;
const SYMLINK_TYPEFLAG: u8 = b'2';
const MODE_OFFSET: usize = 100;
const MODE_LEN: usize = 8;
const UID_OFFSET: usize = 108;
const UID_LEN: usize = 8;
const GID_OFFSET: usize = 116;
const GID_LEN: usize = 8;
const MTIME_OFFSET: usize = 136;
const MTIME_LEN: usize = 12;
const USTAR_UNAME_OFFSET: usize = 265;
const USTAR_UNAME_LEN: usize = 32;
const USTAR_GNAME_OFFSET: usize = 297;
const USTAR_GNAME_LEN: usize = 32;
fn ustar_name_from_bytes(bytes: &[u8], start: usize, len: usize) -> String {
let end = (start + len).min(bytes.len());
let slice = &bytes[start..end];
let trunc = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
let s = &slice[..trunc];
String::from_utf8_lossy(s).trim().to_string()
}
fn list_tz_offset_seconds() -> i64 {
use time::UtcOffset;
let offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
let s = offset.whole_hours() as i64 * 3600
+ offset.minutes_past_hour() as i64 * 60
+ offset.seconds_past_minute() as i64;
s
}
fn format_timestamp_utc(ts: i64) -> String {
const SECS_PER_DAY: i64 = 86400;
const SECS_PER_HOUR: u32 = 3600;
const SECS_PER_MIN: u32 = 60;
let (_, day) = if ts >= 0 {
(ts, ts / SECS_PER_DAY)
} else {
let day = (ts - SECS_PER_DAY + 1) / SECS_PER_DAY;
(ts - day * SECS_PER_DAY, day)
};
let secs = ts.rem_euclid(SECS_PER_DAY) as u32;
let hour = secs / SECS_PER_HOUR;
let min = (secs % SECS_PER_HOUR) / SECS_PER_MIN;
let sec = secs % SECS_PER_MIN;
let (year, month, day_of_month) = julian_day_to_ymd(day + 2440588);
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
year, month, day_of_month, hour, min, sec
)
}
#[allow(clippy::many_single_char_names)]
fn julian_day_to_ymd(jdn: i64) -> (i32, u8, u8) {
let a = jdn + 32044;
let b = (4 * a + 3) / 146097;
let c = a - (146097 * b) / 4;
let d = (4 * c + 3) / 1461;
let e = c - (1461 * d) / 4;
let m = (5 * e + 2) / 153;
let day = (e - (153 * m + 2) / 5 + 1) as u8;
let year = (100 * b + d - 4800 + m / 10) as i32;
let month = (m + 3 - 12 * (m / 10)) as u8;
(year, month, day)
}
fn sanitize_header(block: &mut [u8; BLOCK_SIZE]) {
if env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar sanitize] entry");
eprintln!("[tar sanitize] block first 10 bytes: {:?}", &block[..10]);
let size_slice = &block[SIZE_OFFSET..SIZE_OFFSET + SIZE_LEN];
eprintln!("[tar sanitize] size field bytes: {:?}", std::str::from_utf8(size_slice).unwrap_or("invalid"));
eprintln!("[tar sanitize] typeflag: {:?}", block[TYPEFLAG_OFFSET] as char);
}
let mut all_zero = true;
for &b in block.iter() {
if b != 0 {
all_zero = false;
break;
}
}
if env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar sanitize] zero block check: all_zero = {}", all_zero);
if !all_zero {
let first_nonzero = block.iter().position(|&b| b != 0);
eprintln!("[tar sanitize] first_nonzero = {:?}", first_nonzero);
}
}
if all_zero {
if env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar sanitize] zero block, skipping");
}
return;
}
let typeflag = block[TYPEFLAG_OFFSET];
if typeflag == SYMLINK_TYPEFLAG {
let size_slice = &mut block[SIZE_OFFSET..SIZE_OFFSET + SIZE_LEN];
if env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar sanitize] symlink size field bytes: {:?}", std::str::from_utf8(size_slice).unwrap_or("invalid"));
}
size_slice[0] = b'0';
size_slice[1] = 0;
for b in &mut size_slice[2..] {
*b = b' ';
}
}
let numeric_fields = [
(MODE_OFFSET, MODE_LEN),
(UID_OFFSET, UID_LEN),
(GID_OFFSET, GID_LEN),
(SIZE_OFFSET, SIZE_LEN),
(MTIME_OFFSET, MTIME_LEN),
(CHECKSUM_OFFSET, CHECKSUM_LEN),
];
for (offset, len) in numeric_fields.iter() {
let before = if env::var_os("EPKG_DEBUG").is_some() && *offset == SIZE_OFFSET {
Some(std::str::from_utf8(&block[*offset..*offset + *len]).unwrap_or("invalid").to_string())
} else {
None
};
if let Some(ref before) = before {
eprintln!("[tar sanitize] size field before sanitization: {:?}", before);
}
let slice = &mut block[*offset..*offset + *len];
for b in slice.iter_mut() {
if !(*b == 0 || *b == b' ' || (*b >= b'0' && *b <= b'7')) {
*b = b' ';
}
}
if env::var_os("EPKG_DEBUG").is_some() && *offset == SIZE_OFFSET {
let after = std::str::from_utf8(slice).unwrap_or("invalid");
eprintln!("[tar sanitize] size field after sanitization: {:?}", after);
}
}
let (left, right) = block.split_at_mut(CHECKSUM_OFFSET);
let (cksum_slice, right) = right.split_at_mut(CHECKSUM_LEN);
if env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar sanitize] checksum before: {:?}", std::str::from_utf8(cksum_slice).unwrap_or("invalid"));
}
for b in cksum_slice.iter_mut() {
if !(*b == 0 || *b == b' ' || (*b >= b'0' && *b <= b'7')) {
*b = b' ';
}
}
for b in cksum_slice.iter_mut() {
*b = b' ';
}
let sum = left.iter()
.chain(right.iter())
.fold(0u32, |a, &b| a + b as u32)
+ 8 * 32;
let octal = format!("{:07o}", sum);
let bytes = octal.as_bytes();
let len = bytes.len().min(CHECKSUM_LEN - 1);
cksum_slice[..len].copy_from_slice(&bytes[..len]);
for i in len..CHECKSUM_LEN {
cksum_slice[i] = b' ';
}
if env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar sanitize] checksum after: {:?}", std::str::from_utf8(cksum_slice).unwrap_or("invalid"));
eprintln!("[tar sanitize] computed sum={} octal={}", sum, octal);
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Compression {
Gz,
Bz2,
Xz,
#[allow(dead_code)]
None,
}
struct CountReader<R> {
inner: R,
count: Rc<Cell<u64>>,
}
impl<R: io::Read> io::Read for CountReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.inner.read(buf)?;
self.count.set(self.count.get() + n as u64);
Ok(n)
}
}
struct SanitizingReader<R: io::Read> {
inner: R,
buffer: [u8; BLOCK_SIZE],
buf_pos: usize,
buf_len: usize,
}
impl<R: io::Read> io::Read for SanitizingReader<R> {
fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
if self.buf_pos >= self.buf_len {
let mut block = [0u8; BLOCK_SIZE];
let n = self.inner.read(&mut block)?;
if n == 0 {
return Ok(0);
}
if n == BLOCK_SIZE {
sanitize_header(&mut block);
}
self.buffer = block;
self.buf_len = n;
self.buf_pos = 0;
}
let available = self.buf_len - self.buf_pos;
let to_copy = available.min(out.len());
out[..to_copy].copy_from_slice(&self.buffer[self.buf_pos..self.buf_pos + to_copy]);
self.buf_pos += to_copy;
Ok(to_copy)
}
}
fn sanitize_reader(reader: Box<dyn std::io::Read>) -> Box<dyn std::io::Read> {
Box::new(SanitizingReader {
inner: reader,
buffer: [0u8; BLOCK_SIZE],
buf_pos: 0,
buf_len: 0,
})
}
fn wrap_reader(reader: Box<dyn std::io::Read>, compression: Option<Compression>) -> Box<dyn std::io::Read> {
match compression {
Some(Compression::Gz) => Box::new(GzDecoder::new(reader)),
Some(Compression::Bz2) => Box::new(BzDecoder::new(reader)),
Some(Compression::Xz) => Box::new(XzDecoder::new(reader)),
Some(Compression::None) | None => reader,
}
}
fn wrap_writer(writer: Box<dyn std::io::Write>, compression: Option<Compression>) -> Box<dyn std::io::Write> {
match compression {
Some(Compression::Gz) => Box::new(GzEncoder::new(writer, FlateCompression::default())),
Some(Compression::Bz2) => Box::new(BzEncoder::new(writer, BzCompression::default())),
Some(Compression::Xz) => Box::new(XzEncoder::new(writer, 6)),
Some(Compression::None) | None => writer,
}
}
fn extract_entry<R: std::io::Read>(entry: &mut tar::Entry<R>, extract_path: &str) -> std::io::Result<()> {
use std::fs;
use std::io::prelude::*;
use tar::EntryType;
let entry_type = entry.header().entry_type();
let path = entry.path()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("failed to get entry path: {}", e)))?;
let dest = std::path::Path::new(extract_path).join(lfs::sanitize_path_for_windows(&path));
if std::env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar] extract_entry: type={:?} dest={:?}", entry_type, dest);
}
match entry_type {
EntryType::Regular => {
let mut file = lfs::file_create(&dest)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let mut reader = entry.take(entry.header().size().unwrap_or(0));
std::io::copy(&mut reader, &mut file)?;
drop(file);
if let Ok(mode) = entry.header().mode() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(mode & 0o777);
lfs::set_permissions(&dest, perms)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
}
}
Ok(())
}
EntryType::Directory => {
lfs::create_dir_all(&dest)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(())
}
EntryType::Symlink => {
let linkname = entry.link_name()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("failed to get link name: {}", e)))?
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "symlink entry missing link name"))?;
let linkname = lfs::sanitize_path_for_windows(&linkname);
#[cfg(unix)]
{
lfs::symlink_for_native(&linkname, &dest)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
}
#[cfg(not(unix))]
{
return Err(std::io::Error::new(std::io::ErrorKind::Other, "symlinks not supported on this platform"));
}
Ok(())
}
_ => {
if let Some(p) = dest.parent() {
lfs::create_dir_all(p)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
}
entry.unpack(&dest)?;
Ok(())
}
}
}
fn maybe_extract_entry<R: std::io::Read>(
entry: &mut tar::Entry<R>,
extract_path: &str,
files: &[String],
all_exclude_patterns: &[String],
matched: &mut [bool],
) -> Result<bool> {
let original_path = entry.path()
.map_err(|e| eyre!("tar: error getting entry path: {}", e))?
.display()
.to_string();
let path = normalize_path(&original_path);
if std::env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar] entry: original_path={:?} path={:?} type={:?} is_dir={}", original_path, path, entry.header().entry_type(), entry.header().entry_type() == tar::EntryType::Directory);
}
let is_dir = entry.header().entry_type() == tar::EntryType::Directory;
let extract = should_extract_entry(&path, is_dir, files, all_exclude_patterns, matched);
if !extract {
return Ok(false);
}
let dest = std::path::Path::new(extract_path).join(lfs::sanitize_path_for_windows(std::path::Path::new(&path)));
if std::env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar] extract: dest={:?} path={:?} extract_path={:?}", dest, path, extract_path);
eprintln!("[tar] entry header: type={:?} size={} mode={:o}", entry.header().entry_type(), entry.header().size().unwrap_or(0), entry.header().mode().unwrap_or(0));
}
if entry.header().entry_type() == tar::EntryType::Regular && dest.exists() {
eprintln!("[tar] dest exists: {}", dest.display());
if let Ok(metadata) = fs::metadata(&dest) {
if metadata.is_dir() {
eprintln!("[tar] removing directory: {}", dest.display());
if let Err(e) = lfs::remove_dir_all(&dest) {
eprintln!("[tar] remove_dir_all failed: {}", e);
}
}
}
}
if std::env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar] before unpack: dest={:?} exists={}", dest, dest.exists());
if dest.exists() {
if let Ok(metadata) = dest.metadata() {
eprintln!("[tar] dest metadata: is_dir={}", metadata.is_dir());
}
}
let _ = std::fs::read_dir(".").map(|entries| {
for entry in entries {
if let Ok(e) = entry {
eprintln!("[tar] dir entry: {:?}", e.path());
}
}
});
}
extract_entry(entry, extract_path)
.map_err(|e| {
eprintln!("tar: extract error details: {:?}", e);
eyre!("tar: error extracting entry '{}': {}", path, e)
})?;
if entry.header().entry_type() == tar::EntryType::Directory {
lfs::create_dir_all(&dest)?;
}
Ok(true)
}
pub struct TarOptions {
pub create: bool,
pub extract: bool,
pub list: bool,
pub verbose: bool,
pub extract_to_stdout: bool,
pub file: Option<String>,
pub directory: Option<String>,
pub files: Vec<String>,
pub compression: Option<Compression>,
pub exclude_patterns: Vec<String>,
pub exclude_files: Vec<String>,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<TarOptions> {
let args = matches.get_many::<String>("args")
.map(|vals| vals.cloned().collect::<Vec<String>>())
.unwrap_or_default();
if args.is_empty() {
return Err(eyre!("tar: missing arguments"));
}
parse_traditional_args(&args)
}
fn parse_option_group(
args: &[String],
i: &mut usize,
create: &mut bool,
extract: &mut bool,
list: &mut bool,
extract_to_stdout: &mut bool,
verbose: &mut bool,
file: &mut Option<String>,
directory: &mut Option<String>,
compression: &mut Option<Compression>,
exclude_files: &mut Vec<String>,
) -> Result<()> {
if *i < args.len() && !args[*i].starts_with('-') && args[*i].chars().all(|c| c.is_ascii_alphabetic()) {
let opts = &args[*i];
*i += 1;
for ch in opts.chars() {
match ch {
'c' => *create = true,
'x' => *extract = true,
't' => *list = true,
'O' => *extract_to_stdout = true,
'f' => {
if *i >= args.len() {
return Err(eyre!("tar: missing archive file after -f"));
}
*file = Some(args[*i].clone());
*i += 1;
}
'C' => {
if *i >= args.len() {
return Err(eyre!("tar: missing directory after -C"));
}
*directory = Some(args[*i].clone());
*i += 1;
}
'z' => *compression = Some(Compression::Gz),
'j' => *compression = Some(Compression::Bz2),
'J' => *compression = Some(Compression::Xz),
'v' => *verbose = true,
'X' => {
if *i >= args.len() {
return Err(eyre!("tar: missing exclude file after -X"));
}
exclude_files.push(args[*i].clone());
*i += 1;
}
_ => return Err(eyre!("tar: invalid option '{}'", ch)),
}
}
}
Ok(())
}
fn get_option_arg(
chars: &mut std::str::Chars,
i: &mut usize,
args: &[String],
missing_msg: &str,
) -> Result<String> {
let next_arg = if chars.clone().next().is_some() {
chars.collect::<String>()
} else if *i < args.len() {
let val = args[*i].clone();
*i += 1;
val
} else {
return Err(eyre!("tar: {}", missing_msg));
};
Ok(next_arg)
}
#[allow(unused_variables)]
fn process_short_options(
arg: &str,
i: &mut usize,
args: &[String],
create: &mut bool,
extract: &mut bool,
list: &mut bool,
extract_to_stdout: &mut bool,
verbose: &mut bool,
file: &mut Option<String>,
directory: &mut Option<String>,
compression: &mut Option<Compression>,
exclude_patterns: &mut Vec<String>,
exclude_files: &mut Vec<String>,
) -> Result<()> {
let mut chars = arg.chars();
let _ = chars.next();
while let Some(ch) = chars.next() {
match ch {
'c' => *create = true,
'x' => *extract = true,
't' => *list = true,
'O' => *extract_to_stdout = true,
'f' => {
let next_arg = get_option_arg(&mut chars, i, args, "missing archive file after -f")?;
*file = Some(next_arg);
break;
}
'C' => {
let next_arg = get_option_arg(&mut chars, i, args, "missing directory after -C")?;
*directory = Some(next_arg);
break;
}
'z' => *compression = Some(Compression::Gz),
'j' => *compression = Some(Compression::Bz2),
'J' => *compression = Some(Compression::Xz),
'v' => *verbose = true,
'X' => {
let next_arg = get_option_arg(&mut chars, i, args, "missing exclude file after -X")?;
exclude_files.push(next_arg);
break;
}
_ => return Err(eyre!("tar: invalid option '{}'", ch)),
}
}
Ok(())
}
fn parse_remaining_args(
args: &[String],
i: &mut usize,
create: &mut bool,
extract: &mut bool,
list: &mut bool,
extract_to_stdout: &mut bool,
verbose: &mut bool,
file: &mut Option<String>,
directory: &mut Option<String>,
compression: &mut Option<Compression>,
exclude_patterns: &mut Vec<String>,
exclude_files: &mut Vec<String>,
files: &mut Vec<String>,
) -> Result<()> {
while *i < args.len() {
let arg = &args[*i];
if arg.starts_with('-') {
*i += 1;
if arg == "--" {
break;
}
if arg == "--exclude" {
if *i >= args.len() {
return Err(eyre!("tar: missing pattern after --exclude"));
}
exclude_patterns.push(args[*i].clone());
*i += 1;
continue;
}
process_short_options(arg, i, args, create, extract, list, extract_to_stdout, verbose, file, directory, compression, exclude_patterns, exclude_files)?;
} else {
files.push(args[*i].clone());
*i += 1;
}
}
Ok(())
}
fn parse_traditional_args(args: &[String]) -> Result<TarOptions> {
let mut i = 0;
let mut create = false;
let mut extract = false;
let mut list = false;
let mut extract_to_stdout = false;
let mut verbose = false;
let mut file = None;
let mut directory = None;
let mut files = Vec::new();
let mut compression = None;
let mut exclude_patterns = Vec::new();
let mut exclude_files = Vec::new();
parse_option_group(args, &mut i, &mut create, &mut extract, &mut list, &mut extract_to_stdout, &mut verbose, &mut file, &mut directory, &mut compression, &mut exclude_files)?;
parse_remaining_args(args, &mut i, &mut create, &mut extract, &mut list, &mut extract_to_stdout, &mut verbose, &mut file, &mut directory, &mut compression, &mut exclude_patterns, &mut exclude_files, &mut files)?;
if create && extract {
return Err(eyre!("tar: cannot specify both -c and -x"));
}
if create && list {
return Err(eyre!("tar: cannot specify both -c and -t"));
}
if extract && list {
return Err(eyre!("tar: cannot specify both -x and -t"));
}
if !create && !extract && !list {
return Err(eyre!("tar: must specify one of -c, -x, or -t"));
}
if extract_to_stdout && !extract {
return Err(eyre!("tar: -O requires -x"));
}
Ok(TarOptions {
create,
extract,
list,
extract_to_stdout,
verbose,
file,
directory,
files,
compression,
exclude_patterns: exclude_patterns,
exclude_files: exclude_files,
})
}
pub fn command() -> Command {
Command::new("tar")
.about("Archive files")
.arg(Arg::new("args")
.help("Raw arguments (for traditional tar syntax)")
.num_args(0..)
.trailing_var_arg(true)
.allow_hyphen_values(true))
}
fn create_archive(archive_path: &str, files: &[String], compression: Option<Compression>) -> Result<()> {
let archive_file: Box<dyn std::io::Write> = if archive_path == "-" {
Box::new(std::io::stdout())
} else {
Box::new(File::create(archive_path)
.map_err(|e| eyre!("tar: cannot create '{}': {}", archive_path, e))?)
};
let archive_file = wrap_writer(archive_file, compression);
let mut builder = tar::Builder::new(archive_file);
for file_path in files {
let path = Path::new(file_path);
if path.is_dir() {
builder.append_dir_all(path.file_name().unwrap_or(path.as_os_str()), path)
.map_err(|e| eyre!("tar: error adding directory '{}': {}", file_path, e))?;
} else {
builder.append_path(path)
.map_err(|e| eyre!("tar: error adding file '{}': {}", file_path, e))?;
}
}
builder.finish()
.map_err(|e| eyre!("tar: error finishing archive: {}", e))?;
Ok(())
}
fn normalize_path(p: &str) -> String {
p.strip_prefix("./").unwrap_or(p).to_string()
}
fn load_exclude_patterns(exclude_patterns: &[String], exclude_files: &[String]) -> Result<Vec<String>> {
let mut all_exclude_patterns = Vec::new();
all_exclude_patterns.extend_from_slice(exclude_patterns);
for file in exclude_files {
let content = fs::read_to_string(file)
.map_err(|e| eyre!("tar: cannot read exclude file '{}': {}", file, e))?;
for line in content.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with('#') {
all_exclude_patterns.push(line.to_string());
}
}
}
Ok(all_exclude_patterns)
}
fn matches_any_pattern(path: &str, patterns: &[String]) -> bool {
let norm_path = normalize_path(path);
patterns.iter().any(|pattern| {
let norm_pattern = normalize_path(pattern);
norm_path.starts_with(&norm_pattern)
})
}
fn should_extract_entry(path: &str, is_dir: bool, files: &[String], all_exclude_patterns: &[String], matched: &mut [bool]) -> bool {
if files.is_empty() {
!matches_any_pattern(path, all_exclude_patterns)
} else {
let mut pattern_matched = false;
for (i, pattern) in files.iter().enumerate() {
let norm_pattern = normalize_path(pattern);
if path.starts_with(&norm_pattern) {
matched[i] = true;
pattern_matched = true;
} else if is_dir {
if norm_pattern.starts_with(path) {
pattern_matched = true;
}
}
}
let matches = matches_any_pattern(path, all_exclude_patterns);
pattern_matched && !matches
}
}
fn extract_archive(archive_path: &str, directory: Option<&str>, compression: Option<Compression>, files: &[String], exclude_patterns: &[String], exclude_files: &[String]) -> Result<()> {
let archive_file: Box<dyn std::io::Read> = if archive_path == "-" {
Box::new(std::io::stdin())
} else {
Box::new(File::open(archive_path)
.map_err(|e| eyre!("tar: cannot open '{}': {}", archive_path, e))?)
};
let archive_file = sanitize_reader(wrap_reader(archive_file, compression));
let count = Rc::new(Cell::new(0));
let count_reader = CountReader {
inner: archive_file,
count: count.clone(),
};
let mut archive = tar::Archive::new(count_reader);
archive.set_overwrite(true);
archive.set_preserve_permissions(false);
archive.set_preserve_mtime(false);
archive.set_ignore_zeros(true);
let extract_path = directory.unwrap_or(".");
let all_exclude_patterns = load_exclude_patterns(exclude_patterns, exclude_files)?;
let entries = archive.entries()
.map_err(|e| {
if std::env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar] archive.entries error: {:?}", e);
}
if e.kind() == std::io::ErrorKind::UnexpectedEof {
eyre!("tar: short read")
} else {
eyre!("tar: error reading archive entries: {}", e)
}
})?;
let mut matched = vec![false; files.len()];
let mut has_entry = false;
let mut _extracted_count = 0;
for entry in entries {
let mut entry = entry.map_err(|e| {
if std::env::var_os("EPKG_DEBUG").is_some() {
eprintln!("[tar] entry iteration error: {:?}", e);
}
if e.kind() == std::io::ErrorKind::UnexpectedEof {
eyre!("tar: short read")
} else {
eyre!("tar: error reading entry: {}", e)
}
})?;
has_entry = true;
let extracted = maybe_extract_entry(&mut entry, extract_path, files, &all_exclude_patterns, &mut matched)?;
if extracted {
_extracted_count += 1;
}
}
if !files.is_empty() {
let missing_patterns: Vec<_> = files.iter().enumerate()
.filter(|(i, _)| !matched[*i])
.map(|(_, pattern)| pattern.clone())
.collect();
if !missing_patterns.is_empty() {
return Err(eyre!("tar: {}: not found in archive", missing_patterns.join(", ")));
}
}
if !has_entry && count.get() == 0 {
return Err(eyre!("tar: short read"));
}
Ok(())
}
fn list_archive(archive_path: &str, compression: Option<Compression>, verbose: bool) -> Result<()> {
let archive_file: Box<dyn std::io::Read> = if archive_path == "-" {
Box::new(std::io::stdin())
} else {
Box::new(File::open(archive_path)
.map_err(|e| eyre!("tar: cannot open '{}': {}", archive_path, e))?)
};
let archive_file = sanitize_reader(wrap_reader(archive_file, compression));
let count = Rc::new(Cell::new(0));
let count_reader = CountReader {
inner: archive_file,
count: count.clone(),
};
let mut archive = tar::Archive::new(count_reader);
archive.set_overwrite(true);
archive.set_preserve_permissions(false);
archive.set_preserve_mtime(false);
let entries = archive.entries()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
eyre!("tar: short read")
} else {
eyre!("tar: error reading archive entries: {}", e)
}
})?;
let mut has_entry = false;
let format_mode = |mode: u32, typ: tar::EntryType| -> String {
let mut s = String::with_capacity(10);
match typ {
tar::EntryType::Regular => s.push('-'),
tar::EntryType::Directory => s.push('d'),
tar::EntryType::Symlink => s.push('l'),
tar::EntryType::Char => s.push('c'),
tar::EntryType::Block => s.push('b'),
tar::EntryType::Fifo => s.push('p'),
_ => s.push('?'),
}
s.push(if mode & 0o400 != 0 { 'r' } else { '-' });
s.push(if mode & 0o200 != 0 { 'w' } else { '-' });
s.push(if mode & 0o100 != 0 { 'x' } else { '-' });
s.push(if mode & 0o040 != 0 { 'r' } else { '-' });
s.push(if mode & 0o020 != 0 { 'w' } else { '-' });
s.push(if mode & 0o010 != 0 { 'x' } else { '-' });
s.push(if mode & 0o004 != 0 { 'r' } else { '-' });
s.push(if mode & 0o002 != 0 { 'w' } else { '-' });
s.push(if mode & 0o001 != 0 { 'x' } else { '-' });
s
};
for entry in entries {
has_entry = true;
let entry = match entry {
Ok(entry) => entry,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
eprintln!("tar: short read");
break;
}
Err(e) => return Err(eyre!("tar: error reading entry: {}", e)),
};
let path = entry.path()
.map_err(|e| eyre!("tar: error getting entry path: {}", e))?
.display()
.to_string();
if verbose {
let header = entry.header();
let typ = header.entry_type();
let mode = header.mode().unwrap_or(0);
let perm_str = format_mode(mode, typ);
let _uid = header.uid().unwrap_or(0);
let _gid = header.gid().unwrap_or(0);
let raw = header.as_bytes();
let uname = match header.username().ok() {
Some(Some(s)) if !s.is_empty() => s.to_string(),
_ => {
let s = ustar_name_from_bytes(raw, USTAR_UNAME_OFFSET, USTAR_UNAME_LEN);
if s.is_empty() { "user".to_string() } else { s }
}
};
let gname = match header.groupname().ok() {
Some(Some(s)) if !s.is_empty() => s.to_string(),
_ => {
let s = ustar_name_from_bytes(raw, USTAR_GNAME_OFFSET, USTAR_GNAME_LEN);
if s.is_empty() { "group".to_string() } else { s }
}
};
let size = header.size().unwrap_or(0);
let mtime = header.mtime().unwrap_or(0);
let ts = mtime as i64 + list_tz_offset_seconds();
let timestamp = format_timestamp_utc(ts);
let size_str = {
let s = size.to_string();
let pad = 10_usize.saturating_sub(s.len());
" ".repeat(pad) + &s
};
let link_target = if typ == tar::EntryType::Symlink {
entry.link_name().ok().flatten().map(|p| format!(" -> {}", p.display())).unwrap_or_default()
} else {
String::new()
};
println!("{} {}/{}{} {} {}{}", perm_str, uname, gname, size_str, timestamp, path, link_target);
} else {
println!("{}", path);
}
}
if !has_entry && count.get() == 0 {
return Err(eyre!("tar: short read"));
}
Ok(())
}
fn extract_to_stdout(archive_path: &str, compression: Option<Compression>) -> Result<()> {
let archive_file: Box<dyn std::io::Read> = if archive_path == "-" {
Box::new(std::io::stdin())
} else {
Box::new(File::open(archive_path)
.map_err(|e| eyre!("tar: cannot open '{}': {}", archive_path, e))?)
};
let archive_file = sanitize_reader(wrap_reader(archive_file, compression));
let mut archive = tar::Archive::new(archive_file);
archive.set_overwrite(true);
archive.set_preserve_permissions(false);
archive.set_preserve_mtime(false);
let mut stdout = std::io::stdout();
let entries = archive.entries()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
eyre!("tar: short read")
} else {
eyre!("tar: error reading archive entries: {}", e)
}
})?;
for entry in entries {
let mut entry = entry.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
eyre!("tar: short read")
} else {
eyre!("tar: error reading entry: {}", e)
}
})?;
std::io::copy(&mut entry, &mut stdout)
.map_err(|e| eyre!("tar: error extracting to stdout: {}", e))?;
break;
}
Ok(())
}
fn handle_tar_error(e: color_eyre::eyre::Error) -> ! {
for cause in e.chain() {
eprintln!("{}", cause);
break;
}
std::process::exit(1);
}
fn run_inner(options: TarOptions) -> Result<()> {
let file_path = options.file.as_deref().unwrap_or("-");
if options.create {
if options.files.is_empty() {
return Err(eyre!("tar: no files specified for archive"));
}
create_archive(file_path, &options.files, options.compression)?;
} else if options.extract_to_stdout {
extract_to_stdout(file_path, options.compression)?;
} else if options.extract {
extract_archive(file_path, options.directory.as_deref(), options.compression, &options.files, &options.exclude_patterns, &options.exclude_files)?;
} else if options.list {
list_archive(file_path, options.compression, options.verbose)?;
}
Ok(())
}
pub fn run(options: TarOptions) -> Result<()> {
if let Err(e) = run_inner(options) {
handle_tar_error(e);
}
Ok(())
}