bdiff 0.1.0

Binary file diff
Documentation
use crate::input::Settings;
use crate::{Line, Side, Batch};

fn side_char(side: &Side) -> char {
    match side {
        Side::Both => ' ',
        Side::Left => '-',
        Side::Right => '+'
    }
}

fn print_byte(b: &&u8) -> char {
    if 32 <= **b && **b < 127 {
        match char::from_u32((**b).into()) {
            None => '.',
            Some(c) => c
        }
    } else {
        '.'
    }
}

#[test]
fn print_byte_returns_char_for_printable() {
    assert_eq!(print_byte(&&69), 'E');
    assert_eq!(print_byte(&&32), ' ');
    assert_eq!(print_byte(&&126), '~');
}

#[test]
fn print_byte_returns_dot_for_not_printable() {
    assert_eq!(print_byte(&&10), '.');
}



fn line_header(side: &Side, left_offs: usize, right_offs: usize) -> String {
    match side {
        Side::Both => format!("{}{:#08x}, {:#08x}:", side_char(side), left_offs, right_offs),
        Side::Left => format!("{}{:#08x},         :", side_char(side), left_offs),
        Side::Right => format!("{}        , {:#08x}:", side_char(side), right_offs),
    }
}

fn pad_to_width(bytes: usize, left: bool) -> String {
    " ".repeat(bytes * 2 + ((bytes + if left { 2 } else { 1 }) / 2))
}

fn cluster_hex(bytes: &Vec<&u8>, offs: usize) -> String {
    bytes.iter().enumerate().map(|(i, b)| if i % 2 == offs % 2 { format!("{:02x}", b) } else { format!("{:02x} ", b) }).collect::<Vec<String>>().join("")
}

fn print_single_line(side: &Side, start_offs: usize, width: usize, bytes: Vec<&u8>, left_offs: isize, right_offs: isize) -> String {
    if start_offs + bytes.len() > width {
        format!(
            "{}\n{}",
            print_single_line(side, start_offs, width, bytes[..width-start_offs].to_vec(), left_offs, right_offs),
            print_single_line(side, 0, width, bytes[width-start_offs..].to_vec(), left_offs+width as isize, right_offs+width as isize),
        )
    } else {
        format!("{}{}{}{}| {}{}", line_header(side, (left_offs+start_offs as isize) as usize, (right_offs+start_offs as isize) as usize), pad_to_width(start_offs, true), cluster_hex(&bytes, start_offs), pad_to_width(width-bytes.len()-start_offs, width % 2 == 1), " ".repeat(start_offs), bytes.iter().map(print_byte).collect::<String>())
    }
}

fn print_line(line: &Line, width: usize, align: fn(usize, usize) -> usize) -> String {
    if line.content.is_empty() { "".into() } else {
        let mut left_end = None;
        let mut right_end = None;
        let mut total_range = None;
        let mut li = 0;
        let mut ri = 0;
        for (i, r) in line.content.iter().enumerate() {
            match r.1 {
                Side::Left => {
                    li += 1;
                    left_end = Some(li);
                    if total_range == None { total_range = Some((i, i, 0)); }
                    total_range = total_range.map(|mut r| { r.1 = i+1; r.2 = align(li, ri); r });
                },
                Side::Right => {
                    ri += 1;
                    right_end = Some(ri);
                    if total_range == None { total_range = Some((i, i, 0)); }
                    total_range = total_range.map(|mut r| { r.1 = i+1; r.2 = align(li, ri); r });
                },
                Side::Both => {
                    li += 1;
                    ri += 1;
                }
            }
        }
        let mut lines = vec![];
        if total_range.is_some() {
            let tr = total_range.unwrap();
            let hd: Vec<&u8> = line.content[..tr.0].iter().map(|e| e.0).collect();
            let tl: Vec<&u8> = line.content[tr.1..].iter().map(|e| e.0).collect();
            if !hd.is_empty() {
                lines.push(print_single_line(&Side::Both, 0, width, hd, line.left_offs as isize, line.right_offs as isize));
            }
            if left_end.is_some() {
                let lf = line.content[tr.0..tr.1].iter().filter(|r| r.1 != Side::Right).map(|r| r.0).collect();
                lines.push(print_single_line(&Side::Left, tr.0, width, lf, line.left_offs as isize, line.right_offs as isize));
            }
            let mut consumed_both = 0;
            if left_end.is_none() != right_end.is_none() {
                let both: Vec<(usize, &(&u8, Side))> = line.content[tr.0..tr.1].iter().enumerate().filter(|r| r.1.1 == Side::Both).collect();
                if !both.is_empty() {
                    consumed_both = both.len();
                    let offs: isize = (both[0].0 + tr.0) as isize;
                    lines.push(print_single_line(if left_end.is_some() { &Side::Right } else { &Side::Left }, if left_end.is_some() { tr.0 + (align)(offs as usize, 0) } else { tr.0 + (align)(0, offs as usize) }, width, both.iter().map(|r| r.1.0).collect(), line.left_offs as isize, line.right_offs as isize));
                }
            }
            if right_end.is_some() {
                let rt = line.content[tr.0..tr.1].iter().filter(|r| r.1 != Side::Left).map(|r| r.0).collect();
                lines.push(print_single_line(&Side::Right, tr.0, width, rt, line.left_offs as isize, line.right_offs as isize));
            }
            if !tl.is_empty() {
                let le = left_end.unwrap_or(tr.0 + consumed_both);
                let re = right_end.unwrap_or(tr.0 + consumed_both);
                lines.push(print_single_line(&Side::Both, tr.2, width, tl, line.left_offs as isize + le as isize - tr.2 as isize, line.right_offs as isize + re as isize - tr.2 as isize));
            }
        } else {
            // no diff
            let bytes: Vec<&u8> = line.content.iter().map(|r| r.0).collect();
            lines.push(print_single_line(&Side::Both, 0, width, bytes, line.left_offs as isize, line.right_offs as isize));
        }
        lines.join("\n")
    }
}

fn print_lines(lines: &Vec<Line>, settings: &Settings, left_diff: usize, right_diff: usize) -> String {
    let mut out = format!("\n@@ -{:#x},{} +{:#x},{} @@ {:#08x}, {:#08x}", lines[0].left_offs, left_diff, lines[0].right_offs, right_diff, lines[0].left_offs, lines[0].right_offs);
    for line in lines {
        out.push('\n');
        out.push_str(&print_line(&line, settings.width, settings.align));
    }
    out
}

/// Prints a batch as a diff.
///
/// Converts a batch to a diff string, adhering to the given width and align (in `settings`).
///
/// # Arguments
///
/// * `batch`: The batch to print.
/// * `settings`: Settings object containing a width and align rule.
///
/// # Returns
///
/// A diff string representing the given batch.
pub fn print_batch(batch: Batch, settings: &Settings) -> String {
    print_lines(&batch.lines, settings, batch.left_diff, batch.right_diff)
}