iroh_test/
hexdump.rs

1use anyhow::{ensure, Context, Result};
2
3/// Parses a commented multi line hexdump into a vector of bytes.
4///
5/// This is useful to write wire level protocol tests.
6pub fn parse_hexdump(s: &str) -> Result<Vec<u8>> {
7    let mut result = Vec::new();
8
9    for (line_number, line) in s.lines().enumerate() {
10        let data_part = line.split('#').next().unwrap_or("");
11        let cleaned: String = data_part.chars().filter(|c| !c.is_whitespace()).collect();
12
13        ensure!(
14            cleaned.len() % 2 == 0,
15            "Non-even number of hex chars detected on line {}.",
16            line_number + 1
17        );
18
19        for i in (0..cleaned.len()).step_by(2) {
20            let byte_str = &cleaned[i..i + 2];
21            let byte = u8::from_str_radix(byte_str, 16)
22                .with_context(|| format!("Invalid hex data on line {}.", line_number + 1))?;
23
24            result.push(byte);
25        }
26    }
27
28    Ok(result)
29}
30
31/// Returns a hexdump of the given bytes in multiple lines as a String.
32pub fn print_hexdump(bytes: impl AsRef<[u8]>, line_lengths: impl AsRef<[usize]>) -> String {
33    let line_lengths = line_lengths.as_ref();
34    let mut bytes_iter = bytes.as_ref().iter();
35    let default_line_length = line_lengths
36        .last()
37        .filter(|x| **x != 0)
38        .copied()
39        .unwrap_or(16);
40    let mut line_lengths_iter = line_lengths.iter();
41    let mut output = String::new();
42
43    loop {
44        let line_length = line_lengths_iter
45            .next()
46            .copied()
47            .unwrap_or(default_line_length);
48        if line_length == 0 {
49            output.push('\n');
50        } else {
51            let line: Vec<_> = bytes_iter.by_ref().take(line_length).collect();
52
53            if line.is_empty() {
54                break;
55            }
56
57            for byte in &line {
58                output.push_str(&format!("{:02x} ", byte));
59            }
60            output.pop(); // Remove the trailing space
61            output.push('\n');
62        }
63    }
64
65    output
66}
67
68/// This is a macro to assert that two byte slices are equal.
69///
70/// It is like assert_eq!, but it will print a nicely formatted hexdump of the
71/// two slices if they are not equal. This makes it much easier to track down
72/// a difference in a large byte slice.
73#[macro_export]
74macro_rules! assert_eq_hex {
75    ($a:expr, $b:expr) => {
76        assert_eq_hex!($a, $b, [])
77    };
78    ($a:expr, $b:expr, $hint:expr) => {
79        let a = $a;
80        let b = $b;
81        let hint = $hint;
82        let ar: &[u8] = a.as_ref();
83        let br: &[u8] = b.as_ref();
84        let hintr: &[usize] = hint.as_ref();
85        if ar != br {
86            panic!(
87                "assertion failed: `(left == right)`\nleft:\n{}\nright:\n{}\n",
88                ::iroh_test::hexdump::print_hexdump(ar, hintr),
89                ::iroh_test::hexdump::print_hexdump(br, hintr),
90            )
91        }
92    };
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{parse_hexdump, print_hexdump};
98
99    #[test]
100    fn test_basic() {
101        let input = r"
102            a1b2 # comment
103            3c4d
104        ";
105        let result = parse_hexdump(input).unwrap();
106        assert_eq!(result, vec![0xa1, 0xb2, 0x3c, 0x4d]);
107    }
108
109    #[test]
110    fn test_upper_case() {
111        let input = r"
112            A1B2 # comment
113            3C4D
114        ";
115        let result = parse_hexdump(input).unwrap();
116        assert_eq!(result, vec![0xa1, 0xb2, 0x3c, 0x4d]);
117    }
118
119    #[test]
120    fn test_mixed_case() {
121        let input = r"
122            a1B2 # comment
123            3C4d
124        ";
125        let result = parse_hexdump(input).unwrap();
126        assert_eq!(result, vec![0xa1, 0xb2, 0x3c, 0x4d]);
127    }
128
129    #[test]
130    fn test_odd_characters() {
131        let input = r"
132            a1b
133        ";
134        let result = parse_hexdump(input);
135        assert!(result.is_err());
136    }
137
138    #[test]
139    fn test_invalid_characters() {
140        let input = r"
141            a1g2 # 'g' is not valid in hex
142        ";
143        let result = parse_hexdump(input);
144        assert!(result.is_err());
145    }
146    #[test]
147    fn test_basic_hexdump() {
148        let data: &[u8] = &[0x1, 0x2, 0x3, 0x4, 0x5];
149        let output = print_hexdump(data, [1, 2]);
150        assert_eq!(output, "01\n02 03\n04 05\n");
151    }
152
153    #[test]
154    fn test_newline_insertion() {
155        let data: &[u8] = &[0x1, 0x2, 0x3, 0x4];
156        let output = print_hexdump(data, [1, 0, 2]);
157        assert_eq!(output, "01\n\n02 03\n04\n");
158    }
159
160    #[test]
161    fn test_indefinite_line_length() {
162        let data: &[u8] = &[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8];
163        let output = print_hexdump(data, [2, 4]);
164        assert_eq!(output, "01 02\n03 04 05 06\n07 08\n");
165    }
166
167    #[test]
168    fn test_empty_data() {
169        let data: &[u8] = &[];
170        let output = print_hexdump(data, [1, 2]);
171        assert_eq!(output, "");
172    }
173
174    #[test]
175    fn test_zeros_then_default() {
176        let data: &[u8] = &[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8];
177        let output = print_hexdump(data, [1, 0, 0, 2]);
178        assert_eq!(output, "01\n\n\n02 03\n04 05\n06 07\n08\n");
179    }
180}