#![forbid(unsafe_code)]
use std::io::prelude::*;
use std::{fs, io, process};
use zbsdiff::{Bsdiff, ParallelScheme};
const USAGE: &str = "Usage: zbsdiff [OPTIONS] <SOURCE> <TARGET> <PATCH>";
const HELP: &str = "fast and memory saving bsdiff 4.x compatible delta \
compressor
Usage: zbsdiff [OPTIONS] <SOURCE> <TARGET> <PATCH>
Arguments:
<SOURCE> source file
<TARGET> target file
<PATCH> patch file
Options:
-P, --no-parallel disable parallel searching
-c, --chunk-size <BYTES> parallel chunk size in bytes (minimum 256 KiB)
-b, --buffer-size <BYTES> delta calculation buffer size in bytes
-z, --compress-level <LEVEL> bzip2 compression level (0-9)
-s, --small-match <BYTES> skip exact matches no longer than this many \
bytes
-h, --help print help
-V, --version print version information";
#[derive(Debug)]
struct BsdiffArgs {
source_path: String,
target_path: String,
patch_path: String,
parallel: bool,
chunk_size: Option<usize>,
buffer_size: Option<usize>,
compress_level: Option<u32>,
small_match: Option<usize>
}
#[derive(Debug)]
enum Action {
Run(BsdiffArgs),
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!("zbsdiff {}", 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 parallel = true;
let mut chunk_size: Option<usize> = None;
let mut buffer_size: Option<usize> = None;
let mut compress_level: Option<u32> = None;
let mut small_match: 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()
{
positionals = push_positional(
arg,
&mut source_path,
&mut target_path,
&mut patch_path,
positionals
)?;
continue;
}
if arg == "--" {
end_of_options = true;
continue;
}
if arg.starts_with("--") {
let (name, inline_value) = split_long(arg);
match name {
"--no-parallel" => {
reject_option_value(name, inline_value)?;
parallel = false;
}
"--chunk-size" => {
chunk_size = Some(parse_usize_value(
name,
inline_value,
&mut iter,
"<BYTES>"
)?);
}
"--buffer-size" => {
buffer_size = Some(parse_usize_value(
name,
inline_value,
&mut iter,
"<BYTES>"
)?);
}
"--compress-level" => {
compress_level = Some(parse_u32_value(
name,
inline_value,
&mut iter,
"<LEVEL>"
)?);
}
"--small-match" => {
small_match = Some(parse_usize_value(
name,
inline_value,
&mut iter,
"<BYTES>"
)?);
}
"--help" => {
reject_option_value(name, inline_value)?;
help = true;
}
"--version" => {
reject_option_value(name, inline_value)?;
version = true;
}
_ => {
return Err(format!("unexpected argument '{arg}'"));
}
}
continue;
}
let body = arg.trim_start_matches('-');
if body.chars().all(|c| matches!(c, 'P' | 'h' | 'V')) && body.len() > 1
{
for flag in body.chars() {
match flag {
'P' => parallel = false,
'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 {
'P' | 'h' | 'V' => {
if !rest.is_empty() {
return Err(format!("unexpected argument '{arg}'"));
}
match flag {
'P' => parallel = false,
'h' => help = true,
_ => version = true
}
}
'c' | 'b' | 'z' | 's' => {
let name = match flag {
'c' => "--chunk-size",
'b' => "--buffer-size",
'z' => "--compress-level",
_ => "--small-match"
};
let value_name =
if flag == 'z' { "<LEVEL>" } else { "<BYTES>" };
if let Some(inline) = attached_value(&rest) {
if flag == 'z' {
compress_level =
Some(parse_u32_valued(name, inline, value_name)?);
} else {
let parsed =
parse_usize_valued(name, inline, value_name)?;
match flag {
'c' => chunk_size = Some(parsed),
'b' => buffer_size = Some(parsed),
_ => small_match = Some(parsed)
}
}
} else {
let raw = iter.next().ok_or_else(|| {
format!(
"a value is required for '-{flag} {value_name} \
[{name} {value_name}]'"
)
})?;
if flag == 'z' {
compress_level =
Some(parse_u32_valued(name, raw, value_name)?);
} else {
let parsed = parse_usize_valued(name, raw, value_name)?;
match flag {
'c' => chunk_size = Some(parsed),
'b' => buffer_size = Some(parsed),
_ => small_match = Some(parsed)
}
}
}
}
_ => {
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(BsdiffArgs {
source_path,
target_path,
patch_path,
parallel,
chunk_size,
buffer_size,
compress_level,
small_match
}))
}
fn push_positional(
arg: &str,
source_path: &mut Option<String>,
target_path: &mut Option<String>,
patch_path: &mut Option<String>,
positionals: usize
) -> Result<usize, String> {
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}'"))
}
Ok(positionals + 1)
}
fn split_long(arg: &str) -> (&str, Option<&str>) {
match arg.split_once('=') {
Some((name, value)) => (name, Some(value)),
None => (arg, None)
}
}
fn attached_value(rest: &str) -> Option<&str> {
if rest.is_empty() {
None
} else if let Some(stripped) = rest.strip_prefix('=') {
Some(stripped)
} else {
Some(rest)
}
}
fn reject_option_value(
name: &str,
inline_value: Option<&str>
) -> Result<(), String> {
if inline_value.is_some() {
return Err(format!("option '{name}' does not take a value"));
}
Ok(())
}
fn parse_usize_value<'a>(
name: &str,
inline_value: Option<&'a str>,
iter: &mut impl Iterator<Item = &'a str>,
value_name: &str
) -> Result<usize, String> {
let raw = match inline_value {
Some(value) => value,
None => iter.next().ok_or_else(|| {
format!("a value is required for '{name} {value_name}'")
})?
};
parse_usize_valued(name, raw, value_name)
}
fn parse_u32_value<'a>(
name: &str,
inline_value: Option<&'a str>,
iter: &mut impl Iterator<Item = &'a str>,
value_name: &str
) -> Result<u32, String> {
let raw = match inline_value {
Some(value) => value,
None => iter.next().ok_or_else(|| {
format!("a value is required for '{name} {value_name}'")
})?
};
parse_u32_valued(name, raw, value_name)
}
fn parse_usize_valued(
name: &str,
raw: &str,
value_name: &str
) -> Result<usize, String> {
raw.parse::<usize>().map_err(|_| {
format!(
"invalid value '{raw}' for '{name} {value_name}': expected a \
non-negative integer"
)
})
}
fn parse_u32_valued(
name: &str,
raw: &str,
value_name: &str
) -> Result<u32, String> {
raw.parse::<u32>().map_err(|_| {
format!(
"invalid value '{raw}' for '{name} {value_name}': expected a \
non-negative integer"
)
})
}
fn execute(args: &BsdiffArgs) -> io::Result<()> {
if !matches!(args.compress_level, Some(0..=9) | None) {
return Err(io::Error::other("compression level must be in range 0-9"));
}
if args.source_path == "-" && args.target_path == "-" {
return Err(io::Error::other("source and target are both from stdin"));
}
let source = input_bytes(&args.source_path)?;
let target = input_bytes(&args.target_path)?;
let patch = output_writer(&args.patch_path)?;
let mut bsdiff = Bsdiff::new(source.as_slice(), target.as_slice());
if !args.parallel {
if args.chunk_size.is_some() {
return Err(io::Error::other(
"--chunk-size cannot be used with --no-parallel"
));
}
bsdiff = bsdiff.parallel_scheme(ParallelScheme::Never);
} else if let Some(mut chunk_size) = args.chunk_size {
chunk_size = Ord::max(chunk_size, 256 * 1024);
bsdiff = bsdiff.parallel_scheme(ParallelScheme::ChunkSize(chunk_size));
} else {
bsdiff = bsdiff.parallel_scheme(ParallelScheme::Auto);
}
if let Some(compress_level) = args.compress_level {
bsdiff = bsdiff.compression_level(compress_level);
}
if let Some(buffer_size) = args.buffer_size {
bsdiff = bsdiff.buffer_size(buffer_size);
}
if let Some(small_match) = args.small_match {
bsdiff = bsdiff.small_match(small_match);
}
bsdiff.compare(patch)?;
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)?))
}
}