mkups 0.1.0

Toolkit for creating, applying, and inspecting .ups patches
Documentation
use std::fmt::Display;
use std::fs::File;
use std::path::PathBuf;

use mkups::{parse, create, apply, data::{Trailer, Hunk}};
use clap::{Parser, Subcommand};
use nom::Finish;

mod mman;

#[derive(Parser)]
#[command(version, about)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Creates a .ups patch. The patch is written to standard output.
    Create {
        /// Input file
        input: PathBuf,

        /// Patched file
        patched: PathBuf,
    },

    /// Applies a .ups patch. The input file is unmodified, the patched file is
    /// written to standard output.
    Apply {
        /// Input file
        input: PathBuf,

        /// .ups file to apply
        patch: PathBuf,

        /// Assume forward application of the patch (usually auto-detected)
        #[arg(long, short)]
        forward: bool,

        /// Assume reverse application of the patch (usually auto-detected)
        #[arg(long, short)]
        reverse: bool,
    },

    /// Shows the contents of a .ups file
    Show {
        /// .ups file to show
        patch: PathBuf,
    },
}

#[derive(Debug)]
pub enum CliError {
    Usage(String),
    BadPatch(String),
    Io(std::io::Error),
}

impl From<std::io::Error> for CliError {
    fn from(err: std::io::Error) -> Self {
        CliError::Io(err)
    }
}

impl From<mman::MapError> for CliError {
    fn from(err: mman::MapError) -> Self {
        match err {
            mman::MapError::ZeroLength => CliError::BadPatch("Zero-length patch file".to_owned()),
            mman::MapError::Io(io) => io.into(),
        }
    }
}

impl From<apply::ApplyError> for CliError {
    fn from(err: apply::ApplyError) -> Self {
        match err {
            apply::ApplyError::Io(io) => io.into(),
            apply::ApplyError::Parse(msg) => CliError::BadPatch(msg),
            apply::ApplyError::DirectionMustBeSpecified => CliError::Usage("Direction must be specified".to_owned()),
        }
    }
}

impl From<parse::Error<'_>> for CliError {
    fn from(err: parse::Error) -> Self {
        CliError::BadPatch(parse::format_error(err))
    }
}

impl Display for CliError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CliError::Usage(u) => write!(f, "Error: {}", u),
            CliError::BadPatch(b) => write!(f, "Malformed patch:\n{}", b),
            CliError::Io(io) => write!(f, "IO error {:?}", io),
        }
    }
}

fn main() {
    match main1() {
        Ok(_) => (),
        Err(e) => {
            eprintln!("{}", e);
            std::process::exit(1);
        },
    }
}

fn main1() -> Result<(), CliError> {
    let cli = Cli::parse();
    match &cli.command {
        Commands::Create { input, patched } => {
            let input_file = File::open(input)?;
            let input_buf = mman::MappedFile::new(&input_file)?;
            let patched_file = File::open(patched)?;
            let patched_buf = mman::MappedFile::new(&patched_file)?;
            create::create(input_buf.data(), patched_buf.data(), &mut std::io::stdout())?;
            Ok(())
        },

        Commands::Apply { input, patch, forward, reverse } => {
            let direction = match (forward, reverse) {
                (false, false) => apply::Direction::Auto,
                (true, false) => apply::Direction::Forward,
                (false, true) => apply::Direction::Reverse,
                _ => return Err(CliError::Usage("Only one of --forward or --reverse may be specified".to_owned())),
            };
            let mut input_file = File::open(input)?;
            let patch_file = File::open(patch)?;
            let patch_buf = mman::MappedFile::new(&patch_file)?;
            apply::patch_file(&mut input_file, &mut std::io::stdout(), patch_buf.data(), direction)?;
            Ok(())
        },

        Commands::Show { patch } => show(patch),
    }
}

fn show(patch: &PathBuf) -> Result<(), CliError> {
    println!("UPS Patch {}:", patch.as_path().to_str().unwrap());
    let f = File::open(patch)?;
    let patch_buf = mman::MappedFile::new(&f)?;
    let input = patch_buf.data();
    let (input, header) = parse::header(input).finish()?;
    println!("  Source length:      {}", header.src_len);
    println!("  Destination length: {}", header.src_len);
    println!();
    let input = show_hunks(input)?;
    let (_, trailer) = parse::trailer(input).finish()?;
    println!("CRCs: Source=0x{:08x} Destination=0x{:08x} Patch=0x{:08x}", trailer.src_crc, trailer.dst_crc, trailer.ups_crc);
    Ok(())
}

fn show_hunks(input: &[u8]) -> Result<&[u8], parse::Error> {
    let mut input = input;
    let mut n = 1;
    let mut pos = 0usize;
    while input.len() > Trailer::LENGTH {
        let hunk: Hunk;
        (input, hunk) = parse::hunk(input).finish()?;
        pos += hunk.skip;
        println!("Hunk #{}: Patch 0x{:x} bytes at 0x{:08x}", n, hunk.xor.len(), pos);
        show_hexdump(hunk.xor);
        println!();
        pos += hunk.xor.len() + 1;
        n += 1;
    }
    Ok(input)
}

fn show_hexdump(input: &[u8]) {
    for (n, b) in input.iter().enumerate() {
        if n % 16 == 0 {
            print!("  {:08x}  ", n);
        }
        print!("{:02x}", b);
        if n % 2 == 1 {
            print!(" ");
        }
        if n % 8 == 7 {
            print!(" ");
        }
        if n % 16 == 15 {
            println!();
        }
    }
    println!();
}