hexout 0.3.2

A compact and dependency-free, flexible and customizable hex dump library for Rust that provides beautiful, configurable binary data visualization.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
use std::{fmt::{Display, Debug}};

/// A compact and dependency-free, flexible and customizable hex dump library for Rust 
/// that provides beautiful, configurable binary data visualization.
/// 
/// # Features
/// - Configurable group sizes to any arbitrary byte count (up to 16 bytes)
/// - Adjustable number of groups per line
/// - Options for big-endian or little-endian byte order
/// - Toggleable ASCII representation alongside hex output
/// - Customizable display options (offsets, centerline, uppercase hex)
/// # Usage
/// Add `hexout` to your `Cargo.toml`:
/// ```toml
/// [dependencies]
/// hexout = "0.1"
/// ```
/// Then use it in your Rust code:
/// ```rust
/// use hexout::{hex_out, HexOutSettings};
/// let data = vec![0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9];
/// let settings = HexOutSettings {
///     group_size: 2,
///     groups_per_line: 8,
///     big_endian: false,
///     show_ascii: true,
///     ..Default::default()
/// };
/// let dump = hex_out(&data, &settings, 0, 0, 1).unwrap();
/// println!("{}", dump);
/// ```
/// # License
/// This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

#[derive(Debug, Clone)]
/// Settings to customize the hex dump output.
pub struct HexOutSettings {
    /// The origin address to start from.  Does not change the actual offset of the data, just the displayed address.  (default is 0).
    pub address_origin: usize,
    /// Width of the address field in characters (default is 8 for 32-bit addresses).
    pub address_width: usize,
    /// Whether to align the address to the nearest group boundary.
    pub align_address: bool,
    /// Whether to use big-endian byte order.
    pub big_endian: bool,
    /// Number of bytes in each group.  This can be arbitrary number up to 16, doesn't have to be 1, 2, 4, or 8.
    pub group_size: usize,
    /// Number of groups to display per line.
    pub groups_per_line: usize,
    /// Character to use for invalid or out-of-bounds data (default is '?').
    pub invalid_data_placeholder: char,
    /// Whether to show the ASCII representation alongside the hex output.
    pub show_ascii: bool,
    /// Whether to show a centerline between groups.
    pub show_centerline: bool,
    /// Whether to show a header row with column addresses.
    pub show_header: bool,
    /// Whether to show the offset at the start of each line.
    pub show_offset: bool,
    /// Whether to use strict mode (HexOutError on incomplete groups).
    pub strict: bool,
    /// Whether to use uppercase letters for hex digits.
    pub uppercase: bool,
    /// Optional prefix to add to HexOutError indicators.  This is useful for things like ANSI color codes.
    pub hex_out_error_prefix: Option<String>,
    /// Optional postfix to add to HexOutError indicators.  This is useful for things like ANSI color codes.
    pub hex_out_error_postfix: Option<String>,
}

impl Default for HexOutSettings {
    fn default() -> Self {
        Self {
            address_origin: 0,
            address_width: 8,
            align_address: true,
            big_endian: false,
            group_size: 1,
            groups_per_line: 16,
            invalid_data_placeholder: '?',
            show_ascii: true,
            show_centerline: true,
            show_header: false,
            show_offset: true,
            strict: false,
            uppercase: false,
            hex_out_error_prefix: None,
            hex_out_error_postfix: None,
        }
    }
}

/// Errors that can occur during hex dump generation.
#[derive(Clone)]
pub enum HexOutError {
    /// The specified group size is invalid (must be between 1 and 16).
    InvalidGroupSize,
    /// The specified offset does not align with the group size in strict mode.
    UnalignedOffset { offset: usize, group_size: usize },
    InvalidAddressWidth,
}

