1use std::ops::RangeInclusive;
2
3use std::str::FromStr;
4use unc_primitives_core::hash::CryptoHash;
5use unc_primitives_core::serialize::{base64_display, from_base64};
6
7pub struct Bytes<'a>(pub &'a [u8]);
34
35impl<'a> std::fmt::Display for Bytes<'a> {
36 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 bytes_format(self.0, fmt, false)
38 }
39}
40
41impl<'a> std::fmt::Debug for Bytes<'a> {
42 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 bytes_format(self.0, fmt, false)
44 }
45}
46
47impl<'a> Bytes<'a> {
48 pub fn from_str(s: &str) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
56 if s.len() >= 2 && s.starts_with('`') && s.ends_with('`') {
57 let hash = CryptoHash::from_str(&s[1..s.len().checked_sub(1).expect("s.len() >= 2 ")])?;
59 Ok(hash.as_bytes().to_vec())
60 } else if s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'') {
61 Ok(s[1..s.len().checked_sub(1).expect("s.len() >= 2 ")].as_bytes().to_vec())
63 } else {
64 from_base64(s).map_err(|err| err.into())
66 }
67 }
68}
69
70pub struct AbbrBytes<T>(pub T);
78
79impl<'a> std::fmt::Debug for AbbrBytes<&'a [u8]> {
80 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 truncated_bytes_format(self.0, fmt)
82 }
83}
84
85impl<'a> std::fmt::Debug for AbbrBytes<&'a Vec<u8>> {
86 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 AbbrBytes(self.0.as_slice()).fmt(fmt)
88 }
89}
90
91impl<'a> std::fmt::Debug for AbbrBytes<Option<&'a [u8]>> {
92 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self.0 {
94 None => fmt.write_str("None"),
95 Some(bytes) => truncated_bytes_format(bytes, fmt),
96 }
97 }
98}
99
100impl<'a> std::fmt::Display for AbbrBytes<&'a [u8]> {
101 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 truncated_bytes_format(self.0, fmt)
103 }
104}
105
106impl<'a> std::fmt::Display for AbbrBytes<&'a Vec<u8>> {
107 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 AbbrBytes(self.0.as_slice()).fmt(fmt)
109 }
110}
111
112impl<'a> std::fmt::Display for AbbrBytes<Option<&'a [u8]>> {
113 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 match self.0 {
115 None => fmt.write_str("None"),
116 Some(bytes) => truncated_bytes_format(bytes, fmt),
117 }
118 }
119}
120
121pub struct StorageKey<'a>(pub &'a [u8]);
150
151impl<'a> std::fmt::Display for StorageKey<'a> {
152 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 bytes_format(self.0, fmt, true)
154 }
155}
156
157impl<'a> std::fmt::Debug for StorageKey<'a> {
158 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 bytes_format(self.0, fmt, true)
160 }
161}
162
163pub struct Slice<'a, T>(pub &'a [T]);
169
170impl<'a, T: std::fmt::Debug> std::fmt::Debug for Slice<'a, T> {
171 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 let slice = self.0;
173
174 struct Ellipsis;
175
176 impl std::fmt::Debug for Ellipsis {
177 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 fmt.write_str("…")
179 }
180 }
181
182 if let [a, b, _c, .., _x, y, z] = slice {
183 write!(fmt, "({})", slice.len())?;
184 fmt.debug_list().entry(a).entry(b).entry(&Ellipsis).entry(y).entry(z).finish()
185 } else {
186 std::fmt::Debug::fmt(&slice, fmt)
187 }
188 }
189}
190
191fn bytes_format(
196 bytes: &[u8],
197 fmt: &mut std::fmt::Formatter<'_>,
198 consider_hash: bool,
199) -> std::fmt::Result {
200 if consider_hash && bytes.len() == 32 {
201 write!(fmt, "`{}`", CryptoHash(bytes.try_into().unwrap()))
202 } else if bytes.iter().all(|ch| 0x20 <= *ch && *ch <= 0x7E) {
203 let value = unsafe { std::str::from_utf8_unchecked(bytes) };
206 write!(fmt, "'{value}'")
207 } else {
208 std::fmt::Display::fmt(&base64_display(bytes), fmt)
209 }
210}
211
212fn truncated_bytes_format(bytes: &[u8], fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 const PRINTABLE_ASCII: RangeInclusive<u8> = 0x20..=0x7E;
215 const OVERALL_LIMIT: usize = 128;
216 const DISPLAY_ASCII_FULL_LIMIT: usize = OVERALL_LIMIT - 2;
217 const DISPLAY_ASCII_PREFIX_LIMIT: usize = OVERALL_LIMIT - 9;
218 const DISPLAY_BASE64_FULL_LIMIT: usize = OVERALL_LIMIT / 4 * 3;
219 const DISPLAY_BASE64_PREFIX_LIMIT: usize = (OVERALL_LIMIT - 8) / 4 * 3;
220 let len = bytes.len();
221 if bytes.iter().take(DISPLAY_ASCII_FULL_LIMIT).all(|ch| PRINTABLE_ASCII.contains(ch)) {
222 if len <= DISPLAY_ASCII_FULL_LIMIT {
223 let value = unsafe { std::str::from_utf8_unchecked(bytes) };
226 write!(fmt, "'{value}'")
227 } else {
228 let bytes = &bytes[..DISPLAY_ASCII_PREFIX_LIMIT];
229 let value = unsafe { std::str::from_utf8_unchecked(bytes) };
230 write!(fmt, "({len})'{value}'…")
231 }
232 } else if bytes.len() <= DISPLAY_BASE64_FULL_LIMIT {
233 std::fmt::Display::fmt(&base64_display(bytes), fmt)
234 } else {
235 let bytes = &bytes[..DISPLAY_BASE64_PREFIX_LIMIT];
236 let value = base64_display(bytes);
237 write!(fmt, "({len}){value}…")
238 }
239}
240
241#[cfg(test)]
242macro_rules! do_test_bytes_formatting {
243 ($type:ident, $consider_hash:expr, $truncate:expr) => {{
244 #[track_caller]
245 fn test(want: &str, slice: &[u8]) {
246 assert_eq!(want, $type(slice).to_string(), "unexpected formatting");
247 if !$truncate {
248 assert_eq!(&Bytes::from_str(want).expect("decode fail"), slice, "wrong decoding");
249 }
250 }
251
252 #[track_caller]
253 fn test2(cond: bool, want_true: &str, want_false: &str, slice: &[u8]) {
254 test(if cond { want_true } else { want_false }, slice);
255 }
256
257 test("''", b"");
258 test("'foo'", b"foo");
259 test("'foo bar'", b"foo bar");
260 test("WsOzxYJ3", "Zółw".as_bytes());
261 test("EGZvbyBiYXI=", b"\x10foo bar");
262 test("f2ZvbyBiYXI=", b"\x7Ffoo bar");
263
264 test2(
265 $consider_hash,
266 "`11111111111111111111111111111111`",
267 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
268 &[0; 32],
269 );
270 let hash = CryptoHash::hash_bytes(b"foo");
271 test2(
272 $consider_hash,
273 "`3yMApqCuCjXDWPrbjfR5mjCPTHqFG8Pux1TxQrEM35jj`",
274 "LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=",
275 hash.as_bytes(),
276 );
277
278 let long_str = "rabarbar".repeat(16);
279 test2(
280 $truncate,
281 &format!("(128)'{}'…", &long_str[..119]),
282 &format!("'{long_str}'"),
283 long_str.as_bytes(),
284 );
285 test2(
286 $truncate,
287 &format!("(102){}…", &"deadbeef".repeat(15)),
288 &"deadbeef".repeat(17),
289 &b"u\xe6\x9dm\xe7\x9f".repeat(17),
290 );
291 }};
292}
293
294#[test]
295fn test_bytes() {
296 do_test_bytes_formatting!(Bytes, false, false);
297}
298
299#[test]
300fn test_truncated_bytes() {
301 do_test_bytes_formatting!(AbbrBytes, false, true);
302}
303
304#[test]
305fn test_storage_key() {
306 do_test_bytes_formatting!(StorageKey, true, false);
307}
308
309#[test]
310fn test_slice() {
311 macro_rules! test {
312 ($want:literal, $fmt:literal, $len:expr) => {
313 assert_eq!(
314 $want,
315 format!($fmt, Slice(&[0u8, 11, 22, 33, 44, 55, 66, 77, 88, 99][..$len]))
316 )
317 };
318 }
319
320 test!("[]", "{:?}", 0);
321 test!("[0, 11, 22, 33]", "{:?}", 4);
322 test!("[0, b, 16, 21]", "{:x?}", 4);
323 test!("(10)[0, 11, …, 88, 99]", "{:?}", 10);
324 test!("(10)[0, b, …, 58, 63]", "{:x?}", 10);
325}