zbsdiff-cli 1.6.5

CLI tools for zbsdiff
//! zbspatch command line tool.
#![forbid(unsafe_code)]
use std::io::prelude::*;
use std::{fs, io, process};

use zbsdiff::Bspatch;

/// Usage message printed on argument errors.
const USAGE: &str = "Usage: zbspatch [OPTIONS] <SOURCE> <TARGET> <PATCH>";

/// Help text printed for `-h`/`--help`.
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";

/// Command line arguments for zbspatch.
#[derive(Debug)]
struct BspatchArgs {
    /// Source file path, or `-` for stdin.
    source_path: String,
    /// Target file path, or `-` for stdout.
    target_path: String,
    /// Patch file path, or `-` for stdin.
    patch_path: String,
    /// Write buffer size in bytes.
    buffer_size: Option<usize>
}

/// Action requested by the command line arguments.
#[derive(Debug)]
enum Action {
    /// Run the patcher with the parsed arguments.
    Run(BspatchArgs),
    /// Print help text and exit successfully.
    Help,
    /// Print version information and exit successfully.
    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);
            }
        }
    }
}

/// Parses command line arguments.
///
/// # Errors
///
/// Returns a message describing the parse failure.
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;
        }
        // Short options: `-x`, `-xvalue`, `-x=value`, or bundles of
        // flag-only options such as `-hV`.
        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
    }))
}

/// Parses a raw `usize` value.
///
/// # Errors
///
/// Returns a message if the value is not a non-negative integer.
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"
        )
    })
}

/// Executes the zbspatch process.
///
/// # Errors
///
/// Returns error if validation fails or I/O operations fail.
fn execute(args: &BspatchArgs) -> io::Result<()> {
    // setup input/output
    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)?;

    // setup delta patcher
    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);
    }

    // execute delta patcher
    bspatch.apply(source.as_slice(), target)?;
    Ok(())
}

/// Reads bytes from a file or stdin.
///
/// # Errors
///
/// Returns error if the file cannot be read.
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)
}

/// Creates an output writer for a file or stdout.
///
/// # Errors
///
/// Returns error if the file cannot be created.
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)?))
    }
}