impl Display for HexOutError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HexOutError::InvalidGroupSize => write!(f, "Invalid group size (must be 1-16)"),
            HexOutError::UnalignedOffset { offset, group_size } => {
                write!(
                    f, 
                    "Offset {} does not align with group size {} in strict mode (offset % group_size = {})",
                    offset, group_size, offset % group_size
                )
            },
            HexOutError::InvalidAddressWidth => write!(f, "Invalid address width (must be 2-16)"),
        }
    }
}

impl Debug for HexOutError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HexOutError::InvalidGroupSize => write!(f, "HexOutError::InvalidGroupSize"),
            HexOutError::UnalignedOffset { offset, group_size } => {
                write!(f, "HexOutError::UnalignedOffset {{ offset: {offset}, group_size: {group_size} }}")
            }
            HexOutError::InvalidAddressWidth => write!(f, "HexOutError::InvalidAddressWidth"),
        }
    }
}

impl std::error::Error for HexOutError {}

/// A trait to provide functionality for byte slices.
pub trait HexOut {
    fn hex_out(&self) -> Result<String, HexOutError>;
    fn hex_out_lines(&self, start_line: usize, line_count: usize) -> Result<String, HexOutError>;
    fn hex_out_with_settings(&self, settings: HexOutSettings) -> Result<String, HexOutError>;
    fn hex_out_lines_with_settings(
        &self,
        settings: HexOutSettings,
        start_line: usize,
        line_count: usize,
    ) -> Result<String, HexOutError>;
}

impl HexOut for &[u8] {
    fn hex_out(&self) -> Result<String, HexOutError> {
        hex_out(self, &HexOutSettings::default(), 0, 0, 0)
    }

    fn hex_out_lines(&self, start_line: usize, line_count: usize) -> Result<String, HexOutError> {
        hex_out(self, &HexOutSettings::default(), 0, start_line, line_count)
    }

    fn hex_out_with_settings(&self, settings: HexOutSettings) -> Result<String, HexOutError> {
        hex_out(self, &settings, 0, 0, 0)
    }

    fn hex_out_lines_with_settings(
        &self,
        settings: HexOutSettings,
        start_line: usize,
        line_count: usize,
    ) -> Result<String, HexOutError> {
        hex_out(self, &settings, 0, start_line, line_count)
    }
}

impl HexOut for Vec<u8> {
    fn hex_out(&self) -> Result<String, HexOutError> {
        self.as_slice().hex_out()
    }

    fn hex_out_lines(&self, start_line: usize, line_count: usize) -> Result<String, HexOutError> {
        self.as_slice().hex_out_lines(start_line, line_count)
    }

    fn hex_out_with_settings(&self, settings: HexOutSettings) -> Result<String, HexOutError> {
        self.as_slice().hex_out_with_settings(settings)
    }

    fn hex_out_lines_with_settings(
        &self,
        settings: HexOutSettings,
        start_line: usize,
        line_count: usize,
    ) -> Result<String, HexOutError> {
        self.as_slice().hex_out_lines_with_settings(settings, start_line, line_count)
    }
}

