From 2e2643782d6ced9b5ac75596169a79127d8e535a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=93=81=E5=91=88?= Date: Mon, 20 Oct 2025 03:38:21 +0800 Subject: [PATCH] Add option to output result in C include file style (#242) (#246) * feat: Implement "-i/-include" option * make clippy happy Suppress clippy warning for too many arguments in Printer struct * add tests & use buffered reading for include mode to handle large files * Update CHANGELOG --- CHANGELOG.md | 1 + src/lib.rs | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 42 +++++++++++++- 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b75c6..791eb72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Add option to output result in C include file style, see #242 (@wpcwzy) - Add `--color-scheme` option, see #247 (@aticu) - Add `braille` character table, see #247 (@aticu) - Add command line argument to generate shell completion, see #155 (@friedz) diff --git a/src/lib.rs b/src/lib.rs index dd1e343..be19669 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,13 @@ pub enum ByteCategory { NonAscii, } +pub enum IncludeMode { + File(String), // filename + Stdin, + Slice, + Off, +} + #[derive(Copy, Clone, Debug, Default, ValueEnum)] #[non_exhaustive] pub enum CharacterTable { @@ -267,6 +274,7 @@ pub struct PrinterBuilder<'a, Writer: Write> { base: Base, endianness: Endianness, character_table: CharacterTable, + include_mode: IncludeMode, color_scheme: ColorScheme, } @@ -284,6 +292,7 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { base: Base::Hexadecimal, endianness: Endianness::Big, character_table: CharacterTable::Default, + include_mode: IncludeMode::Off, color_scheme: ColorScheme::Default, } } @@ -338,6 +347,11 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { self } + pub fn include_mode(mut self, include: IncludeMode) -> Self { + self.include_mode = include; + self + } + pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self { self.color_scheme = color_scheme; self @@ -382,6 +396,7 @@ impl<'a, Writer: Write> PrinterBuilder<'a, Writer> { Base::Hexadecimal => 2, }, endianness: self.endianness, + include_mode: self.include_mode, } } } @@ -412,6 +427,8 @@ pub struct Printer<'a, Writer: Write> { base_digits: u8, /// Whether to show groups in little or big endian format. endianness: Endianness, + /// Whether to output in C include file style. + include_mode: IncludeMode, } impl<'a, Writer: Write> Printer<'a, Writer> { @@ -663,6 +680,36 @@ impl<'a, Writer: Write> Printer<'a, Writer> { let mut buf = BufReader::new(reader); + // special handler for include mode + match &self.include_mode { + // Input from a file + // Output like `unsigned char [] = { ... }; unsigned int _len = ...;` + IncludeMode::File(filename) => { + // convert non-alphanumeric characters to '_' + let var_name = filename + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect::(); + + writeln!(self.writer, "unsigned char {}[] = {{", var_name)?; + + let total_bytes = self.print_bytes_in_include_style(&mut buf)?; + + writeln!(self.writer, "}};")?; + writeln!( + self.writer, + "unsigned int {}_len = {};", + var_name, total_bytes + )?; + return Ok(()); + } + IncludeMode::Stdin | IncludeMode::Slice => { + self.print_bytes_in_include_style(&mut buf)?; + return Ok(()); + } + IncludeMode::Off => {} + } + let leftover = loop { // read a maximum of 8 * self.panels bytes from the reader if let Ok(n) = buf.read(&mut self.line_buf) { @@ -803,6 +850,44 @@ impl<'a, Writer: Write> Printer<'a, Writer> { Ok(()) } + + /// Print the bytes in C include file style + /// Return the number of bytes read + fn print_bytes_in_include_style( + &mut self, + buf: &mut BufReader, + ) -> Result { + let mut buffer = [0; 1024]; + let mut total_bytes = 0; + let mut is_first_chunk = true; + let mut line_counter = 0; + loop { + match buf.read(&mut buffer) { + Ok(0) => break, // EOF + Ok(bytes_read) => { + total_bytes += bytes_read; + + for &byte in &buffer[..bytes_read] { + if line_counter % 12 == 0 { + if !is_first_chunk || line_counter > 0 { + writeln!(self.writer, ",")?; + } + // indentation of first line + write!(self.writer, " ")?; + is_first_chunk = false; + } else { + write!(self.writer, ", ")?; + } + write!(self.writer, "0x{:02x}", byte)?; + line_counter += 1; + } + } + Err(e) => return Err(e), + } + } + writeln!(self.writer)?; + Ok(total_bytes) + } } #[cfg(test)] @@ -825,6 +910,7 @@ mod tests { .with_base(Base::Hexadecimal) .endianness(Endianness::Big) .character_table(CharacterTable::Default) + .include_mode(IncludeMode::Off) .color_scheme(ColorScheme::Default) .build(); @@ -881,6 +967,7 @@ mod tests { .with_base(Base::Hexadecimal) .endianness(Endianness::Big) .character_table(CharacterTable::Default) + .include_mode(IncludeMode::Off) .color_scheme(ColorScheme::Default) .build(); printer.display_offset(0xdeadbeef); @@ -916,6 +1003,7 @@ mod tests { .with_base(Base::Hexadecimal) .endianness(Endianness::Big) .character_table(CharacterTable::Default) + .include_mode(IncludeMode::Off) .color_scheme(ColorScheme::Default) .build(); @@ -977,6 +1065,7 @@ mod tests { .with_base(Base::Hexadecimal) .endianness(Endianness::Big) .character_table(CharacterTable::Default) + .include_mode(IncludeMode::Off) .color_scheme(ColorScheme::Default) .build(); @@ -999,4 +1088,66 @@ mod tests { .to_owned(); assert_print_all_output(input, expected_string); } + + #[test] + fn include_mode_from_file() { + let input = io::Cursor::new(b"spamspamspamspamspam"); + let expected_string = "unsigned char test_txt[] = { + 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d, + 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d +}; +unsigned int test_txt_len = 20; +" + .to_owned(); + let mut output = vec![]; + let mut printer: Printer> = PrinterBuilder::new(&mut output) + .show_color(false) + .show_char_panel(true) + .show_position_panel(true) + .with_border_style(BorderStyle::Unicode) + .enable_squeezing(true) + .num_panels(2) + .group_size(1) + .with_base(Base::Hexadecimal) + .endianness(Endianness::Big) + .character_table(CharacterTable::Default) + .include_mode(IncludeMode::File("test.txt".to_owned())) + .color_scheme(ColorScheme::Default) + .build(); + + printer.print_all(input).unwrap(); + + let actual_string: &str = str::from_utf8(&output).unwrap(); + assert_eq!(actual_string, expected_string) + } + + #[test] + fn include_mode_from_stdin() { + let input = io::Cursor::new(b"spamspamspamspamspam"); + let expected_string = + " 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d, + 0x73, 0x70, 0x61, 0x6d, 0x73, 0x70, 0x61, 0x6d +" + .to_owned(); + let mut output = vec![]; + let mut printer: Printer> = PrinterBuilder::new(&mut output) + .show_color(false) + .show_char_panel(true) + .show_position_panel(true) + .with_border_style(BorderStyle::Unicode) + .enable_squeezing(true) + .num_panels(2) + .group_size(1) + .with_base(Base::Hexadecimal) + .endianness(Endianness::Big) + .character_table(CharacterTable::Default) + .include_mode(IncludeMode::Stdin) + .color_scheme(ColorScheme::Default) + .build(); + + printer.print_all(input).unwrap(); + + let actual_string: &str = str::from_utf8(&output).unwrap(); + assert_eq!(actual_string, expected_string) + } } diff --git a/src/main.rs b/src/main.rs index 8ae912c..1208fb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,9 @@ use thiserror::Error as ThisError; use terminal_size::terminal_size; -use hexyl::{Base, BorderStyle, CharacterTable, ColorScheme, Endianness, Input, PrinterBuilder}; +use hexyl::{ + Base, BorderStyle, CharacterTable, ColorScheme, Endianness, IncludeMode, Input, PrinterBuilder, +}; use hexyl::{ COLOR_ASCII_OTHER, COLOR_ASCII_PRINTABLE, COLOR_ASCII_WHITESPACE, COLOR_NONASCII, COLOR_NULL, @@ -194,6 +196,16 @@ struct Opt { #[arg(long)] print_color_table: bool, + /// Output in C include file style (similar to xxd -i). + #[arg( + short('i'), + long("include"), + help = "Output in C include file style", + conflicts_with("little_endian_format"), + conflicts_with("endianness") + )] + include_mode: bool, + /// Show shell completion for a certain shell #[arg(long, value_name("SHELL"))] completion: Option, @@ -262,12 +274,12 @@ fn run() -> Result<()> { let stdin = io::stdin(); - let mut reader = match opt.file { + let mut reader = match &opt.file { Some(filename) => { if filename.is_dir() { bail!("'{}' is a directory.", filename.to_string_lossy()); } - let file = File::open(&filename)?; + let file = File::open(filename)?; Input::File(file) } @@ -449,6 +461,29 @@ fn run() -> Result<()> { let mut stdout = BufWriter::new(io::stdout().lock()); + let include_mode = match opt.include_mode { + // include mode on + true => { + if opt.file.is_some() { + // input from a file + IncludeMode::File( + opt.file + .as_ref() + .unwrap() + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("file") + .to_string(), + ) + } else { + // input from stdin + IncludeMode::Stdin + } + } + // include mode off + false => IncludeMode::Off, + }; + let mut printer = PrinterBuilder::new(&mut stdout) .show_color(show_color) .show_char_panel(show_char_panel) @@ -460,6 +495,7 @@ fn run() -> Result<()> { .with_base(base) .endianness(endianness) .character_table(character_table) + .include_mode(include_mode) .color_scheme(color_scheme) .build(); printer.display_offset(skip_offset + display_offset);