Skip to main content

workflow_log/hex/
format.rs

1use std::io;
2use std::ops::Range;
3
4use termcolor::{Buffer, BufferWriter, Color, ColorChoice, WriteColor};
5
6use super::byte_mapping;
7use super::color::{ColorRange, ColorlessString, Colors, Spec};
8
9/// The HexView struct represents the configuration of how to display the data.
10pub struct HexView<'a> {
11    address_offset: usize,
12    codepage: &'a [char],
13    data: &'a [u8],
14    replacement_character: char,
15    row_width: usize,
16    colors: Colors,
17    force_color: bool,
18}
19
20macro_rules! color {
21    ($fmt:ident, $color:ident, $str:expr) => {{
22        $fmt.set_color(&$color)?;
23        write!($fmt, "{}", $str)?;
24        $fmt.reset()
25    }};
26}
27
28impl<'a> HexView<'a> {
29    /// Prints the hextable to stdout. If any colors were given during construction, the specified ranges will be printed in color.
30    pub fn print(&self) -> io::Result<()> {
31        let cc = if self.force_color || std::io::IsTerminal::is_terminal(&std::io::stdout()) {
32            ColorChoice::Auto
33        } else {
34            ColorChoice::Never
35        };
36        let writer = BufferWriter::stdout(cc);
37        let mut buffer: Buffer = writer.buffer();
38        self.fmt(&mut buffer)?;
39        writer.print(&buffer)?;
40        Ok(())
41    }
42
43    /// Constructs a new HexView for the given data without offset and using codepage 850, a row width
44    /// of 16 and `.` as replacement character.
45    pub fn new(data: &[u8]) -> HexView {
46        HexView {
47            address_offset: 0,
48            codepage: &byte_mapping::CODEPAGE_0850,
49            data: data,
50            replacement_character: '.',
51            row_width: 16,
52            colors: Colors::new(),
53            force_color: false,
54        }
55    }
56
57    pub fn fmt<W: WriteColor>(&self, buffer: &mut W) -> io::Result<()> {
58        let begin_padding = calculate_begin_padding(self.address_offset, self.row_width);
59        let end_padding = calculate_end_padding(begin_padding + self.data.len(), self.row_width);
60        let mut address = self.address_offset - begin_padding;
61        let mut offset = 0;
62        let mut color_range = ColorRange::new(&self.colors);
63        let mut separator = "";
64
65        if self.data.len() + begin_padding + end_padding <= self.row_width {
66            fmt_line(
67                buffer,
68                address,
69                &self.codepage,
70                self.replacement_character,
71                &self.data,
72                &mut color_range,
73                &Padding::new(begin_padding, end_padding),
74            )?;
75            return Ok(());
76        }
77
78        if begin_padding != 0 {
79            let slice = &self.data[offset..offset + self.row_width - begin_padding];
80            fmt_line(
81                buffer,
82                address,
83                &self.codepage,
84                self.replacement_character,
85                &slice,
86                &mut color_range,
87                &Padding::from_left(begin_padding),
88            )?;
89            offset += self.row_width - begin_padding;
90            address += self.row_width;
91            separator = "\n";
92            color_range.update_offset(offset);
93        }
94
95        while offset + (self.row_width - 1) < self.data.len() {
96            let slice = &self.data[offset..offset + self.row_width];
97            write!(buffer, "{}", separator)?;
98            fmt_line(
99                buffer,
100                address,
101                &self.codepage,
102                self.replacement_character,
103                &slice,
104                &mut color_range,
105                &Padding::default(),
106            )?;
107            offset += self.row_width;
108            address += self.row_width;
109            separator = "\n";
110            color_range.update_offset(offset);
111        }
112
113        if end_padding != 0 {
114            let slice = &self.data[offset..];
115            writeln!(buffer, "")?;
116            fmt_line(
117                buffer,
118                address,
119                &self.codepage,
120                self.replacement_character,
121                &slice,
122                &mut color_range,
123                &Padding::from_right(end_padding),
124            )?;
125        }
126        Ok(())
127    }
128}
129
130/// A builder for the [HexView](struct.HexView.html) struct.
131pub struct HexViewBuilder<'a> {
132    hex_view: HexView<'a>,
133}
134
135impl<'a> HexViewBuilder<'a> {
136    /// Constructs a new HexViewBuilder for the given data.
137    pub fn new(data: &[u8]) -> HexViewBuilder {
138        HexViewBuilder {
139            hex_view: HexView::new(&data),
140        }
141    }
142
143    /// Configures the address offset of the HexView under construction.
144    pub fn address_offset(mut self, offset: usize) -> HexViewBuilder<'a> {
145        self.hex_view.address_offset = offset;
146        self
147    }
148
149    /// Forces any color data to be printed in `print`, even if redirected to a file or pipe.
150    pub fn force_color(mut self) -> Self {
151        self.hex_view.force_color = true;
152        self
153    }
154
155    /// Configures the codepage of the HexView under construction.
156    pub fn codepage<'b: 'a>(mut self, codepage: &'b [char]) -> HexViewBuilder<'a> {
157        self.hex_view.codepage = codepage;
158        self
159    }
160
161    /// Configures the replacement character of the HexView under construction.
162    ///
163    /// The replacement character is the character that will be used for nonprintable
164    /// characters in the codepage.
165    pub fn replacement_character(mut self, ch: char) -> HexViewBuilder<'a> {
166        self.hex_view.replacement_character = ch;
167        self
168    }
169
170    /// Configures the row width of the HexView under construction.
171    pub fn row_width(mut self, width: usize) -> HexViewBuilder<'a> {
172        self.hex_view.row_width = width;
173        self
174    }
175    /// Adds the vector of `colors` to the range color printer
176    pub fn add_colors(mut self, colors: Colors) -> HexViewBuilder<'a> {
177        self.hex_view.colors.extend(colors);
178        self
179    }
180    /// Adds the `color` to the given `range`, using a more ergonomic API
181    pub fn add_color(mut self, color: &str, range: Range<usize>) -> HexViewBuilder<'a> {
182        use std::str::FromStr;
183        self.hex_view.colors.push((
184            Spec::new()
185                .set_fg(Some(Color::from_str(color).unwrap()))
186                .clone(),
187            range,
188        ));
189        self
190    }
191    /// Constructs the HexView.
192    pub fn finish(mut self) -> HexView<'a> {
193        self.hex_view
194            .colors
195            .sort_by(|&(_, ref r1), &(_, ref r2)| r1.start.cmp(&r2.start));
196        self.hex_view
197    }
198}
199
200#[derive(Default)]
201struct Padding {
202    left: usize,
203    right: usize,
204}
205
206impl Padding {
207    fn new(left_padding: usize, right_padding: usize) -> Padding {
208        Padding {
209            left: left_padding,
210            right: right_padding,
211        }
212    }
213
214    fn from_left(left_padding: usize) -> Padding {
215        Padding {
216            left: left_padding,
217            right: 0,
218        }
219    }
220
221    fn from_right(right_padding: usize) -> Padding {
222        Padding {
223            left: 0,
224            right: right_padding,
225        }
226    }
227}
228
229fn fmt_bytes_as_hex<W: WriteColor>(
230    f: &mut W,
231    bytes: &[u8],
232    color_range: &ColorRange,
233    padding: &Padding,
234) -> io::Result<()> {
235    let mut separator = "";
236
237    for _ in 0..padding.left {
238        write!(f, "{}  ", separator)?;
239        separator = " ";
240    }
241
242    for (i, byte) in bytes.iter().enumerate() {
243        match color_range.get(i) {
244            Some(rgb) => {
245                write!(f, "{}", separator)?;
246                color!(f, rgb, format!("{:02X}", byte))?;
247            }
248            None => write!(f, "{}{:02X}", separator, byte)?,
249        }
250        separator = " ";
251    }
252
253    for _ in 0..padding.right {
254        write!(f, "{}  ", separator)?;
255        separator = " ";
256    }
257
258    Ok(())
259}
260
261fn fmt_bytes_as_char<W: WriteColor>(
262    f: &mut W,
263    cp: &[char],
264    repl_char: char,
265    bytes: &[u8],
266    color_range: &ColorRange,
267    padding: &Padding,
268) -> io::Result<()> {
269    for _ in 0..padding.left {
270        write!(f, " ")?;
271    }
272
273    for (i, &byte) in bytes.iter().enumerate() {
274        let byte = byte_mapping::as_char(byte, cp, repl_char);
275        match color_range.get(i) {
276            Some(rgb) => {
277                color!(f, rgb, format!("{}", byte))?;
278            }
279            _ => write!(f, "{}", byte)?,
280        }
281    }
282
283    for _ in 0..padding.right {
284        write!(f, " ")?;
285    }
286
287    Ok(())
288}
289
290fn fmt_line<W: WriteColor>(
291    f: &mut W,
292    address: usize,
293    cp: &[char],
294    repl_char: char,
295    bytes: &[u8],
296    color_range: &mut ColorRange,
297    padding: &Padding,
298) -> io::Result<()> {
299    write!(f, "{:0width$X}", address, width = 8)?;
300
301    write!(f, "  ")?;
302    fmt_bytes_as_hex(f, bytes, &color_range, &padding)?;
303    write!(f, "  ")?;
304
305    write!(f, "| ")?;
306    fmt_bytes_as_char(f, cp, repl_char, bytes, &color_range, &padding)?;
307    write!(f, " |")?;
308
309    Ok(())
310}
311
312fn calculate_begin_padding(address_offset: usize, row_width: usize) -> usize {
313    debug_assert!(
314        row_width != 0,
315        "A zero row width is can not be used to calculate the begin padding"
316    );
317    address_offset % row_width
318}
319
320fn calculate_end_padding(data_size: usize, row_width: usize) -> usize {
321    debug_assert!(
322        row_width != 0,
323        "A zero row width is can not be used to calculate the end padding"
324    );
325    (row_width - data_size % row_width) % row_width
326}
327
328impl<'a> std::fmt::Display for HexView<'a> {
329    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
330        if self.row_width == 0 {
331            write!(f, "Invalid HexView::width")?;
332            return Err(std::fmt::Error);
333        }
334        let mut string = ColorlessString(String::new());
335        match self.fmt(&mut string) {
336            Ok(()) => {
337                write!(f, "{}", string.0)
338            }
339            Err(e) => write!(f, "{}", e),
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use std;
348
349    #[test]
350    fn test_begin_padding() {
351        // Rust 1.13 needs the fully qualified name here
352        assert_eq!(super::calculate_begin_padding(0, 16), 0);
353        assert_eq!(super::calculate_begin_padding(16, 16), 0);
354        assert_eq!(super::calculate_begin_padding(54, 16), 6);
355    }
356
357    #[test]
358    fn a_full_line_is_formatted_as_expected() {
359        let data: Vec<u8> = (0x40..0x40 + 0xF + 1).collect();
360
361        let row_view = HexViewBuilder::new(&data).row_width(data.len()).finish();
362
363        let result = format!("{}", row_view);
364
365        assert_eq!(
366            result,
367            "00000000  40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F  | @ABCDEFGHIJKLMNO |"
368        );
369    }
370
371    #[test]
372    fn an_incomplete_line_is_padded_on_the_right() {
373        let data = ['a' as u8; 10];
374
375        let row_view = HexViewBuilder::new(&data).row_width(16).finish();
376
377        let result = format!("{}", row_view);
378
379        assert_eq!(
380            result,
381            "00000000  61 61 61 61 61 61 61 61 61 61                    | aaaaaaaaaa       |"
382        );
383    }
384
385    #[test]
386    fn an_unaligned_address_causes_padded_on_the_left() {
387        let data = ['a' as u8; 11];
388
389        let row_view = HexViewBuilder::new(&data)
390            .address_offset(5)
391            .row_width(16)
392            .finish();
393
394        let result = format!("{}", row_view);
395        println!("{}", result);
396
397        assert_eq!(
398            result,
399            "00000000                 61 61 61 61 61 61 61 61 61 61 61  |      aaaaaaaaaaa |"
400        );
401    }
402
403    #[test]
404    fn an_unaligned_incomplete_line_causes_padding_on_both_sides() {
405        let data = ['a' as u8; 8];
406
407        let row_view = HexViewBuilder::new(&data)
408            .address_offset(5)
409            .row_width(16)
410            .finish();
411
412        let result = format!("{}", row_view);
413        println!("{}", result);
414
415        assert_eq!(
416            result,
417            "00000000                 61 61 61 61 61 61 61 61           |      aaaaaaaa    |"
418        );
419    }
420
421    #[test]
422    fn decreasing_the_row_width_increases_the_total_character_count() {
423        let data: Vec<u8> = (0..64).collect();
424
425        let short_row_view = HexViewBuilder::new(&data).row_width(1).finish();
426        let long_row_view = HexViewBuilder::new(&data).row_width(16).finish();
427
428        let short_row_result = format!("{}", short_row_view);
429        let long_row_result = format!("{}", long_row_view);
430
431        assert!(long_row_result.len() < short_row_result.len());
432    }
433
434    #[test]
435    fn the_address_offset_is_zero_by_default() {
436        let data = [99; 16];
437
438        let row_view = HexViewBuilder::new(&data).row_width(16).finish();
439
440        let result = format!("{}", row_view);
441        let address_offset_str = format!("{:X}", 0);
442
443        assert!(result.contains(&address_offset_str));
444    }
445
446    #[test]
447    fn the_address_offset_is_used_when_given() {
448        let data = [0; 16];
449
450        let address_offset = data.len() * 10;
451        let row_view = HexViewBuilder::new(&data)
452            .row_width(16)
453            .address_offset(address_offset)
454            .finish();
455
456        let result = format!("{}", row_view);
457        let address_offset_str = format!("{:X}", address_offset);
458
459        assert!(result.contains(&address_offset_str));
460    }
461
462    #[test]
463    fn the_address_offset_increases_by_the_row_width_for_each_row() {
464        let data = [0; 16 * 5];
465
466        let address_offset = data.len() * 10;
467        let row_view = HexViewBuilder::new(&data)
468            .row_width(16)
469            .address_offset(address_offset)
470            .finish();
471
472        let result = format!("{}", row_view);
473        let row_2_address_offset_str = format!("{:X}", address_offset + 2 * row_view.row_width);
474        let row_4_address_offset_str = format!("{:X}", address_offset + 4 * row_view.row_width);
475
476        assert!(result.contains(&row_2_address_offset_str));
477        assert!(result.contains(&row_4_address_offset_str));
478    }
479
480    #[test]
481    fn there_is_no_superfluous_whitespace() {
482        let data = [0; 17];
483
484        let one_line_result = format!("{}", HexViewBuilder::new(&data[0..16]).finish());
485        let two_line_result = format!("{}", HexViewBuilder::new(&data[0..17]).finish());
486
487        println!("{}", one_line_result);
488        println!("{}", two_line_result);
489
490        assert_eq!(one_line_result, one_line_result.trim());
491        assert_eq!(two_line_result, two_line_result.trim());
492    }
493
494    #[test]
495    fn the_row_width_is_16_by_default() {
496        let data = [0; 17];
497
498        let one_line_result = format!("{}", HexViewBuilder::new(&data[0..16]).finish());
499        let two_line_result = format!("{}", HexViewBuilder::new(&data[0..17]).finish());
500
501        println!("{}", one_line_result);
502        println!("{}", two_line_result);
503
504        assert_eq!(1, one_line_result.lines().count());
505        assert_eq!(2, two_line_result.lines().count());
506    }
507
508    #[test]
509    fn the_replacement_character_is_dot_by_default() {
510        let data = [0; 1];
511        let empty_cp = [];
512
513        let result = format!(
514            "{}",
515            HexViewBuilder::new(&data).codepage(&empty_cp).finish()
516        );
517
518        println!("{}", result);
519
520        assert!(result.contains('.'));
521    }
522
523    #[test]
524    fn the_replacement_character_can_be_changed() {
525        let data = [0; 1];
526        let empty_cp = [];
527
528        let result = format!(
529            "{}",
530            HexViewBuilder::new(&data)
531                .codepage(&empty_cp)
532                .replacement_character(std::char::REPLACEMENT_CHARACTER)
533                .finish()
534        );
535
536        println!("{}", result);
537
538        assert!(result.contains(std::char::REPLACEMENT_CHARACTER));
539    }
540
541    #[test]
542    fn all_characters_can_be_printed() {
543        let data: Vec<u8> = (0u16..256u16).map(|v| v as u8).collect();
544
545        let dump_view = HexViewBuilder::new(&data)
546            .address_offset(20)
547            .row_width(8)
548            .finish();
549
550        let result = format!("{}", dump_view);
551        println!("{}", result);
552
553        assert!(!result.is_empty());
554    }
555}