Skip to main content

wrapbin/repr/
dump.rs

1//!
2//! Hexadecimal dump of a file.
3//!
4//! ```ebnf
5//! DumpRepresentation ::= [ HeaderLine ] { '\n' DataLine }
6//!
7//! HeaderLine ::= PrefixString ' '{4-7} (Column8   | Column16   | Column32  |
8//!                          Column2C8 | Column2C16 | Column2C32)
9//!
10//! DataLine ::= LineIndex  (Column8   | Column16   | Column32  |
11//!                          Column2C8 | Column2C16 | Column2C32)
12//! LineIndex ::= Nybble{3-6} ': '
13//!
14//! Column8 ::= Byte ( ' ' Byte ){0-7}
15//! Column2C8 ::= Column8 [ ' - ' Column8 ]
16//! Column16 ::= Byte ( ' ' Byte ){0-15}
17//! Column2C16 ::= Column16 [ ' - ' Column16 ]
18//! Column32 ::= Byte ( ' ' Byte ){0-31}
19//! Column2C32 ::= Column32 [ ' - ' Column32 ]
20//!
21//! Byte ::= Nybble Nybble Nybble?
22//! Nybble ::= [0-9a-fA-F]
23//! ```
24//!
25//! # Examples
26//!
27#![cfg_attr(
28    any(
29        not(feature = "repr-dump"),
30        all(feature = "repr-dump", feature = "repr-color")
31    ),
32    doc = "```ignore"
33)]
34#![cfg_attr(
35    all(feature = "repr-dump", not(feature = "repr-color")),
36    doc = "```rust"
37)]
38//! use wrapbin::{
39//!     Binary,
40//!     repr::{BinaryFormatOptions, dump::DumpFormatOptions, format, has_color}
41//! };
42//!
43//! let binary = Binary::from([
44//!     0x7b_u8,0xe6_u8,0xd4_u8,0xf2_u8,0x25_u8,0x5c_u8,0x62_u8,0xd3_u8,
45//!     0x21_u8,0x24_u8,0xab_u8,0x7e_u8,0x40_u8,0xf1_u8,0x7b_u8,0xce_u8,
46//!     0x17_u8,0x3c_u8,0x08_u8,0xd2_u8,0xd1_u8,0xce_u8,0xcc_u8,0x17_u8,
47//! ]);
48//!
49//! assert_eq!(
50//!     format(
51//!         &binary,
52//!         DumpFormatOptions::classic_hex_dump()),
53//!     vec![
54//!         "0X       00 01 02 03 04 05 06 07 - 08 09 0A 0B 0C 0D 0E 0F ",
55//!         "000000:  7B E6 D4 F2 25 5C 62 D3 - 21 24 AB 7E 40 F1 7B CE ",
56//!         "000010:  17 3C 08 D2 D1 CE CC 17 - ",
57//!     ].join("\n")
58//! );
59//! ```
60//!
61
62use crate::{
63    Binary,
64    error::Error,
65    repr::{BinaryFormatOptions, ByteKind, RadixFormat, ReprComponentKind},
66};
67use alloc::{
68    format,
69    string::{String, ToString},
70};
71use core::{
72    assert,
73    clone::Clone,
74    cmp::{Eq, PartialEq},
75    default::Default,
76    fmt::Debug,
77    iter::Iterator,
78    marker::Copy,
79    option::Option::{self, None, Some},
80    result::Result,
81    todo, unreachable,
82};
83
84// ------------------------------------------------------------------------------------------------
85// Public Types
86// ------------------------------------------------------------------------------------------------
87
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct DumpFormatOptions {
90    radix_format: RadixFormat,
91    compact: bool,
92    index_header_line: bool,
93    index_line_numbers: bool,
94    index_radix_format: RadixFormat,
95    column_width: DumpColumnWidth,
96    two_columns: bool,
97    show_ascii: bool,
98    show_extended_ascii: bool,
99    line_index_spacing: String,
100    value_spacing: String,
101    column_separator: char,
102    column_index_underline: Option<char>,
103    colored: bool,
104}
105
106#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
107#[repr(usize)]
108pub enum DumpColumnWidth {
109    #[default]
110    Eight = 8,
111    Sixteen = 16,
112    ThirtyTwo = 32,
113}
114
115// ------------------------------------------------------------------------------------------------
116// Public Functions
117// ------------------------------------------------------------------------------------------------
118
119pub fn dump_representation(value: &Binary<'_>, options: &DumpFormatOptions) -> String {
120    // --------------------------------------------------------------------------------------------
121    // This is not supported the line indexes get ridiculous.
122    // --------------------------------------------------------------------------------------------
123    assert!(options.index_radix_format != RadixFormat::Binary);
124
125    let (mid, end) = options.byte_counts();
126    let mut buffer = String::default();
127
128    // --------------------------------------------------------------------------------------------
129    // Header line(s).
130    // --------------------------------------------------------------------------------------------
131    if options.index_header_line {
132        buffer.push_str(&format!(
133            "{:1$}{2:3$}",
134            options.radix_format.prefix_str(),
135            options.line_index_width(),
136            "",
137            options.line_index_spacing.len(),
138        ));
139        for i in 0..end {
140            buffer.push_str(&options.format_column_index(i));
141            if (i + 1) % end != 0 && options.two_columns && (i + 1) % mid == 0 {
142                buffer.push_str(&options.format_column_separator());
143            }
144        }
145        buffer.push('\n');
146        if let Some(underline) = options.format_header_underline() {
147            buffer.push_str(&format!(
148                "{:1$}",
149                "",
150                options.line_index_width() + options.line_index_spacing.len()
151            ));
152            buffer.push_str(&underline);
153        }
154    }
155
156    // --------------------------------------------------------------------------------------------
157    // Actual data formatting.
158    // --------------------------------------------------------------------------------------------
159    for (index, byte) in value.iter().enumerate() {
160        let one_index = index + 1;
161
162        if options.index_line_numbers && index == 0 || index % end == 0 {
163            buffer.push_str(&options.format_line_index(index));
164        }
165
166        if options.show_ascii {
167            buffer.push_str(&options.format_ascii_char(byte));
168        } else {
169            buffer.push_str(&options.format_data_value(*byte));
170        }
171
172        if one_index % end == 0 {
173            buffer.push('\n');
174        } else if options.two_columns && one_index > 0 && one_index % mid == 0 {
175            buffer.push_str(&options.format_column_separator());
176        }
177    }
178    buffer
179}
180
181pub fn parse_dump_representation(_s: &str) -> Result<Binary<'_>, Error> {
182    todo!()
183}
184
185// ------------------------------------------------------------------------------------------------
186// Implementations
187// ------------------------------------------------------------------------------------------------
188
189impl From<DumpFormatOptions> for BinaryFormatOptions {
190    fn from(value: DumpFormatOptions) -> Self {
191        Self::Dump(value)
192    }
193}
194
195impl Default for DumpFormatOptions {
196    fn default() -> Self {
197        Self {
198            radix_format: RadixFormat::default(),
199            compact: false,
200            index_header_line: true,
201            index_line_numbers: true,
202            index_radix_format: RadixFormat::default(),
203            column_width: DumpColumnWidth::default(),
204            two_columns: true,
205            show_ascii: false,
206            show_extended_ascii: false,
207            line_index_spacing: ":  ".to_string(),
208            value_spacing: " ".to_string(),
209            column_separator: '│',
210            column_index_underline: Some('─'),
211            colored: cfg!(feature = "repr-color"),
212        }
213    }
214}
215
216impl DumpFormatOptions {
217    pub fn classic_hex_dump() -> Self {
218        Self::default()
219            .with_upper_hex_bytes()
220            .with_upper_hex_indices()
221            .compact(false)
222            .two_columns_of(DumpColumnWidth::Eight)
223            .no_column_index_underline()
224            .separate_columns_with('-')
225            .show_ascii(false)
226    }
227
228    pub fn ascii_hex_dump() -> Self {
229        Self::default()
230            .with_upper_hex_bytes()
231            .with_upper_hex_indices()
232            .compact(false)
233            .two_columns_of(DumpColumnWidth::Eight)
234            .no_column_index_underline()
235            .separate_columns_with('-')
236            .show_extended_ascii(true)
237    }
238
239    pub fn hex_dump() -> Self {
240        Self::default()
241            .with_upper_hex_bytes()
242            .with_upper_hex_indices()
243            .compact(false)
244            .two_columns_of(DumpColumnWidth::Eight)
245            .show_ascii(false)
246    }
247
248    pub fn octal_dump() -> Self {
249        Self::default()
250            .with_octal_bytes()
251            .with_octal_indices()
252            .compact(false)
253            .two_columns_of(DumpColumnWidth::Eight)
254            .show_ascii(false)
255    }
256
257    pub fn binary_dump() -> Self {
258        Self::default()
259            .with_binary_bytes()
260            .with_upper_hex_indices()
261            .compact(false)
262            .two_columns_of(DumpColumnWidth::Eight)
263            .show_ascii(false)
264    }
265
266    pub fn decimal_dump() -> Self {
267        Self::default()
268            .with_decimal_bytes()
269            .with_decimal_indices()
270            .compact(false)
271            .two_columns_of(DumpColumnWidth::Eight)
272            .show_ascii(false)
273    }
274
275    pub fn lower_hex_dump() -> Self {
276        Self::default()
277            .with_lower_hex_bytes()
278            .with_lower_hex_indices()
279            .compact(false)
280            .two_columns_of(DumpColumnWidth::Eight)
281            .show_ascii(false)
282    }
283
284    /// Sets the radix format for each byte in the array to be one of the values of the enum
285    /// [`RadixFormat`].
286    pub fn with_byte_radix_format(mut self, radix_format: RadixFormat) -> Self {
287        self.radix_format = radix_format;
288        self
289    }
290    /// Sets the radix format for each byte in the array to [`RadixFormat::Binary`].
291    pub fn with_binary_bytes(self) -> Self {
292        Self::with_byte_radix_format(self, RadixFormat::Binary)
293    }
294    /// Sets the radix format for each byte in the array to [`RadixFormat::Decimal`].
295    pub fn with_decimal_bytes(self) -> Self {
296        Self::with_byte_radix_format(self, RadixFormat::Decimal)
297    }
298    /// Sets the radix format for each byte in the array to [`RadixFormat::LowerHex`].
299    pub fn with_lower_hex_bytes(self) -> Self {
300        Self::with_byte_radix_format(self, RadixFormat::LowerHex)
301    }
302    /// Sets the radix format for each byte in the array to [`RadixFormat::Octal`].
303    pub fn with_octal_bytes(self) -> Self {
304        Self::with_byte_radix_format(self, RadixFormat::Octal)
305    }
306    /// Sets the radix format for each byte in the array to [`RadixFormat::UpperHex`].
307    pub fn with_upper_hex_bytes(self) -> Self {
308        Self::with_byte_radix_format(self, RadixFormat::UpperHex)
309    }
310
311    pub fn has_index_header_line(mut self, header_line: bool) -> Self {
312        self.index_header_line = header_line;
313        self
314    }
315
316    pub fn underline_column_index_with(mut self, underline: char) -> Self {
317        self.column_index_underline = Some(underline);
318        self
319    }
320
321    pub fn no_column_index_underline(mut self) -> Self {
322        self.column_index_underline = None;
323        self
324    }
325
326    pub fn separate_columns_with(mut self, column_separator: char) -> Self {
327        self.column_separator = column_separator;
328        self
329    }
330
331    pub fn has_index_line_numbers(mut self, line_numbers: bool) -> Self {
332        self.index_line_numbers = line_numbers;
333        self
334    }
335
336    /// Sets the radix format for line and column indices to be one of the values
337    /// of the enum [`RadixFormat`].
338    pub fn with_index_radix_format(mut self, index_radix_format: RadixFormat) -> Self {
339        self.index_radix_format = index_radix_format;
340        self
341    }
342    /// Sets the radix format for line and column indices to [`RadixFormat::Decimal`].
343    pub fn with_decimal_indices(self) -> Self {
344        Self::with_index_radix_format(self, RadixFormat::Decimal)
345    }
346    /// Sets the radix format for line and column indices to [`RadixFormat::LowerHex`].
347    pub fn with_lower_hex_indices(self) -> Self {
348        Self::with_index_radix_format(self, RadixFormat::LowerHex)
349    }
350    /// Sets the radix format for line and column indices to [`RadixFormat::Octal`].
351    pub fn with_octal_indices(self) -> Self {
352        Self::with_index_radix_format(self, RadixFormat::Octal)
353    }
354    /// Sets the radix format for line and column indices to [`RadixFormat::UpperHex`].
355    pub fn with_upper_hex_indices(self) -> Self {
356        Self::with_index_radix_format(self, RadixFormat::UpperHex)
357    }
358
359    pub fn has_two_columns(mut self, two_columns: bool) -> Self {
360        self.two_columns = two_columns;
361        self
362    }
363
364    pub fn with_column_width(mut self, column_width: DumpColumnWidth) -> Self {
365        self.column_width = column_width;
366        self
367    }
368
369    pub fn one_column_of(mut self, column_width: DumpColumnWidth) -> Self {
370        self.two_columns = false;
371        self.column_width = column_width;
372        self
373    }
374
375    pub fn two_columns_of(mut self, column_width: DumpColumnWidth) -> Self {
376        self.two_columns = true;
377        self.column_width = column_width;
378        self
379    }
380
381    pub fn compact(mut self, compact: bool) -> Self {
382        self.compact = compact;
383        self
384    }
385
386    pub fn show_ascii(mut self, show_ascii: bool) -> Self {
387        self = self.with_upper_hex_bytes();
388        self.show_ascii = show_ascii;
389        self
390    }
391
392    pub fn show_extended_ascii(mut self, show_extended_ascii: bool) -> Self {
393        self = self.show_ascii(true);
394        self.show_extended_ascii = show_extended_ascii;
395        self
396    }
397
398    /// Use color to denote byte kind according the ASCII conventions denoted by the
399    /// enums `ByteStyle` and `ReprStyle`.
400    #[cfg(feature = "repr-color")]
401    pub fn use_color(mut self, colored: bool) -> Self {
402        self.colored = colored;
403        self
404    }
405
406    const fn byte_counts(&self) -> (usize, usize) {
407        match (self.two_columns, self.column_width) {
408            (false, w @ DumpColumnWidth::Eight) => (0, w.byte_count()),
409            (true, w @ DumpColumnWidth::Eight) => (w.byte_count(), w.two_column_byte_count()),
410            (false, w @ DumpColumnWidth::Sixteen) => (0, w.byte_count()),
411            (true, w @ DumpColumnWidth::Sixteen) => (w.byte_count(), w.two_column_byte_count()),
412            (false, w @ DumpColumnWidth::ThirtyTwo) => (0, w.byte_count()),
413            (true, w @ DumpColumnWidth::ThirtyTwo) => (w.byte_count(), w.two_column_byte_count()),
414        }
415    }
416
417    fn format_column_index(&self, index: usize) -> String {
418        let style = ReprComponentKind::Index.display_style(self.colored);
419        match self.radix_format {
420            RadixFormat::Binary => {
421                format!(
422                    "{style}{index:00$b}{style:#}{spacing}",
423                    self.data_value_width(),
424                    spacing = self.value_spacing
425                )
426            }
427            RadixFormat::Decimal => {
428                format!(
429                    "{style}{index:00$}{style:#}{spacing}",
430                    self.data_value_width(),
431                    spacing = self.value_spacing
432                )
433            }
434            RadixFormat::Octal => {
435                format!(
436                    "{style}{index:00$o}{style:#}{spacing}",
437                    self.data_value_width(),
438                    spacing = self.value_spacing
439                )
440            }
441            RadixFormat::LowerHex => {
442                format!(
443                    "{style}{index:00$x}{style:#}{spacing}",
444                    self.data_value_width(),
445                    spacing = self.value_spacing
446                )
447            }
448            RadixFormat::UpperHex => {
449                format!(
450                    "{style}{index:00$X}{style:#}{spacing}",
451                    self.data_value_width(),
452                    spacing = self.value_spacing
453                )
454            }
455        }
456    }
457
458    fn format_header_underline(&self) -> Option<String> {
459        if let Some(underline) = self.column_index_underline {
460            let width =
461                (self.data_value_width() + self.value_spacing.len()) * self.column_width as usize;
462            let style = ReprComponentKind::Separator.display_style(self.colored);
463            let underline = format!("{style}{}{style:#}", underline.to_string().repeat(width));
464            let mut buffer = String::default();
465            buffer.push_str(&underline);
466            if self.two_columns {
467                buffer.push_str(&self.format_column_separator());
468                buffer.push_str(&underline);
469            }
470            buffer.push('\n');
471            Some(buffer)
472        } else {
473            None
474        }
475    }
476
477    fn format_column_separator(&self) -> String {
478        let style = ReprComponentKind::Separator.display_style(self.colored);
479        format!(
480            "{style}{}{}{style:#}",
481            self.column_separator, self.value_spacing
482        )
483    }
484
485    const fn line_index_width(&self) -> usize {
486        match self.index_radix_format {
487            RadixFormat::Decimal | RadixFormat::Octal => 8,
488            RadixFormat::LowerHex | RadixFormat::UpperHex => 6,
489            _ => unreachable!(),
490        }
491    }
492
493    fn format_line_index(&self, index: usize) -> String {
494        let style = ReprComponentKind::Index.display_style(self.colored);
495        match self.index_radix_format {
496            RadixFormat::Decimal => format!(
497                "{style}{index:0width$}{spacer}{style:#}",
498                width = self.line_index_width(),
499                spacer = self.line_index_spacing
500            ),
501            RadixFormat::Octal => format!(
502                "{style}{index:0width$o}{spacer}{style:#}",
503                width = self.line_index_width(),
504                spacer = self.line_index_spacing
505            ),
506            RadixFormat::LowerHex => format!(
507                "{style}{index:0width$x}{spacer}{style:#}",
508                width = self.line_index_width(),
509                spacer = self.line_index_spacing
510            ),
511            RadixFormat::UpperHex => format!(
512                "{style}{index:0width$X}{spacer}{style:#}",
513                width = self.line_index_width(),
514                spacer = self.line_index_spacing
515            ),
516            _ => unreachable!(),
517        }
518    }
519
520    const fn data_value_width(&self) -> usize {
521        match self.radix_format {
522            RadixFormat::Binary => 8,
523            RadixFormat::Decimal | RadixFormat::Octal => 3,
524            RadixFormat::LowerHex | RadixFormat::UpperHex => 2,
525        }
526    }
527
528    fn format_data_value(&self, byte: u8) -> String {
529        let style = ByteKind::ascii_char_display_style(&byte, self.colored);
530        match self.radix_format {
531            RadixFormat::Binary => {
532                format!(
533                    "{style}{byte:00$b}{style:#}{spacing}",
534                    self.data_value_width(),
535                    spacing = self.value_spacing
536                )
537            }
538            RadixFormat::Decimal => {
539                format!(
540                    "{style}{byte:00$}{style:#}{spacing}",
541                    self.data_value_width(),
542                    spacing = self.value_spacing
543                )
544            }
545            RadixFormat::Octal => {
546                format!(
547                    "{style}{byte:00$o}{style:#}{spacing}",
548                    self.data_value_width(),
549                    spacing = self.value_spacing
550                )
551            }
552            RadixFormat::LowerHex => {
553                format!(
554                    "{style}{byte:00$x}{style:#}{spacing}",
555                    self.data_value_width(),
556                    spacing = self.value_spacing
557                )
558            }
559            RadixFormat::UpperHex => {
560                format!(
561                    "{style}{byte:00$X}{style:#}{spacing}",
562                    self.data_value_width(),
563                    spacing = self.value_spacing
564                )
565            }
566        }
567    }
568
569    fn format_ascii_char(&self, byte: &u8) -> String {
570        // This follows ISO 8859-1.
571        let decoded_char = match byte {
572            // 7-bit ASCII control characters
573            0x00 if self.show_extended_ascii => Some('␀'),
574            0x01 if self.show_extended_ascii => Some('␁'),
575            0x02 if self.show_extended_ascii => Some('␂'),
576            0x03 if self.show_extended_ascii => Some('␃'),
577            0x04 if self.show_extended_ascii => Some('␄'),
578            0x05 if self.show_extended_ascii => Some('␅'),
579            0x06 if self.show_extended_ascii => Some('␆'),
580            0x07 if self.show_extended_ascii => Some('␇'),
581            0x08 if self.show_extended_ascii => Some('␈'),
582            0x09 if self.show_extended_ascii => Some('␉'),
583            0x0A if self.show_extended_ascii => Some('␊'),
584            0x0B if self.show_extended_ascii => Some('␋'),
585            0x0C if self.show_extended_ascii => Some('␌'),
586            0x0D if self.show_extended_ascii => Some('␍'),
587            0x0E if self.show_extended_ascii => Some('␎'),
588            0x0F if self.show_extended_ascii => Some('␏'),
589            0x10 if self.show_extended_ascii => Some('␐'),
590            0x11 if self.show_extended_ascii => Some('␑'),
591            0x12 if self.show_extended_ascii => Some('␒'),
592            0x13 if self.show_extended_ascii => Some('␓'),
593            0x14 if self.show_extended_ascii => Some('␔'),
594            0x15 if self.show_extended_ascii => Some('␕'),
595            0x16 if self.show_extended_ascii => Some('␖'),
596            0x17 if self.show_extended_ascii => Some('␗'),
597            0x18 if self.show_extended_ascii => Some('␘'),
598            0x19 if self.show_extended_ascii => Some('␙'),
599            0x1A if self.show_extended_ascii => Some('␚'),
600            0x1B if self.show_extended_ascii => Some('␛'),
601            0x1C if self.show_extended_ascii => Some('␜'),
602            0x1D if self.show_extended_ascii => Some('␝'),
603            0x1E if self.show_extended_ascii => Some('␞'),
604            0x1F if self.show_extended_ascii => Some('␟'),
605            0x20 if self.show_extended_ascii => Some('␠'),
606            // Printable 7-bit ASCII characters.
607            0x21..=0x7E => Some(*byte as char),
608            // 7-bit ASCII control character
609            0x7F if self.show_extended_ascii => Some('␡'),
610            // 8-bit Extended ASCII characters
611            // 0x80..=0x9F: Not defined by ISO 8859-1.
612            0xA0 if self.show_extended_ascii => Some('⍽'), // NBSP
613            // AD: SHY (soft hyphen)
614            // Printable 8-bit ASCII characters.
615            0xA1..=0xAC | 0xAE..=0xFF => Some(*byte as char),
616            _ => None, // Non-printable characters
617        };
618        let style = ByteKind::ascii_char_display_style(byte, self.colored);
619        if let Some(c) = decoded_char {
620            format!(
621                "{style}{c:0$}{style:#}{spacing}",
622                self.data_value_width(),
623                spacing = self.value_spacing
624            )
625        } else {
626            format!(
627                "{style}{byte:00$X}{style:#}{spacing}",
628                self.data_value_width(),
629                spacing = self.value_spacing
630            )
631        }
632    }
633}
634
635// ------------------------------------------------------------------------------------------------
636// Implementations > DumpColumnWidth
637// ------------------------------------------------------------------------------------------------
638
639impl DumpColumnWidth {
640    #[inline(always)]
641    pub const fn byte_count(&self) -> usize {
642        *self as usize
643    }
644
645    #[inline(always)]
646    pub const fn two_column_byte_count(&self) -> usize {
647        self.byte_count() * 2
648    }
649}