#![forbid(unsafe_code)]
use std::io::prelude::*;
use std::{fs, io, process};
use zbsdiff::Bspatch;
const USAGE: &str = "Usage: zbspatch [OPTIONS] <SOURCE> <TARGET> <PATCH>";
const HELP: &str = "fast and memory saving bsdiff 4.x compatible patcher
Usage: zbspatch [OPTIONS] <SOURCE> <TARGET> <PATCH>
Arguments:
<SOURCE> source file
<TARGET> target file
<PATCH> patch file
Options:
-b, --buffer-size <BYTES> write buffer size in bytes
-h, --help print help
-V, --version print version information";
#[derive(Debug)]
struct BspatchArgs {
source_path: String,
target_path: String,
patch_path: String,
buffer_size: Option<usize>
}
#[derive(Debug)]
enum Action {
Run(BspatchArgs),
Help,
Version
}
fn main() {
let cli_args: Vec<String> = std::env::args().skip(1).collect();
let action = match parse_args(cli_args.iter().map(String::as_str)) {
Ok(action) => action,
Err(message) => {
eprintln!(
"error: {message}\n\n{USAGE}\n\nFor more information, try \
'--help'."
);
process::exit(2);
}
};
match action {
Action::Help => {
println!("{HELP}");
}
Action::Version => {
println!("zbspatch {}", env!("CARGO_PKG_VERSION"));
}
Action::Run(ref parsed) => {
if let Err(e) = execute(parsed) {
eprintln!("error: {e}");
process::exit(1);
}
}
}
}
fn parse_args<'a>(
args: impl IntoIterator<Item = &'a str>
) -> Result<Action, String> {
let mut source_path: Option<String> = None;
let mut target_path: Option<String> = None;
let mut patch_path: Option<String> = None;
let mut buffer_size: Option<usize> = None;
let mut help = false;
let mut version = false;
let mut end_of_options = false;
let mut positionals: usize = 0;
let mut iter = args.into_iter().peekable();
while let Some(arg) = iter.next() {
if end_of_options
|| arg == "-"
|| !arg.starts_with('-')
|| arg.is_empty()
{
match positionals {
0 => source_path = Some(arg.to_string()),
1 => target_path = Some(arg.to_string()),
2 => patch_path = Some(arg.to_string()),
_ => return Err(format!("unexpected argument '{arg}'"))
}
positionals += 1;
continue;
}
if arg == "--" {
end_of_options = true;
continue;
}
if arg.starts_with("--") {
let (name, inline_value) = match arg.split_once('=') {
Some((name, value)) => (name, Some(value)),
None => (arg, None)
};
match name {
"--buffer-size" => {
let raw = match inline_value {
Some(value) => value,
None => iter.next().ok_or_else(|| {
"a value is required for '--buffer-size <BYTES>'"
.to_string()
})?
};
buffer_size = Some(parse_usize_valued(raw)?);
}
"--help" => {
if inline_value.is_some() {
return Err(
"option '--help' does not take a value".to_string()
);
}
help = true;
}
"--version" => {
if inline_value.is_some() {
return Err("option '--version' does not take a value"
.to_string());
}
version = true;
}
_ => {
return Err(format!("unexpected argument '{arg}'"));
}
}
continue;
}
let body = arg.trim_start_matches('-');
if body.chars().all(|c| matches!(c, 'h' | 'V')) && body.len() > 1 {
for flag in body.chars() {
match flag {
'h' => help = true,
'V' => version = true,
_ => return Err(format!("unexpected argument '{arg}'"))
}
}
continue;
}
let mut chars = body.chars();
let Some(flag) = chars.next() else {
return Err(format!("unexpected argument '{arg}'"));
};
let rest: String = chars.collect();
match flag {
'h' | 'V' => {
if !rest.is_empty() {
return Err(format!("unexpected argument '{arg}'"));
}
if flag == 'h' {
help = true;
} else {
version = true;
}
}
'b' => {
let inline = if rest.is_empty() {
None
} else if let Some(stripped) = rest.strip_prefix('=') {
Some(stripped)
} else {
Some(rest.as_str())
};
let raw = match inline {
Some(value) => value,
None => iter.next().ok_or_else(|| {
"a value is required for '-b <BYTES> [--buffer-size \
<BYTES>]'"
.to_string()
})?
};
buffer_size = Some(parse_usize_valued(raw)?);
}
_ => {
return Err(format!("unexpected argument '{arg}'"));
}
}
}
if help {
return Ok(Action::Help);
}
if version {
return Ok(Action::Version);
}
if positionals != 3 {
return Err("expected <SOURCE> <TARGET> <PATCH>".to_string());
}
let (Some(source_path), Some(target_path), Some(patch_path)) =
(source_path, target_path, patch_path)
else {
return Err("expected <SOURCE> <TARGET> <PATCH>".to_string());
};
Ok(Action::Run(BspatchArgs {
source_path,
target_path,
patch_path,
buffer_size
}))
}
fn parse_usize_valued(raw: &str) -> Result<usize, String> {
raw.parse::<usize>().map_err(|_| {
format!(
"invalid value '{raw}' for '--buffer-size <BYTES>': expected a \
non-negative integer"
)
})
}
fn execute(args: &BspatchArgs) -> io::Result<()> {
if args.source_path == "-" && args.patch_path == "-" {
return Err(io::Error::other("source and patch are both from stdin"));
}
let source = input_bytes(&args.source_path)?;
let patch = input_bytes(&args.patch_path)?;
let target = output_writer(&args.target_path)?;
let mut bspatch = Bspatch::new(patch.as_slice())?;
if let Some(buffer_size) = args.buffer_size {
bspatch = bspatch.buffer_size(buffer_size);
bspatch = bspatch.delta_min(buffer_size / 4);
}
bspatch.apply(source.as_slice(), target)?;
Ok(())
}
fn input_bytes(path: &str) -> io::Result<Vec<u8>> {
let mut data;
if path == "-" {
data = Vec::new();
io::stdin().read_to_end(&mut data)?;
} else {
data = fs::read(path)?;
}
Ok(data)
}
fn output_writer(path: &str) -> io::Result<Box<dyn Write>> {
if path == "-" {
Ok(Box::new(io::stdout()))
} else {
Ok(Box::new(fs::File::create(path)?))
}
}