use std::env::set_current_dir;
use std::fs::{create_dir, read_dir, File};
use std::io::{ErrorKind, Stderr, Write};
use std::num::NonZeroU32;
use std::path::Path;
use std::process::ExitCode;
use glob::Pattern;
use lexopt::prelude::*;
use threecpio::examine::examine_cpio_content;
use threecpio::extract::{extract_cpio_archive, ExtractOptions, ExtractTarget};
use threecpio::logger::{Level, Logger};
use threecpio::ranges::Ranges;
use threecpio::{create_cpio_archive, get_cpio_archive_count, list_cpio_content};
#[derive(Debug)]
struct Args {
count: bool,
create: bool,
data_alignment: Option<NonZeroU32>,
directory: String,
examine: bool,
extract: bool,
force: bool,
list: bool,
log_level: Level,
archives: Vec<String>,
make_directories: bool,
parts: Option<Ranges>,
patterns: Vec<Pattern>,
preserve_permissions: bool,
raw: bool,
subdir: Option<String>,
to_stdout: bool,
}
impl Args {
fn extract_options(&self) -> ExtractOptions {
ExtractOptions::new(
self.make_directories,
self.parts.clone(),
self.patterns.clone(),
self.preserve_permissions,
self.subdir.clone(),
)
}
}
fn gnu_println(args: std::fmt::Arguments) {
let mut stdout = std::io::stdout().lock();
if let Err(e) = writeln!(stdout, "{}", args) {
if e.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(141); } else {
let name = std::option_env!("CARGO_BIN_NAME").unwrap();
let _ = writeln!(std::io::stderr(), "{}: stdout write error: {}", name, e);
std::process::exit(1);
}
}
}
macro_rules! gnu_println {
($($arg:tt)*) => {
gnu_println(format_args!($($arg)*))
};
}
fn print_help() {
let executable = std::env::args().next().unwrap();
gnu_println!(
"Usage:
{executable} --count ARCHIVE
{executable} {{-c|--create}} [-v|--debug] [-C DIR] [--data-align BYTES] [ARCHIVE] < manifest
{executable} {{-e|--examine}} [--raw] ARCHIVE
{executable} {{-t|--list}} [-v|--debug] [-P LIST] ARCHIVE [pattern...]
{executable} {{-x|--extract}} [-v|--debug] [-C DIR] [--make-directories] [-P LIST] \
[-p] [-s NAME] [--to-stdout] [--force] [-F|--file] \
ARCHIVE [-F|--file ARCHIVE...] [pattern...]
Optional arguments:
--count Print the number of concatenated cpio archives.
-c, --create Create a new cpio archive from the manifest on stdin.
-e, --examine List the offsets of the cpio archives and their compression.
-t, --list List the contents of the cpio archives.
-x, --extract Extract cpio archives.
-C, --directory=DIR Change directory before performing any operation.
--data-align=BYTES Pad the cpio metadata to align the file data on BYTEs.
--make-directories Create leading directories where needed.
-P, --parts=LIST Only operate on the cpio archives that matches LIST.
-p, --preserve-permissions
Set permissions of extracted files to those recorded in the
archive (default for superuser).
--raw Use a machine-readable output format.
-s, --subdir Extract the cpio archives into separate directories (using the
given name plus an incrementing number)
--to-stdout Extract files to standard output
-v, --verbose Verbose output
--debug Debug output
--force Force overwriting existing files
-h, --help print help message
-V, --version print version number and exit",
);
}
fn print_version() {
let name = std::option_env!("CARGO_BIN_NAME").unwrap();
let version = std::option_env!("CARGO_PKG_VERSION").unwrap();
gnu_println!("{name} {version}");
}
fn parse_args() -> Result<Args, lexopt::Error> {
let mut count = 0;
let mut create = 0;
let mut data_alignment = None;
let mut examine = 0;
let mut extract = 0;
let mut force = false;
let mut parts = None;
let mut preserve_permissions = is_root();
let mut list = 0;
let mut log_level = Level::Warning;
let mut directory = ".".into();
let mut archives = Vec::new();
let mut make_directories = false;
let mut patterns = Vec::new();
let mut raw = false;
let mut subdir: Option<String> = None;
let mut to_stdout = false;
let mut arguments = Vec::new();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Long("count") => {
count = 1;
}
Short('c') | Long("create") => {
create = 1;
}
Short('C') | Long("directory") => {
directory = parser.value()?.string()?;
}
Long("data-align") => {
let value = parser.value()?;
data_alignment = if let Ok(int_value) = value.parse::<NonZeroU32>() {
if int_value.get() % 4 != 0 {
return Err("--data-align must be a multiple of 4 bytes".into());
}
Some(int_value)
} else {
return Err("--data-align must be a positive number".into());
};
}
Long("debug") => {
log_level = Level::Debug;
}
Short('e') | Long("examine") => {
examine = 1;
}
Long("force") => {
force = true;
}
Short('h') | Long("help") => {
print_help();
std::process::exit(0);
}
Long("make-directories") => {
make_directories = true;
}
Short('P') | Long("parts") => {
parts = Some(parser.value()?.parse()?);
}
Short('p') | Long("preserve-permissions") => {
preserve_permissions = true;
}
Long("raw") => {
raw = true;
}
Short('s') | Long("subdir") => {
subdir = Some(parser.value()?.string()?);
}
Short('t') | Long("list") => {
list = 1;
}
Long("to-stdout") => {
to_stdout = true;
}
Short('v') | Long("verbose") => {
if log_level <= Level::Info {
log_level = Level::Info;
}
}
Short('V') | Long("version") => {
print_version();
std::process::exit(0);
}
Short('x') | Long("extract") => {
extract = 1;
}
Value(val) if archives.is_empty() => {
archives.push(val.string()?);
}
Short('F') | Long("file") => {
archives.push(parser.value()?.string()?);
}
Value(val) => arguments.push(val.string()?),
_ => return Err(arg.unexpected()),
}
}
if count + create + examine + extract + list != 1 {
return Err(
"Either --count, --create, --examine, --extract, or --list must be specified!".into(),
);
}
if extract + list == 1 {
for argument in arguments {
let pattern = Pattern::new(&argument)
.map_err(|e| format!("invalid pattern '{argument}': {e}"))?;
patterns.push(pattern);
}
} else if !arguments.is_empty() {
let first = &arguments[0];
return Err(Value(first.into()).unexpected());
}
if let Some(ref s) = subdir {
if s.contains('/') {
return Err(format!("Subdir '{s}' must not contain slashes!").into());
}
}
if create != 1 && archives.is_empty() {
return Err("missing argument ARCHIVE".into());
}
if extract != 1 && archives.len() > 1 {
return Err("specifying multiple --file=ARCHIVE is only supported for --extract".into());
}
Ok(Args {
count: count == 1,
create: create == 1,
data_alignment,
directory,
examine: examine == 1,
extract: extract == 1,
force,
list: list == 1,
log_level,
archives,
make_directories,
parts,
patterns,
preserve_permissions,
raw,
subdir,
to_stdout,
})
}
fn is_empty_directory<P: AsRef<Path>>(path: P) -> std::io::Result<bool> {
Ok(read_dir(path)?.next().is_none())
}
fn is_root() -> bool {
let uid = unsafe { libc::getuid() };
uid == 0
}
fn create_and_set_current_dir(path: &str, force: bool) -> Result<(), String> {
if let Err(e) = set_current_dir(path) {
if e.kind() != ErrorKind::NotFound {
return Err(format!("Failed to change directory to '{path}': {e}"));
}
if let Err(e) = create_dir(path) {
return Err(format!("Failed to create directory '{path}': {e}"));
}
if let Err(e) = set_current_dir(path) {
return Err(format!("Failed to change directory to '{path}': {e}"));
}
}
if !force {
match is_empty_directory(".") {
Err(e) => {
return Err(format!(
"Failed to check content of directory '{path}': {e}"
));
}
Ok(false) => {
return Err(format!(
"Target directory '{path}' is not empty. \
Use --force to overwrite existing files!",
));
}
Ok(true) => {}
}
}
Ok(())
}
fn print_cpio_archive_count<W: Write>(mut archive: File, out: &mut W) -> std::io::Result<()> {
let count = get_cpio_archive_count(&mut archive)?;
writeln!(out, "{count}")?;
Ok(())
}
fn operate_on_archive(
archive: File,
args: &Args,
cwd: &Path,
logger: &mut Logger<Stderr>,
) -> (&'static str, std::io::Result<()>) {
let mut stdout = std::io::stdout();
if args.count {
(
"count number of cpio archives",
print_cpio_archive_count(archive, &mut stdout),
)
} else if args.examine {
(
"examine content",
examine_cpio_content(archive, &mut stdout, args.raw),
)
} else if args.extract {
(
"extract content",
extract_cpio_archive(
archive,
if args.to_stdout {
ExtractTarget::WritableStream(&mut stdout)
} else {
ExtractTarget::Directory(cwd.to_path_buf())
},
&args.extract_options(),
logger,
),
)
} else if args.list {
(
"list content",
list_cpio_content(
archive,
&mut stdout,
args.parts.as_ref(),
&args.patterns,
args.log_level.clone(),
),
)
} else {
unreachable!("no operation specified");
}
}
fn main() -> ExitCode {
let executable = std::env::args().next().unwrap();
let args = match parse_args() {
Ok(a) => a,
Err(e) => {
eprintln!("{executable}: Error: {e}");
return ExitCode::from(2);
}
};
let mut logger = Logger::new_stderr(args.log_level.clone());
if args.create {
let mut archive = None;
if let Some(path) = args.archives.first() {
archive = match File::create(path) {
Ok(f) => Some(f),
Err(e) => {
eprintln!("{executable}: Error: Failed to create '{path}': {e}");
return ExitCode::FAILURE;
}
};
if args.log_level >= Level::Debug {
eprintln!("{executable}: Opened '{path}' for writing.");
}
}
if let Err(e) = set_current_dir(&args.directory) {
eprintln!(
"{executable}: Error: Failed to change directory to '{}': {e}",
args.directory,
);
return ExitCode::FAILURE;
}
let result = create_cpio_archive(archive, args.data_alignment, &mut logger);
if let Err(error) = result {
match error.kind() {
ErrorKind::BrokenPipe => {}
_ => {
eprintln!(
"{executable}: Error: Failed to create '{}': {error}",
args.archives.first().unwrap_or(&"cpio on stdout".into()),
);
return ExitCode::FAILURE;
}
}
}
return ExitCode::SUCCESS;
};
let mut opened_archives = Vec::new();
for path in &args.archives {
let archive = match File::open(path) {
Ok(f) => f,
Err(e) => {
eprintln!("{executable}: Error: Failed to open '{path}': {e}");
return ExitCode::FAILURE;
}
};
opened_archives.push((path.clone(), archive));
}
if args.extract && !args.to_stdout {
if let Err(e) = create_and_set_current_dir(&args.directory, args.force) {
eprintln!("{executable}: Error: {e}");
return ExitCode::FAILURE;
}
}
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(e) => {
eprintln!("{executable}: Error: Failed to determine current directory: {e}");
return ExitCode::FAILURE;
}
};
for (path, archive) in opened_archives {
let (operation, result) = operate_on_archive(archive, &args, &cwd, &mut logger);
if let Err(e) = result {
match e.kind() {
ErrorKind::BrokenPipe => break,
_ => {
eprintln!("{executable}: Error: Failed to {operation} of '{path}': {e}");
return ExitCode::FAILURE;
}
}
}
}
ExitCode::SUCCESS
}