unc_fmt/
lib.rs

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
7/// A wrapper for bytes slice which tries to guess best way to format it.
8///
9/// If the slice contains printable ASCII characters only, it’s represented as
10/// a string surrounded by single quotes (as a consequence, empty value is
11/// converted to pair of single quotes).  Otherwise, it converts the value into
12/// base64.
13///
14/// The intended usage for this type is when trying to format binary data whose
15/// structure isn’t known to the caller.  For example, when generating debugging
16/// or tracing data at database layer where everything is just slices of bytes.
17/// At higher levels of abstractions, if the structure of the data is known,
18/// it’s usually better to format data in a way that makes sense for the given
19/// type.
20///
21/// The type can be used as with `tracing::info!` and similar calls.  For
22/// example:
23///
24/// ```ignore
25/// tracing::trace!(target: "state",
26///                 db_op = "insert",
27///                 key = %unc_fmt::Bytes(key),
28///                 size = value.len())
29/// ```
30///
31/// See also [`StorageKey`] which tries to guess if the data is not a crypto
32/// hash.
33pub 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    /// Reverses `bytes_format` to allow decoding `Bytes` written with `Display`.
49    ///
50    /// This looks  similar to `FromStr` but due to lifetime constraints on
51    /// input and output, the trait cannot be implemented.
52    ///
53    /// Error: Returns an error when the input does not look like an output from
54    /// `bytes_format`.
55    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            // hash encoded as base58
58            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            // plain string
62            Ok(s[1..s.len().checked_sub(1).expect("s.len() >= 2 ")].as_bytes().to_vec())
63        } else {
64            // encoded with base64
65            from_base64(s).map_err(|err| err.into())
66        }
67    }
68}
69
70/// A wrapper for bytes slice which tries to guess best way to format it
71/// truncating the value if it’s too long.
72///
73/// Behaves like [`Bytes`] but truncates the formatted string to around 128
74/// characters.  If the value is longer then that, the length of the value in
75/// bytes is included at the beginning and ellipsis is included at the end of
76/// the value.
77pub 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
121/// A wrapper for bytes slice which tries to guess best way to format it.
122///
123/// If the slice is exactly 32-byte long, it’s assumed to be a hash and is
124/// converted into base58 and printed surrounded by backtics.  Otherwise,
125/// behaves like [`Bytes`] representing the data as string if it contains ASCII
126/// printable bytes only or base64 otherwise.
127///
128/// The motivation for such choices is that we only ever use base58 to format
129/// hashes which are 32-byte long.  It’s therefore not useful to use it for any
130/// other types of keys.
131///
132/// The intended usage for this type is when trying to format binary data whose
133/// structure isn’t known to the caller.  For example, when generating debugging
134/// or tracing data at database layer where everything is just slices of bytes.
135/// At higher levels of abstractions, if the structure of the data is known,
136/// it’s usually better to format data in a way that makes sense for the given
137/// type.
138///
139/// The type can be used as with `tracing::info!` and similar calls.  For
140/// example:
141///
142/// ```ignore
143/// tracing::info!(target: "store",
144///                op = "set",
145///                col = %col,
146///                key = %unc_fmt::StorageKey(key),
147///                size = value.len())
148/// ```
149pub 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
163/// A wrapper for slices which formats the slice limiting the length.
164///
165/// If the slice has no more than five elements, it’s printed in full.
166/// Otherwise, only the first two and last two elements are printed to limit the
167/// length of the formatted value.
168pub 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
191/// Implementation of [`Bytes`] and [`StorageKey`] formatting.
192///
193/// If the `consider_hash` argument is false, formats bytes as described in
194/// [`Bytes`].  If it’s true, formats the bytes as described in [`StorageKey`].
195fn 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        // SAFETY: We’ve just checked that the value contains ASCII
204        // characters only.
205        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
212/// Implementation of [`AbbrBytes`].
213fn 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            // SAFETY: We’ve just checked that the value contains ASCII
224            // characters only.
225            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}