From 6978f7a2778a6f952591e82a6b578b7645a71420 Mon Sep 17 00:00:00 2001 From: Armin Widegreen Date: Tue, 26 Mar 2019 23:24:55 +0100 Subject: [PATCH] Adds squeeze support The squeeze feature is implement as for hexdump. Reoccurring lines will be squeezed together and visualized with an asterisk. Further hexyl received an additional command line argument '-v|--no-squeezing' to disable squeezing. Squeezing is enabled by default. If the squeezing goes until the end of the input data, an empty line with address next to last line will be printed to indicate the highest address (again similar to hexdump). --- src/main.rs | 105 +++++++++++++++---- src/squeezer.rs | 260 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 21 deletions(-) create mode 100644 src/squeezer.rs diff --git a/src/main.rs b/src/main.rs index 34fe918..4437633 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ extern crate ansi_term; extern crate atty; extern crate ctrlc; +mod squeezer; + use std::fs::File; use std::io::{self, prelude::*, StdoutLock}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -15,6 +17,7 @@ use ansi_term::Color; use ansi_term::Color::Fixed; use atty::Stream; +use squeezer::{SqueezeAction, Squeezer}; const BUFFER_SIZE: usize = 256; @@ -159,10 +162,16 @@ struct Printer<'a> { header_was_printed: bool, byte_hex_table: Vec, byte_char_table: Vec, + squeezer: Squeezer, } impl<'a> Printer<'a> { - fn new(stdout: StdoutLock, show_color: bool, border_style: BorderStyle) -> Printer { + fn new( + stdout: StdoutLock, + show_color: bool, + border_style: BorderStyle, + use_squeeze: bool, + ) -> Printer { Printer { idx: 1, raw_line: vec![], @@ -191,6 +200,7 @@ impl<'a> Printer<'a> { } }) .collect(), + squeezer: Squeezer::new(use_squeeze), } } @@ -232,32 +242,38 @@ impl<'a> Printer<'a> { } } + fn print_position_indicator(&mut self) { + if !self.header_was_printed { + self.header(); + self.header_was_printed = true; + } + + let style = COLOR_OFFSET.normal(); + let byte_index = format!("{:08x}", self.idx - 1); + let formatted_string = if self.show_color { + format!("{}", style.paint(byte_index)) + } else { + byte_index + }; + let _ = write!( + &mut self.buffer_line, + "{}{}{} ", + self.border_style.outer_sep(), + formatted_string, + self.border_style.outer_sep() + ); + } + fn print_byte(&mut self, b: u8) -> io::Result<()> { if self.idx % 16 == 1 { - if !self.header_was_printed { - self.header(); - self.header_was_printed = true; - } - - let style = COLOR_OFFSET.normal(); - let byte_index = format!("{:08x}", self.idx - 1); - let formatted_string = if self.show_color { - format!("{}", style.paint(byte_index)) - } else { - byte_index - }; - let _ = write!( - &mut self.buffer_line, - "{}{}{} ", - self.border_style.outer_sep(), - formatted_string, - self.border_style.outer_sep() - ); + self.print_position_indicator(); } write!(&mut self.buffer_line, "{}", self.byte_hex_table[b as usize])?; self.raw_line.push(b); + self.squeezer.process(&b, &self.idx); + match self.idx % 16 { 8 => { let _ = write!(&mut self.buffer_line, "{} ", self.border_style.inner_sep()); @@ -277,6 +293,20 @@ impl<'a> Printer<'a> { let len = self.raw_line.len(); if len == 0 { + if self.squeezer.active() { + self.print_position_indicator(); + let _ = writeln!( + &mut self.buffer_line, + "{0:1$}{4}{0:2$}{5}{0:3$}{4}{0:3$}{5}", + "", + 24, + 25, + 8, + self.border_style.inner_sep(), + self.border_style.outer_sep(), + ); + self.stdout.write_all(&self.buffer_line)?; + } return Ok(()); } @@ -334,6 +364,27 @@ impl<'a> Printer<'a> { self.border_style.outer_sep() ); } + + match self.squeezer.action() { + SqueezeAction::Print => { + self.buffer_line.clear(); + let style = COLOR_OFFSET.normal(); + let _ = writeln!( + &mut self.buffer_line, + "{5}{0}{1:2$}{5}{1:3$}{6}{1:3$}{5}{1:4$}{6}{1:4$}{5}", + style.paint("*"), + "", + 7, + 25, + 8, + self.border_style.outer_sep(), + self.border_style.inner_sep(), + ); + } + SqueezeAction::Delete => self.buffer_line.clear(), + SqueezeAction::Ignore => (), + } + self.stdout.write_all(&self.buffer_line)?; self.raw_line.clear(); @@ -368,6 +419,16 @@ fn run() -> Result<(), Box<::std::error::Error>> { .value_name("N") .help("An alias for -n/--length"), ) + .arg( + Arg::with_name("nosqueezing") + .short("v") + .long("no-squeezing") + .help( + "Displays all input data. Otherwise any number of groups of output \ + lines which would be identical to the preceding group of lines, are \ + replaced with a line comprised of a single asterisk.", + ), + ) .arg( Arg::with_name("color") .long("color") @@ -422,6 +483,8 @@ fn run() -> Result<(), Box<::std::error::Error>> { _ => BorderStyle::None, }; + let squeeze = !matches.is_present("nosqueezing"); + // Set up Ctrl-C handler let cancelled = Arc::new(AtomicBool::new(false)); let c = cancelled.clone(); @@ -432,7 +495,7 @@ fn run() -> Result<(), Box<::std::error::Error>> { .expect("Error setting Ctrl-C handler"); let stdout = io::stdout(); - let mut printer = Printer::new(stdout.lock(), show_color, border_style); + let mut printer = Printer::new(stdout.lock(), show_color, border_style, squeeze); let mut buffer = [0; BUFFER_SIZE]; 'mainloop: loop { diff --git a/src/squeezer.rs b/src/squeezer.rs new file mode 100644 index 0000000..a40ec47 --- /dev/null +++ b/src/squeezer.rs @@ -0,0 +1,260 @@ +#[derive(Debug, PartialEq)] +enum SqueezeState { + /// not enabled + Disabled, + /// Will be set from all states if equal condition can't be hold up. + /// Set if previous byte is not equal the current processed byte. + NoSqueeze, + /// Valid for a whole line to identify if it is candidate for squeezing + Probe, + /// Squeeze line parsing is active, but EOL is not reached yet + SqueezeActive, + /// Squeeze line, EOL is reached, will influence the action + Squeeze, + /// same as Squeeze, however this is only for the first line after + /// the squeeze candidate has been set. + SqueezeFirstLine, + /// same as SqueezeActive, however this is only for the first line after + /// the squeeze candidate has been set. + SqueezeActiveFirstLine, +} + +pub struct Squeezer { + state: SqueezeState, + byte: u8, +} + +#[derive(Debug, PartialEq)] +pub enum SqueezeAction { + Ignore, + Print, + Delete, +} + +/// line size +const LSIZE: usize = 16; + +impl Squeezer { + pub fn new(enabled: bool) -> Squeezer { + Squeezer { + state: if enabled { + SqueezeState::Probe + } else { + SqueezeState::Disabled + }, + byte: 0, + } + } + + pub fn process(&mut self, b: &u8, i: &usize) { + use self::SqueezeState::*; + if self.state == Disabled { + return; + } + let eq = *b == self.byte; + + if i % LSIZE == 0 { + self.state = match self.state { + NoSqueeze => Probe, + Probe => SqueezeActiveFirstLine, + SqueezeActiveFirstLine => SqueezeFirstLine, + SqueezeFirstLine => SqueezeActive, + SqueezeActive => Squeeze, + Squeeze => SqueezeActive, + Disabled => Disabled, + }; + } else if !eq { + if (i % LSIZE == 1 && self.state != Probe) || i % LSIZE != 1 { + self.state = NoSqueeze; + } + } + + self.byte = b.clone(); + } + + pub fn active(&self) -> bool { + use self::SqueezeState::*; + match self.state { + Squeeze | SqueezeActive | SqueezeFirstLine | SqueezeActiveFirstLine => true, + _ => false, + } + } + + pub fn action(&mut self) -> SqueezeAction { + match self.state { + SqueezeState::SqueezeFirstLine => { + self.state = SqueezeState::SqueezeActive; + SqueezeAction::Print + } + SqueezeState::Squeeze => { + self.state = SqueezeState::SqueezeActive; + SqueezeAction::Delete + } + _ => SqueezeAction::Ignore, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn three_same_lines() { + const LINES: usize = 3; + let v = vec![0u8; LINES * LSIZE]; + let mut s = Squeezer::new(true); + // just initialized + assert_eq!(s.action(), SqueezeAction::Ignore); + + let exp = vec![ + SqueezeAction::Ignore, // first line, print as is + SqueezeAction::Print, // print squeeze symbol + SqueezeAction::Delete, // delete reoccurring line + ]; + + for z in 0..LINES { + for i in z * LSIZE..(z + 1) * LSIZE { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[z]); + } + } + + #[test] + fn incomplete_while_squeeze() { + const LINES: usize = 3; + // third line only has 12 bytes and should be printed + let v = vec![0u8; LINES * LSIZE + 12]; + let mut s = Squeezer::new(true); + // just initialized + assert_eq!(s.action(), SqueezeAction::Ignore); + + let exp = vec![ + SqueezeAction::Ignore, // first line, print as is + SqueezeAction::Print, // print squeeze symbol + SqueezeAction::Delete, // delete reoccurring line + SqueezeAction::Ignore, // last line only 12 bytes, print it + ]; + + for z in 0..LINES { + for i in z * LSIZE..(z + 1) * LSIZE { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[z]); + } + + for i in LSIZE * LINES..LINES * LSIZE + 12 { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[3]); + } + + #[test] + /// all three lines are different, print all + fn three_different_lines() { + const LINES: usize = 3; + let mut v: Vec = vec![]; + v.extend(vec![0u8; 16]); + v.extend(vec![1u8; 16]); + v.extend(vec![2u8; 16]); + + let mut s = Squeezer::new(true); + // just initialized + assert_eq!(s.action(), SqueezeAction::Ignore); + + let exp = vec![ + SqueezeAction::Ignore, // first line, print as is + SqueezeAction::Ignore, // different + SqueezeAction::Ignore, // different + ]; + + for z in 0..LINES { + for i in z * LSIZE..(z + 1) * LSIZE { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[z]); + } + } + + #[test] + /// first two lines same, hence squeeze symbol, third line diff, hence + /// print + fn one_squeeze_no_delete() { + const LINES: usize = 3; + let mut v = vec![0u8; (LINES - 1) * LSIZE]; + v.extend(vec![1u8; 16]); + + let mut s = Squeezer::new(true); + // just initialized + assert_eq!(s.action(), SqueezeAction::Ignore); + + let exp = vec![ + SqueezeAction::Ignore, // first line, print as is + SqueezeAction::Print, // print squeeze symbol + SqueezeAction::Ignore, // different lines, print again + ]; + + for z in 0..LINES { + for i in z * LSIZE..(z + 1) * LSIZE { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[z]); + } + } + + #[test] + /// First line all eq, 2nd half eq with first line, then change + fn second_line_different() { + const LINES: usize = 2; + let mut v = vec![0u8; (LINES - 1) * LSIZE]; + v.extend(vec![0u8; 8]); + v.extend(vec![1u8; 8]); + + let mut s = Squeezer::new(true); + // just initialized + assert_eq!(s.action(), SqueezeAction::Ignore); + + let exp = vec![ + SqueezeAction::Ignore, // first line, print as is + SqueezeAction::Ignore, // print squeeze symbol + ]; + + for z in 0..LINES { + for i in z * LSIZE..(z + 1) * LSIZE { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[z]); + } + } + + #[test] + /// all three lines never become squeeze candidate (diff within line) + fn never_squeeze_candidate() { + const LINES: usize = 3; + let mut v = vec![]; + v.extend(vec![0u8; 8]); + v.extend(vec![1u8; 8]); + v.extend(vec![0u8; 8]); + v.extend(vec![1u8; 8]); + v.extend(vec![0u8; 8]); + v.extend(vec![1u8; 8]); + + let mut s = Squeezer::new(true); + // just initialized + assert_eq!(s.action(), SqueezeAction::Ignore); + + let exp = vec![ + SqueezeAction::Ignore, // first line, print as is + SqueezeAction::Ignore, // print squeeze symbol + SqueezeAction::Ignore, // print squeeze symbol + ]; + + for z in 0..LINES { + for i in z * LSIZE..(z + 1) * LSIZE { + s.process(&v[i], &(i + 1)); + } + assert_eq!(s.action(), exp[z]); + } + } +}