gospel_dump/
lib.rs

1/*!
2
3Pretty nice printing of hex dumps.
4
5This is intended to be used alongside `gospel` with the `Reader::dump` function, but can be used with
6plain byte slices with [`Dump::new`].
7
8See [`Dump`] for main documentation.
9
10*/
11
12use std::fmt;
13
14/// A hex dump of a byte slice.
15///
16/// This can be printed with `{:x}`, `{:X}`, or `{:b}` specifiers, to show the dump in the
17/// specified format. In addition, it supports the following format specifiers:
18///
19/// - `#`: enable ANSI color escape codes
20/// - `N`: number of lines to write (defaults to printing until the end)
21/// - `.M`: number of bytes per line (defaults to as many as fits inside 240 terminal columns)
22#[must_use]
23pub struct Dump<'a> {
24	start: usize,
25	end: usize,
26	data: &'a [u8],
27	num_width_as: usize,
28	#[allow(clippy::type_complexity)]
29	preview: Option<Box<dyn Fn(&[u8]) -> String + 'static>>,
30}
31
32impl<'a> Dump<'a> {
33	/// Creates a dump of a byte slice.
34	pub fn new(data: &'a [u8]) -> Self {
35		Self {
36			start: 0,
37			end: data.len(),
38			data,
39			num_width_as: data.len(),
40			preview: Some(Box::new(|a| String::from_utf8_lossy(a).into_owned()))
41		}
42	}
43
44	/// Sets the starting point of the dump.
45	pub fn start(self, start: usize) -> Self {
46		Self {
47			start,
48			..self
49		}
50	}
51
52	/// Sets the end point of the dump.
53	///
54	/// Options passed when printing will have priority.
55	pub fn end(self, end: usize) -> Self {
56		Self {
57			end,
58			..self
59		}
60	}
61
62	/// Sets the end point of the dump, relative to the starting point.
63	///
64	/// Options passed when printing will have priority.
65	pub fn len(self, len: usize) -> Self {
66		Self {
67			end: self.start + len,
68			..self
69		}
70	}
71
72	/// Sets the number of digits to display for the byte offset to fit the given number.
73	pub fn num_width_as(self, num_width_as: usize) -> Self {
74		Self {
75			num_width_as,
76			..self
77		}
78	}
79
80	/// Set a function to use for displaying the unicode preview.
81	///
82	/// By default, this uses `String::from_utf8_lossy()`.
83	pub fn preview(self, preview: impl Fn(&[u8]) -> String + 'static) -> Self {
84		Self {
85			preview: Some(Box::new(preview)),
86			..self
87		}
88	}
89
90	/// Removes the unicode preview.
91	pub fn no_preview(self) -> Self {
92		Self {
93			preview: None,
94			..self
95		}
96	}
97}
98
99impl fmt::UpperHex for Dump<'_> {
100	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
101		self.print(f, |x, f| write!(f, "{x:02X}"), 2)
102	}
103}
104
105impl fmt::LowerHex for Dump<'_> {
106	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107		self.print(f, |x, f| write!(f, "{x:02x}"), 2)
108	}
109}
110
111impl fmt::Binary for Dump<'_> {
112	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113		self.print(f, |x, f| write!(f, "{x:08b}"), 8)
114	}
115}
116
117impl Dump<'_> {
118	fn print(
119		&self,
120		f: &mut fmt::Formatter,
121		write: impl Fn(u8, &mut fmt::Formatter) -> fmt::Result,
122		cell_width: usize,
123	) -> fmt::Result {
124		const SCREEN_WIDTH: usize = 240;
125
126		let num_width = if self.num_width_as == 0 {
127			0
128		} else {
129			format!("{:X}", self.num_width_as).len()
130		};
131
132		let has_text = !f.sign_minus();
133		let lines = f.width().unwrap_or(usize::MAX);
134		let width = f.precision().unwrap_or_else(|| {
135			let c = cell_width + 1 + usize::from(has_text);
136			let w = num_width + usize::from(num_width != 0) + usize::from(has_text);
137			(SCREEN_WIDTH - w) / c / 4 * 4
138		}).max(1);
139
140		if self.data[self.start..self.end].is_empty() || lines == 0 {
141			let pos = self.start;
142			if num_width > 0 {
143				let s = format!("{:X}", pos);
144				if s.len() < num_width {
145					sgr(f, "2;33")?;
146					for _ in s.len()..num_width {
147						f.write_str("0")?;
148					}
149				}
150				sgr(f, "33")?;
151				f.write_str(&s)?;
152				sgr(f, "")?;
153				f.write_str(" ")?;
154			}
155			f.write_str("\n")?;
156		}
157
158		for (i, chunk) in self.data[self.start..self.end].chunks(width).take(lines).enumerate() {
159			let pos = self.start + i * width;
160
161			if num_width > 0 {
162				let s = format!("{:X}", pos);
163				if s.len() < num_width {
164					sgr(f, "2;33")?;
165					for _ in s.len()..num_width {
166						f.write_str("0")?;
167					}
168				}
169				sgr(f, "33")?;
170				f.write_str(&s)?;
171				sgr(f, "")?;
172				f.write_str(" ")?;
173			}
174
175			let mut prev_color = "";
176			for (i, &b) in chunk.iter().enumerate() {
177				if i != 0 {
178					f.write_str(" ")?;
179				}
180
181				let color = match b {
182					0x00        => "2",
183					0xFF        => "38;5;9",
184					0x20..=0x7E => "38;5;10",
185					_           => "",
186				};
187
188				if prev_color != color {
189					sgr(f, color)?;
190					prev_color = color;
191				}
192				write(b, f)?;
193			}
194			sgr(f, "")?;
195
196			if let Some(preview) = &self.preview {
197				for _ in chunk.len()..width {
198					f.write_str("   ")?;
199				}
200				f.write_str(" ▏")?;
201
202				for char in preview(chunk).chars() {
203					let (color, char) = match char {
204						'�' => ("2", '·'),
205						c if c.is_control() => ("38;5;8", '·'),
206						c => ("", c),
207					};
208					if prev_color != color {
209						sgr(f, color)?;
210						prev_color = color;
211					}
212					write!(f, "{char}")?;
213				}
214
215				sgr(f, "")?;
216			}
217			f.write_str("\n")?;
218		}
219
220		Ok(())
221	}
222}
223
224fn sgr(f: &mut fmt::Formatter, arg: &str) -> fmt::Result {
225	if f.alternate() {
226		write!(f, "\x1B[0;{arg}m")
227	} else {
228		Ok(())
229	}
230}