Files
sausage/userland/archive-tools/src/tar.rs
T

222 lines
7.5 KiB
Rust

//! Create, list, and extract portable tar archives through standard WASI files.
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use std::{
fs::File,
io::{self, Read, Write},
path::{Path, PathBuf},
};
fn append<W: Write>(
builder: &mut tar::Builder<W>,
source: &Path,
name: &Path,
verbose: Option<bool>,
) -> io::Result<()> {
let meta = std::fs::symlink_metadata(source)?;
let mut header = tar::Header::new_gnu();
header.set_uid(0);
header.set_gid(0);
header.set_mtime(
meta.modified()?
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
);
if let Some(stdout) = verbose {
let suffix = if meta.is_dir() { "/" } else { "" };
if stdout {
println!("{}{suffix}", name.display());
} else {
eprintln!("{}{suffix}", name.display());
}
}
if meta.is_dir() {
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
header.set_mode(0o755);
header.set_cksum();
builder.append_data(&mut header, name, io::empty())?;
let mut entries = std::fs::read_dir(source)?.collect::<io::Result<Vec<_>>>()?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
append(
builder,
&entry.path(),
&name.join(entry.file_name()),
verbose,
)?;
}
} else if meta.is_file() {
header.set_size(meta.len());
header.set_mode(0o644);
header.set_cksum();
builder.append_data(&mut header, name, File::open(source)?)?;
} else if meta.file_type().is_symlink() {
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header.set_mode(0o777);
header.set_cksum();
builder.append_link(&mut header, name, std::fs::read_link(source)?)?;
} else {
return Err(io::Error::other("unsupported special file"));
}
Ok(())
}
fn create<W: Write>(
writer: W,
cwd: &Path,
paths: &[String],
verbose: Option<bool>,
) -> io::Result<W> {
let mut builder = tar::Builder::new(writer);
for path in paths {
let name = Path::new(path).strip_prefix("/").unwrap_or(Path::new(path));
append(&mut builder, &cwd.join(path), name, verbose)?;
}
builder.finish()?;
builder.into_inner()
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let mut mode = None;
let mut compressed = false;
let mut verbose = false;
let mut archive = None;
let mut cwd = PathBuf::from(".");
let mut strip = 0usize;
let mut paths = Vec::new();
while let Some(arg) = args.next() {
if arg == "--" {
paths.extend(args);
break;
}
if arg == "--strip-components" {
strip = args.next().ok_or("missing strip count")?.parse()?;
continue;
}
if let Some(n) = arg.strip_prefix("--strip-components=") {
strip = n.parse()?;
continue;
}
if arg.starts_with('-') || (mode.is_none() && arg.chars().all(|c| "cxtzvf".contains(c))) {
let flags = arg.trim_start_matches('-');
let mut chars = flags.chars();
while let Some(flag) = chars.next() {
match flag {
'c' | 'x' | 't' => {
if mode.replace(flag).is_some() {
return Err("choose one archive operation".into());
}
}
'z' => compressed = true,
'v' => verbose = true,
'f' | 'C' => {
let tail = chars.as_str();
let value = if tail.is_empty() {
args.next().ok_or("missing option value")?
} else {
tail.to_owned()
};
if flag == 'f' {
archive = Some(value);
} else {
cwd = PathBuf::from(value);
}
break;
}
_ => return Err(format!("unsupported option: {flag}").into()),
}
}
} else {
paths.push(arg);
}
}
let archive = archive.ok_or("-f is required")?;
match mode.ok_or("an operation is required")? {
'c' => {
if paths.is_empty() {
return Err("refusing to create an empty archive".into());
}
let verbose = verbose.then_some(archive != "-");
let writer: Box<dyn Write> = if archive == "-" {
Box::new(io::stdout())
} else {
Box::new(File::create(archive)?)
};
if compressed {
create(
GzEncoder::new(writer, Compression::default()),
&cwd,
&paths,
verbose,
)?
.finish()?
.flush()?;
} else {
create(writer, &cwd, &paths, verbose)?.flush()?;
}
}
operation => {
let reader: Box<dyn Read> = if archive == "-" {
Box::new(io::stdin())
} else {
Box::new(File::open(archive)?)
};
let reader: Box<dyn Read> = if compressed {
Box::new(GzDecoder::new(reader))
} else {
reader
};
let mut archive = tar::Archive::new(reader);
for entry in archive.entries()? {
let mut entry = entry?;
let name = entry.path()?.into_owned();
if !paths.is_empty() && !paths.iter().any(|p| name.starts_with(p)) {
continue;
}
if operation == 't' {
println!("{}", name.display());
continue;
}
let name: PathBuf = name
.components()
.filter(|c| !matches!(c, std::path::Component::CurDir))
.skip(strip)
.collect();
if name.as_os_str().is_empty() {
continue;
}
if name
.components()
.any(|c| !matches!(c, std::path::Component::Normal(_)))
{
return Err("archive path escapes destination".into());
}
if verbose {
println!("{}", name.display());
}
let dest = cwd.join(name);
if entry.header().entry_type().is_dir() {
std::fs::create_dir_all(dest)?;
} else if entry.header().entry_type().is_file() {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
io::copy(&mut entry, &mut File::create(dest)?)?;
} else {
return Err(
"world extraction supports regular files and directories only".into(),
);
}
}
}
}
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("tar: {e}");
std::process::exit(2);
}
}