Skip to main content

stackforge_core/utils/
hex.rs

1//! Hexdump and hex string utilities.
2
3use std::fmt::Write;
4
5/// Generate a hexdump of bytes in the style of `xxd` or Scapy's hexdump.
6///
7/// # Example
8/// ```
9/// use stackforge_core::hexdump;
10/// let data = b"Hello, World!";
11/// println!("{}", hexdump(data));
12/// ```
13#[must_use]
14pub fn hexdump(data: &[u8]) -> String {
15    let mut output = String::new();
16    let mut offset = 0;
17
18    for chunk in data.chunks(16) {
19        // Offset
20        write!(output, "{offset:08x}  ").unwrap();
21
22        // Hex bytes
23        for (i, byte) in chunk.iter().enumerate() {
24            if i == 8 {
25                output.push(' ');
26            }
27            write!(output, "{byte:02x} ").unwrap();
28        }
29
30        // Padding for incomplete lines
31        if chunk.len() < 16 {
32            for i in chunk.len()..16 {
33                if i == 8 {
34                    output.push(' ');
35                }
36                output.push_str("   ");
37            }
38        }
39
40        // ASCII representation
41        output.push(' ');
42        output.push('|');
43        for byte in chunk {
44            if byte.is_ascii_graphic() || *byte == b' ' {
45                output.push(*byte as char);
46            } else {
47                output.push('.');
48            }
49        }
50        output.push('|');
51        output.push('\n');
52
53        offset += 16;
54    }
55
56    output
57}
58
59/// Generate a compact hex string representation.
60#[must_use]
61pub fn hexstr(data: &[u8]) -> String {
62    let mut output = String::with_capacity(data.len() * 2);
63    for byte in data {
64        write!(output, "{byte:02x}").unwrap();
65    }
66    output
67}
68
69/// Generate hex string with separator.
70#[must_use]
71pub fn hexstr_sep(data: &[u8], sep: &str) -> String {
72    data.iter()
73        .map(|b| format!("{b:02x}"))
74        .collect::<Vec<_>>()
75        .join(sep)
76}
77
78/// Parse a hex string into bytes.
79pub fn parse_hex(s: &str) -> Result<Vec<u8>, String> {
80    let s = s.trim().replace([' ', ':', '-'], "");
81
82    if !s.len().is_multiple_of(2) {
83        return Err("hex string must have even length".to_string());
84    }
85
86    (0..s.len())
87        .step_by(2)
88        .map(|i| {
89            u8::from_str_radix(&s[i..i + 2], 16)
90                .map_err(|e| format!("invalid hex at position {i}: {e}"))
91        })
92        .collect()
93}
94
95/// Convert bytes to a pretty-printed representation (like Scapy's `show()`).
96#[must_use]
97pub fn pretty_bytes(data: &[u8], indent: usize) -> String {
98    let indent_str = " ".repeat(indent);
99    let mut output = String::new();
100
101    for (i, chunk) in data.chunks(16).enumerate() {
102        if i > 0 {
103            output.push('\n');
104        }
105        output.push_str(&indent_str);
106
107        for byte in chunk {
108            write!(output, "{byte:02x} ").unwrap();
109        }
110    }
111
112    output
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_hexdump() {
121        let data = b"Hello, World!";
122        let dump = hexdump(data);
123        assert!(dump.contains("48 65 6c 6c")); // "Hell"
124        assert!(dump.contains("|Hello, World!|"));
125    }
126
127    #[test]
128    fn test_hexstr() {
129        let data = [0xde, 0xad, 0xbe, 0xef];
130        assert_eq!(hexstr(&data), "deadbeef");
131        assert_eq!(hexstr_sep(&data, ":"), "de:ad:be:ef");
132    }
133
134    #[test]
135    fn test_parse_hex() {
136        assert_eq!(parse_hex("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
137        assert_eq!(
138            parse_hex("de:ad:be:ef").unwrap(),
139            vec![0xde, 0xad, 0xbe, 0xef]
140        );
141        assert_eq!(
142            parse_hex("de ad be ef").unwrap(),
143            vec![0xde, 0xad, 0xbe, 0xef]
144        );
145        assert!(parse_hex("dea").is_err()); // Odd length
146    }
147}