Skip to main content

nu_pretty_hex/
pretty_hex.rs

1use core::fmt;
2use nu_ansi_term::{Color, Style};
3
4/// Returns a one-line hexdump of `source` grouped in default format without header
5/// and ASCII column.
6pub fn simple_hex<T: AsRef<[u8]>>(source: &T) -> String {
7    let mut writer = String::new();
8    hex_write(&mut writer, source, HexConfig::simple(), None).unwrap_or(());
9    writer
10}
11
12/// Dump `source` as hex octets in default format without header and ASCII column to the `writer`.
13pub fn simple_hex_write<T, W>(writer: &mut W, source: &T) -> fmt::Result
14where
15    T: AsRef<[u8]>,
16    W: fmt::Write,
17{
18    hex_write(writer, source, HexConfig::simple(), None)
19}
20
21/// Return a multi-line hexdump in default format complete with addressing, hex digits,
22/// and ASCII representation.
23pub fn pretty_hex<T: AsRef<[u8]>>(source: &T) -> String {
24    let mut writer = String::new();
25    hex_write(&mut writer, source, HexConfig::default(), Some(true)).unwrap_or(());
26    writer
27}
28
29/// Write multi-line hexdump in default format complete with addressing, hex digits,
30/// and ASCII representation to the writer.
31pub fn pretty_hex_write<T, W>(writer: &mut W, source: &T) -> fmt::Result
32where
33    T: AsRef<[u8]>,
34    W: fmt::Write,
35{
36    hex_write(writer, source, HexConfig::default(), Some(true))
37}
38
39/// Return a hexdump of `source` in specified format.
40pub fn config_hex<T: AsRef<[u8]>>(source: &T, cfg: HexConfig) -> String {
41    let mut writer = String::new();
42    hex_write(&mut writer, source, cfg, Some(true)).unwrap_or(());
43    writer
44}
45
46/// Configuration parameters for hexdump.
47#[derive(Clone, Copy, Debug)]
48pub struct HexConfig {
49    /// Write first line header with data length.
50    pub title: bool,
51    /// Append ASCII representation column.
52    pub ascii: bool,
53    /// Source bytes per row. 0 for single row without address prefix.
54    pub width: usize,
55    /// Chunks count per group. 0 for single group (column).
56    pub group: usize,
57    /// Source bytes per chunk (word). 0 for single word.
58    pub chunk: usize,
59    /// Offset to start counting addresses from
60    pub address_offset: usize,
61    /// Bytes from 0 to skip
62    pub skip: Option<usize>,
63    /// Length to return
64    pub length: Option<usize>,
65    /// Colors / styling for different byte categories.
66    pub styles: HexStyles,
67}
68
69/// Default configuration with `title`, `ascii`, 16 source bytes `width` grouped to 4 separate
70/// hex bytes. Using in `pretty_hex`, `pretty_hex_write` and `fmt::Debug` implementation.
71impl Default for HexConfig {
72    fn default() -> HexConfig {
73        HexConfig {
74            title: true,
75            ascii: true,
76            width: 16,
77            group: 4,
78            chunk: 1,
79            address_offset: 0,
80            skip: None,
81            length: None,
82            styles: HexStyles::default(),
83        }
84    }
85}
86
87impl HexConfig {
88    /// Returns configuration for `simple_hex`, `simple_hex_write` and `fmt::Display` implementation.
89    pub fn simple() -> Self {
90        HexConfig::default().to_simple()
91    }
92
93    fn delimiter(&self, i: usize) -> &'static str {
94        if i > 0 && self.chunk > 0 && i.is_multiple_of(self.chunk) {
95            if self.group > 0 && i.is_multiple_of(self.group * self.chunk) {
96                "  "
97            } else {
98                " "
99            }
100        } else {
101            ""
102        }
103    }
104
105    fn to_simple(self) -> Self {
106        HexConfig {
107            title: false,
108            ascii: false,
109            width: 0,
110            ..self
111        }
112    }
113}
114
115pub fn categorize_byte(byte: &u8, styles: &HexStyles) -> (Style, Option<char>) {
116    let null_char = Some('0');
117    let ascii_printable = None;
118    let ascii_space = Some(' ');
119    let ascii_whitespace = Some('_');
120    let ascii_other = Some('•');
121    let non_ascii = Some('×'); // or Some('.')
122
123    if byte == &0 {
124        (styles.null_char, null_char)
125    } else if byte.is_ascii_graphic() {
126        (styles.printable, ascii_printable)
127    } else if byte.is_ascii_whitespace() {
128        // 0x20 == 32 decimal - replace with a real space
129        if byte == &32 {
130            (styles.whitespace, ascii_space)
131        } else {
132            (styles.whitespace, ascii_whitespace)
133        }
134    } else if byte.is_ascii() {
135        (styles.ascii_other, ascii_other)
136    } else {
137        (styles.non_ascii, non_ascii)
138    }
139}
140
141/// Style parameters for hexdump. These styles will be applied both to the hex representation
142/// and the corresponding ASCII character (or placeholder, for non-printable bytes).
143#[derive(Clone, Copy, Debug)]
144pub struct HexStyles {
145    /// Style for null bytes (`\0`).
146    pub null_char: Style,
147    /// Style for non-whitespace printable ASCII characters.
148    pub printable: Style,
149    /// Style for whitespace printable ASCII characters.
150    pub whitespace: Style,
151    /// Style for other ASCII characters (e.g. control characters).
152    pub ascii_other: Style,
153    /// Style for non-ASCII characters (i.e. `0x80..`).
154    pub non_ascii: Style,
155}
156
157impl Default for HexStyles {
158    fn default() -> Self {
159        Self {
160            null_char: Style::default().fg(Color::Fixed(242)),
161            printable: Style::default().fg(Color::Cyan).bold(),
162            whitespace: Style::default().fg(Color::Green).bold(),
163            ascii_other: Style::default().fg(Color::Purple).bold(),
164            non_ascii: Style::default().fg(Color::Yellow).bold(),
165        }
166    }
167}
168
169/// Write hex dump in specified format.
170pub fn hex_write<T, W>(
171    writer: &mut W,
172    source: &T,
173    cfg: HexConfig,
174    with_color: Option<bool>,
175) -> fmt::Result
176where
177    T: AsRef<[u8]>,
178    W: fmt::Write,
179{
180    let use_color = with_color.unwrap_or(false);
181
182    if source.as_ref().is_empty() {
183        return Ok(());
184    }
185
186    let amount = cfg.length.unwrap_or_else(|| source.as_ref().len());
187
188    let skip = cfg.skip.unwrap_or(0);
189
190    let address_offset = cfg.address_offset;
191
192    let source_part_vec: Vec<u8> = source
193        .as_ref()
194        .iter()
195        .skip(skip)
196        .take(amount)
197        .copied()
198        .collect();
199
200    if cfg.title {
201        write_title(
202            writer,
203            HexConfig {
204                length: Some(source_part_vec.len()),
205                ..cfg
206            },
207            use_color,
208        )?;
209    }
210
211    let lines = source_part_vec.chunks(if cfg.width > 0 {
212        cfg.width
213    } else {
214        source_part_vec.len()
215    });
216
217    let lines_len = lines.len();
218
219    for (i, row) in lines.enumerate() {
220        if cfg.width > 0 {
221            let style = Style::default().fg(Color::Cyan);
222            if use_color {
223                write!(
224                    writer,
225                    "{}{:08x}{}:   ",
226                    style.prefix(),
227                    i * cfg.width + skip + address_offset,
228                    style.suffix()
229                )?;
230            } else {
231                write!(writer, "{:08x}:   ", i * cfg.width + skip + address_offset,)?;
232            }
233        }
234        for (i, x) in row.as_ref().iter().enumerate() {
235            if use_color {
236                let (style, _char) = categorize_byte(x, &cfg.styles);
237                write!(
238                    writer,
239                    "{}{}{:02x}{}",
240                    cfg.delimiter(i),
241                    style.prefix(),
242                    x,
243                    style.suffix()
244                )?;
245            } else {
246                write!(writer, "{}{:02x}", cfg.delimiter(i), x,)?;
247            }
248        }
249        if cfg.ascii {
250            for j in row.len()..cfg.width {
251                write!(writer, "{}  ", cfg.delimiter(j))?;
252            }
253            write!(writer, "   ")?;
254            for x in row {
255                let (style, a_char) = categorize_byte(x, &cfg.styles);
256                let replacement_char = a_char.unwrap_or(*x as char);
257                if use_color {
258                    write!(
259                        writer,
260                        "{}{}{}",
261                        style.prefix(),
262                        replacement_char,
263                        style.suffix()
264                    )?;
265                } else {
266                    write!(writer, "{replacement_char}",)?;
267                }
268            }
269        }
270        if i + 1 < lines_len {
271            writeln!(writer)?;
272        }
273    }
274    Ok(())
275}
276
277/// Write the title for the given config. The length will be taken from `cfg.length`.
278pub fn write_title<W>(writer: &mut W, cfg: HexConfig, use_color: bool) -> Result<(), fmt::Error>
279where
280    W: fmt::Write,
281{
282    let write = |writer: &mut W, length: fmt::Arguments<'_>| {
283        if use_color {
284            writeln!(
285                writer,
286                "Length: {length} | {} {} {} {} {}",
287                cfg.styles.null_char.paint("null_char"),
288                cfg.styles.printable.paint("printable"),
289                cfg.styles.whitespace.paint("whitespace"),
290                cfg.styles.ascii_other.paint("ascii_other"),
291                cfg.styles.non_ascii.paint("non_ascii"),
292            )
293        } else {
294            writeln!(writer, "Length: {length}")
295        }
296    };
297
298    if let Some(len) = cfg.length {
299        write(writer, format_args!("{len} (0x{len:x}) bytes"))
300    } else {
301        write(writer, format_args!("unknown (stream)"))
302    }
303}
304
305/// Reference wrapper for use in arguments formatting.
306pub struct Hex<'a, T: 'a>(&'a T, HexConfig);
307
308impl<'a, T: 'a + AsRef<[u8]>> fmt::Display for Hex<'a, T> {
309    /// Formats the value by `simple_hex_write` using the given formatter.
310    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
311        hex_write(f, self.0, self.1.to_simple(), None)
312    }
313}
314
315impl<'a, T: 'a + AsRef<[u8]>> fmt::Debug for Hex<'a, T> {
316    /// Formats the value by `pretty_hex_write` using the given formatter.
317    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
318        hex_write(f, self.0, self.1, None)
319    }
320}
321
322/// Allows generates hex dumps to a formatter.
323pub trait PrettyHex: Sized {
324    /// Wrap self reference for use in `std::fmt::Display` and `std::fmt::Debug`
325    /// formatting as hex dumps.
326    fn hex_dump(&self) -> Hex<'_, Self>;
327
328    /// Wrap self reference for use in `std::fmt::Display` and `std::fmt::Debug`
329    /// formatting as hex dumps in specified format.
330    fn hex_conf(&self, cfg: HexConfig) -> Hex<'_, Self>;
331}
332
333impl<T> PrettyHex for T
334where
335    T: AsRef<[u8]>,
336{
337    fn hex_dump(&self) -> Hex<'_, Self> {
338        Hex(self, HexConfig::default())
339    }
340    fn hex_conf(&self, cfg: HexConfig) -> Hex<'_, Self> {
341        Hex(self, cfg)
342    }
343}