/// Generate a hex dump of the given data with the specified settings.
/// 
/// # Parameters
/// `data`: The byte slice to be dumped.
/// 
/// `settings`: The settings to customize the output.
/// 
/// `offset`: The starting offset for the dump.
/// 
/// `start_line`: The line number to start the dump from (0-based).
/// 
/// `line_count`: The number of lines to include in the dump. (If 0, dumps all lines from start_line to the end of data.)
/// 
/// # Returns
/// Returns a formatted string representing the hex dump.
pub fn hex_out(
    data: &[u8],
    settings: &HexOutSettings,
    offset: usize,
    start_line: usize,
    line_count: usize,
) -> Result<String, HexOutError> {
    // Validate group_size
    if settings.group_size == 0 || settings.group_size > 16 {
        return Err(HexOutError::InvalidGroupSize);
    }
    // Validate address_width
    if settings.address_width < 2 || settings.address_width > 16 {
        return Err(HexOutError::InvalidAddressWidth);
    }
    // If strict mode is enabled, ensure we don't start in the middle of a group
    if settings.strict && (offset % settings.group_size != 0) {
        return Err(HexOutError::UnalignedOffset { 
            offset, 
            group_size: settings.group_size 
        });
    }
    let total_bytes_per_line = settings.group_size * settings.groups_per_line;
    // Setup buffers
    let mut line = String::with_capacity(total_bytes_per_line * 3);
    let mut ascii = String::with_capacity(total_bytes_per_line);
    // Calculate total lines
    let last_line_offset = if line_count == 0 {
        data.len()
    } else {
        let last_line_offset = ((start_line + line_count) * total_bytes_per_line + offset).min(data.len());
        // Align last_line_offset down to the nearest line boundary if it's beyond the requested line count
        if last_line_offset < line_count * total_bytes_per_line {
            last_line_offset
        } else {
            last_line_offset - (last_line_offset % total_bytes_per_line)
        }
    };
    // Align last_line_offset to group boundary
    let last_line_offset = last_line_offset
        + (settings.group_size - (last_line_offset % settings.group_size)) % settings.group_size;

    // Calculate starting index
    // Allocate result string with estimated capacity
    let mut result = String::with_capacity(total_bytes_per_line * line_count * 5);
    if settings.show_header {
        // Generate header line
        let mut header = String::with_capacity(total_bytes_per_line * 3);
        if settings.show_offset {
            if settings.address_width >= 8 {
                header.push_str("Address: ");
                header.push_str(&" ".repeat(settings.address_width - 8));
            } else if settings.address_width >= 4 {
                header.push_str("Addr: ");
                header.push_str(&" ".repeat(settings.address_width - 4));
            } else {
                header.push_str(&" ".repeat(settings.address_width));
            }
        }
        for group in 0..settings.groups_per_line {
            if group > 0 {
                header.push(' ');
            }
            if group == settings.groups_per_line / 2 && settings.show_centerline {
                header.push(' ');
            }
            let col_index = group * settings.group_size;
            header.push_str(&" ".repeat(settings.group_size * 2 - 2));
            if settings.uppercase {
                header.push_str(&format!("{col_index:02X}"));
            } else {
                header.push_str(&format!("{col_index:02x}"));
            }
        }
        result.push_str(&header);
        result.push('\n');
    }

    let mut cursor = if settings.align_address { 0 } else { offset };
    let mut group_index = 0;
    let mut group_byte_index = 0;
    let mut group_value: u128 = 0;
    let mut out_of_bounds_count = 0;
    // Update start line if offset is greater than a single line, to skip lines before the offset
    let start_line = if offset >= total_bytes_per_line {
        offset / total_bytes_per_line
    } else {
        start_line    
    };

    // Move cursor to the start line
    cursor += start_line * total_bytes_per_line;
    // Setup the starting address
    let mut addr = if settings.align_address {
        cursor - (cursor % (settings.group_size * settings.groups_per_line))
    } else {
        cursor
    } + settings.address_origin;
    // Main loop to process each byte
    while cursor < last_line_offset {
        let byte = if let Some(b) = data.get(cursor) {
            *b
        } else {
            out_of_bounds_count += 1;
            0
        };
        // If enabled, store ASCII representation of each byte
        if settings.show_ascii {
            if out_of_bounds_count > 0 || cursor < offset {
                ascii.push(' ');
            } else if (0x20..0x7F).contains(&byte) {
                ascii.push(byte as char);
            } else {
                ascii.push('.');
            }
        }
        if settings.big_endian {
            // Big-endian: fill the group from the end
            group_value = (group_value << 8) | byte as u128;
        } else {
            // Little-endian: fill the group from the start
            group_value |= (byte as u128) << (8 * group_byte_index);
        }
        group_byte_index += 1;
        // Check if the group is full
        if group_byte_index >= settings.group_size {
            // Add space before group if not the first group
            if !line.is_empty() {
                line.push(' ');
            }
            if cursor < offset {
                // If before the offset, just add spaces
                line.push_str("  ".repeat(settings.group_size).as_str());
            // Group is full, output it
            } else {
                let mut value = if settings.uppercase {
                    format!("{group_value:0width$X}", width = settings.group_size * 2)
                } else {
                    format!("{group_value:0width$x}", width = settings.group_size * 2)
                };
                // If cursor - (group_byte_index - 1) < offset, we are still before the offset
                // replace the leading digits (or trailing if big-endian) with question marks
                if cursor.saturating_sub(group_byte_index - 1) < offset || out_of_bounds_count > 0 {
                    let missing_bytes = if out_of_bounds_count > 0 {
                        out_of_bounds_count
                    } else {
                        settings.group_size - (group_byte_index - cursor % settings.group_size)
                    };
                    let replace_chars = (missing_bytes * 2).min(value.len());
                    let mut replacement = settings.invalid_data_placeholder.to_string().repeat(replace_chars);
                    if let Some(prefix) = &settings.hex_out_error_prefix 
                    {
                        replacement = format!("{prefix}{replacement}");
                    }
                    if let Some(postfix) = &settings.hex_out_error_postfix 
                    {
                        replacement = format!("{replacement}{postfix}");
                    }
                    // The decision to replace leading or trailing characters is based on endiannes and whether we are missing bytes
                    // The following condition represents an XOR operation for the following table:
                    // Big-endian  | Out of bounds bytes | Replace leading chars
                    //    false    |        false        |       false
                    //    false    |         true        |        true
                    //     true    |        false        |        true
                    //     true    |         true        |       false
                    if settings.big_endian != (out_of_bounds_count > 0) {
                        // Big-endian: replace leading characters
                        value.replace_range(0..replace_chars, &replacement);
                    } else {
                        // Little-endian: replace trailing characters
                        let start = value.len() - replace_chars;
                        value.replace_range(start..value.len(), &replacement);
                    }
                }
                line.push_str(&value);
            }
            group_index += 1;
            let is_last_line = cursor + 1 == last_line_offset;
            // Check if we need to add a centerline
            if group_index == settings.groups_per_line / 2 && settings.show_centerline {
                if settings.show_ascii {
                    ascii.push(' ');
                }
                line.push(' ');
            }
            if group_index == settings.groups_per_line || is_last_line || out_of_bounds_count > 0 {
                // End of line or last line
                // Add the address offset if enabled
                if settings.show_offset {
                    result.push_str(&format!(
                        "{:0width$x}: ",
                        addr,
                        width = settings.address_width
                    ));
                }
                // If this is the last line, we may need to pad the line
                if (is_last_line || out_of_bounds_count > 0) && settings.show_ascii {
                    // If centerline is shown, account for that space
                    let centerline_size = if settings.show_centerline { 1 } else { 0 };
                    // Calculate padding needed
                    let pad_length = ((settings.group_size * 2 + 1) * settings.groups_per_line + centerline_size - 1).saturating_sub(line.len());
                    let ascii_pad_length = (total_bytes_per_line + centerline_size).saturating_sub(ascii.len());
                    // Pad both hex and ASCII parts
                    line.push_str(
                        &" ".repeat(pad_length),
                    );
                    if settings.show_ascii {
                        ascii.push_str(
                            &" ".repeat(ascii_pad_length),
                        );
                    }
                }
                // Append the line and ASCII representation to the result
                result.push_str(&line);
                if settings.show_ascii {
                    result.push(' ');
                    result.push('|');
                    result.push_str(&ascii);
                    result.push('|');
                }
                // Add newline if not the last line
                if !is_last_line {
                    result.push('\n');
                }
                // Stop processing if we're past the data length
                if out_of_bounds_count > 0 {
                    break;
                }
                // Rinse and repeat
                line.clear();
                ascii.clear();
                addr += total_bytes_per_line;
                group_index = 0;
            }
            group_value = 0;
            group_byte_index = 0;
        }
        cursor += 1;
    }
    Ok(result)
}

#[cfg(test)]
mod tests;