diff --git a/src/bin/hexyl.rs b/src/bin/hexyl.rs index 4b4cf5b..58e53ac 100644 --- a/src/bin/hexyl.rs +++ b/src/bin/hexyl.rs @@ -19,7 +19,7 @@ use thiserror::Error as ThisError; use terminal_size::terminal_size; -use hexyl::{BorderStyle, Input, PrinterBuilder}; +use hexyl::{Base, BorderStyle, Input, PrinterBuilder}; const DEFAULT_BLOCK_SIZE: i64 = 512; @@ -177,6 +177,18 @@ fn run() -> Result<(), AnyhowError> { The possible options are 1, 2, 4, 8. The default is 1", ), ) + .arg( + Arg::new("base") + .short('b') + .long("base") + .num_args(1) + .value_name("B") + .help( + "Sets the base used for the bytes. The possible options are \ + binary, octal, decimal, and hexadecimal. The default base \ + is hexadecimal." + ) + ) .arg( Arg::new("terminal_width") .long("terminal-width") @@ -310,9 +322,13 @@ fn run() -> Result<(), AnyhowError> { .transpose()? .unwrap_or(0); - let max_panels_fn = |terminal_width: u64| { + let max_panels_fn = |terminal_width: u64, base_digits: u64, group_bytes: u64| { let offset = if show_position_panel { 10 } else { 1 }; - let col_width = if show_char_panel { 35 } else { 26 }; + let col_width = if show_char_panel { + ((8 / group_bytes) * (base_digits * group_bytes + 1)) + 2 + 8 + } else { + ((8 / group_bytes) * (base_digits * group_bytes + 1)) + 2 + }; if (terminal_width - offset) / col_width < 1 { 1 } else { @@ -320,32 +336,36 @@ fn run() -> Result<(), AnyhowError> { } }; - let panels = if matches.get_one::("panels").map(String::as_ref) == Some("auto") { - max_panels_fn(terminal_size().ok_or_else(|| anyhow!("not a TTY"))?.0 .0 as u64) - } else if let Some(panels) = matches - .get_one::("panels") - .map(|s| { - s.parse::().map(u64::from).context(anyhow!( - "failed to parse `--panels` arg {:?} as unsigned nonzero integer", - s - )) - }) - .transpose()? - { - panels - } else if let Some(terminal_width) = matches - .get_one::("terminal_width") - .map(|s| { - s.parse::().map(u64::from).context(anyhow!( - "failed to parse `--terminal-width` arg {:?} as unsigned nonzero integer", - s - )) - }) - .transpose()? - { - max_panels_fn(terminal_width) + let base = if let Some(base) = matches.get_one::("base") + .map(|s| { + if let Ok(base_num) = s.parse::() { + match base_num { + 2 => Ok(Base::Binary), + 8 => Ok(Base::Octal), + 10 => Ok(Base::Decimal), + 16 => Ok(Base::Hexadecimal), + _ => Err(anyhow!("The number provided is not a valid base. Valid bases are 2, 8, 10, and 16.")), + } + } else { + match s.as_str() { + "b" | "bin" | "binary" => Ok(Base::Binary), + "o" | "oct" | "octal" => Ok(Base::Octal), + "d" | "dec" | "decimal" => Ok(Base::Decimal), + "x" | "hex" | "hexadecimal" => Ok(Base::Hexadecimal), + _ => Err(anyhow!("The base provided is not valid. Valid bases are \"b\", \"o\", \"d\", and \"x\".")) + } + } + }).transpose()? { + base } else { - 2 + Base::Hexadecimal + }; + + let base_digits = match base { + Base::Binary => 8, + Base::Octal => 3, + Base::Decimal => 3, + Base::Hexadecimal => 2, }; let group_bytes = if let Some(group_bytes) = matches @@ -369,6 +389,38 @@ fn run() -> Result<(), AnyhowError> { 1 }; + let panels = if matches.get_one::("panels").map(String::as_ref) == Some("auto") { + max_panels_fn( + terminal_size().ok_or_else(|| anyhow!("not a TTY"))?.0 .0 as u64, + base_digits, + group_bytes.into(), + ) + } else if let Some(panels) = matches + .get_one::("panels") + .map(|s| { + s.parse::().map(u64::from).context(anyhow!( + "failed to parse `--panels` arg {:?} as unsigned nonzero integer", + s + )) + }) + .transpose()? + { + panels + } else if let Some(terminal_width) = matches + .get_one::("terminal_width") + .map(|s| { + s.parse::().map(u64::from).context(anyhow!( + "failed to parse `--terminal-width` arg {:?} as unsigned nonzero integer", + s + )) + }) + .transpose()? + { + max_panels_fn(terminal_width, base_digits, group_bytes.into()) + } else { + 2 + }; + let stdout = io::stdout(); let mut stdout_lock = BufWriter::new(stdout.lock()); @@ -380,6 +432,7 @@ fn run() -> Result<(), AnyhowError> { .enable_squeezing(squeeze) .num_panels(panels) .num_group_bytes(group_bytes) + .with_base(base) .build(); printer.display_offset(skip_offset + display_offset); printer.print_all(&mut reader).map_err(|e| anyhow!(e))?; diff --git a/src/lib.rs b/src/lib.rs index ab7b80c..42878d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,13 @@ use std::io::{self, BufReader, Read, Write}; use anstyle::{AnsiColor, Reset}; +pub enum Base { + Binary, + Octal, + Decimal, + Hexadecimal, +} + #[derive(PartialEq, Eq)] #[repr(usize)] enum ByteColor { @@ -162,6 +169,7 @@ pub struct PrinterBuilder<'a, Writer: Write> { use_squeeze: bool, panels: u64, group_bytes: u8, + base: Base, } impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { @@ -175,6 +183,7 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { use_squeeze: true, panels: 2, group_bytes: 1, + base: Base::Hexadecimal, } } @@ -213,6 +222,11 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { self } + pub fn with_base(mut self, base: Base) -> Self { + self.base = base; + self + } + pub fn build(self) -> Printer<'a, Writer> { Printer::new( self.writer, @@ -223,6 +237,7 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { self.use_squeeze, self.panels, self.group_bytes, + self.base, ) } } @@ -250,6 +265,8 @@ pub struct Printer<'a, Writer: Write> { squeeze_byte: usize, /// The number of octets per group. group_bytes: u8, + /// The number of digits used to write the base. + base_digits: u8, } impl<'a, Writer: Write> Printer<'a, Writer> { @@ -262,6 +279,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> { use_squeeze: bool, panels: u64, group_bytes: u8, + base: Base, ) -> Printer<'a, Writer> { Printer { idx: 0, @@ -281,11 +299,18 @@ impl<'a, Writer: Write> Printer<'a, Writer> { Reset.render().to_string(), ], border_style, - byte_hex_panel: (0u8..=u8::MAX).map(|i| format!("{:02x}", i)).collect(), + byte_hex_panel: (0u8..=u8::MAX) + .map(|i| match base { + Base::Binary => format!("{i:08b}"), + Base::Octal => format!("{i:03o}"), + Base::Decimal => format!("{i:03}"), + Base::Hexadecimal => format!("{i:02x}"), + }) + .collect(), byte_char_panel: (0u8..=u8::MAX) .map(|i| format!("{}", Byte(i).as_char())) .collect(), - byte_hex_panel_g: (0u8..=u8::MAX).map(|i| format!("{:02x}", i)).collect(), + byte_hex_panel_g: (0u8..=u8::MAX).map(|i| format!("{i:02x}")).collect(), byte_char_panel_g: (0u8..=u8::MAX) .map(|i| format!("{}", Byte(i).as_char())) .collect(), @@ -298,6 +323,12 @@ impl<'a, Writer: Write> Printer<'a, Writer> { panels, squeeze_byte: 0x00, group_bytes, + base_digits: match base { + Base::Binary => 8, + Base::Octal => 3, + Base::Decimal => 3, + Base::Hexadecimal => 2, + }, } } @@ -308,7 +339,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> { fn panel_sz(&self) -> usize { // add one to include the trailing space of a group - let group_sz = 2 * self.group_bytes as usize + 1; + let group_sz = self.base_digits as usize * self.group_bytes as usize + 1; let group_per_panel = 8 / self.group_bytes as usize; // add one to include the leading space 1 + group_sz * group_per_panel @@ -483,7 +514,9 @@ impl<'a, Writer: Write> Printer<'a, Writer> { self.writer.write_all(b" ")?; } } - self.writer.write_all(b" ")?; + for _ in 0..self.base_digits { + self.writer.write_all(b" ")?; + } } Squeezer::Delete => self.writer.write_all(b" ")?, Squeezer::Ignore | Squeezer::Disabled => { @@ -541,13 +574,15 @@ impl<'a, Writer: Write> Printer<'a, Writer> { let mut buf = BufReader::new(reader); - self.print_header()?; let leftover = loop { // read a maximum of 8 * self.panels bytes from the reader if let Ok(n) = buf.read(&mut self.line_buf) { if n > 0 && n < 8 * self.panels as usize { // if less are read, that indicates end of file after - is_empty = false; + if is_empty { + self.print_header()?; + is_empty = false; + } // perform second check on read if let Ok(0) = buf.read(&mut self.line_buf[n..]) { @@ -564,7 +599,10 @@ impl<'a, Writer: Write> Printer<'a, Writer> { break None; } } - is_empty = false; + if is_empty { + self.print_header()?; + is_empty = false; + } // squeeze is active, check if the line is the same // skip print if still squeezed, otherwise print and deactivate squeeze @@ -617,6 +655,8 @@ impl<'a, Writer: Write> Printer<'a, Writer> { // special ending if is_empty { + self.base_digits = 2; + self.print_header()?; if self.show_position_panel { write!(self.writer, "{0:9}", "│")?; } @@ -678,6 +718,7 @@ mod tests { true, 2, 1, + Base::Hexadecimal, ); printer.print_all(input).unwrap(); @@ -731,6 +772,7 @@ mod tests { true, 2, 1, + Base::Hexadecimal, ); printer.display_offset(0xdeadbeef); @@ -763,6 +805,7 @@ mod tests { true, 4, 1, + Base::Hexadecimal, ); printer.print_all(input).unwrap(); @@ -821,6 +864,7 @@ mod tests { true, 3, 1, + Base::Hexadecimal, ); printer.print_all(input).unwrap(); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index e7a5716..7a0fae1 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -551,3 +551,21 @@ mod group { ); } } + +mod base { + use super::hexyl; + use super::PrettyAssert; + + #[test] + fn base2() { + hexyl() + .arg("ascii") + .arg("--plain") + .arg("--base=binary") + .assert() + .success() + .pretty_stdout( + " 00110000 00110001 00110010 00110011 00110100 00110101 00110110 00110111 00111000 00111001 01100001 01100010 01100011 01100100 01100101 00001010 \n" + ); + } +}