1use std::fmt;
13
14#[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 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 pub fn start(self, start: usize) -> Self {
46 Self {
47 start,
48 ..self
49 }
50 }
51
52 pub fn end(self, end: usize) -> Self {
56 Self {
57 end,
58 ..self
59 }
60 }
61
62 pub fn len(self, len: usize) -> Self {
66 Self {
67 end: self.start + len,
68 ..self
69 }
70 }
71
72 pub fn num_width_as(self, num_width_as: usize) -> Self {
74 Self {
75 num_width_as,
76 ..self
77 }
78 }
79
80 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 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}