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
This commit is contained in:
王品呈 authored and GitHub committed 2025-10-19 21:38:21 +02:00
1 parent 269860ba7e
commit 2e2643782d
3 files changed
+191 -3

No files matched your search

+1
View File
@@ -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)
+151
View File
@@ -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 <filename>[] = { ... }; unsigned int <filename>_len = ...;`
IncludeMode::File(filename) => {
// convert non-alphanumeric characters to '_'
let var_name = filename
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '_' })
.collect::<String>();
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<Reader: Read>(
&mut self,
buf: &mut BufReader<Reader>,
) -> Result<usize, io::Error> {
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<Vec<u8>> = 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<Vec<u8>> = 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)
}
}
+39 -3
View File
@@ -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<Shell>,
@@ -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);