use std::ops::RangeInclusive;
use unc_primitives_core::hash::CryptoHash;
use unc_primitives_core::serialize::{base64_display, from_base64};
use std::str::FromStr;
pub struct Bytes<'a>(pub &'a [u8]);
impl<'a> std::fmt::Display for Bytes<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bytes_format(self.0, fmt, false)
}
}
impl<'a> std::fmt::Debug for Bytes<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bytes_format(self.0, fmt, false)
}
}
impl<'a> Bytes<'a> {
pub fn from_str(s: &str) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
if s.len() >= 2 && s.starts_with('`') && s.ends_with('`') {
let hash = CryptoHash::from_str(&s[1..s.len().checked_sub(1).expect("s.len() >= 2 ")])?;
Ok(hash.as_bytes().to_vec())
} else if s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'') {
Ok(s[1..s.len().checked_sub(1).expect("s.len() >= 2 ")].as_bytes().to_vec())
} else {
from_base64(s).map_err(|err| err.into())
}
}
}
pub struct AbbrBytes<T>(pub T);
impl<'a> std::fmt::Debug for AbbrBytes<&'a [u8]> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
truncated_bytes_format(self.0, fmt)
}
}
impl<'a> std::fmt::Debug for AbbrBytes<&'a Vec<u8>> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
AbbrBytes(self.0.as_slice()).fmt(fmt)
}
}
impl<'a> std::fmt::Debug for AbbrBytes<Option<&'a [u8]>> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
None => fmt.write_str("None"),
Some(bytes) => truncated_bytes_format(bytes, fmt),
}
}
}
impl<'a> std::fmt::Display for AbbrBytes<&'a [u8]> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
truncated_bytes_format(self.0, fmt)
}
}
impl<'a> std::fmt::Display for AbbrBytes<&'a Vec<u8>> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
AbbrBytes(self.0.as_slice()).fmt(fmt)
}
}
impl<'a> std::fmt::Display for AbbrBytes<Option<&'a [u8]>> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
None => fmt.write_str("None"),
Some(bytes) => truncated_bytes_format(bytes, fmt),
}
}
}
pub struct StorageKey<'a>(pub &'a [u8]);
impl<'a> std::fmt::Display for StorageKey<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bytes_format(self.0, fmt, true)
}
}
impl<'a> std::fmt::Debug for StorageKey<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
bytes_format(self.0, fmt, true)
}
}
pub struct Slice<'a, T>(pub &'a [T]);
impl<'a, T: std::fmt::Debug> std::fmt::Debug for Slice<'a, T> {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let slice = self.0;
struct Ellipsis;
impl std::fmt::Debug for Ellipsis {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.write_str("…")
}
}
if let [a, b, _c, .., _x, y, z] = slice {
write!(fmt, "({})", slice.len())?;
fmt.debug_list().entry(a).entry(b).entry(&Ellipsis).entry(y).entry(z).finish()
} else {
std::fmt::Debug::fmt(&slice, fmt)
}
}
}
fn bytes_format(
bytes: &[u8],
fmt: &mut std::fmt::Formatter<'_>,
consider_hash: bool,
) -> std::fmt::Result {
if consider_hash && bytes.len() == 32 {
write!(fmt, "`{}`", CryptoHash(bytes.try_into().unwrap()))
} else if bytes.iter().all(|ch| 0x20 <= *ch && *ch <= 0x7E) {
let value = unsafe { std::str::from_utf8_unchecked(bytes) };
write!(fmt, "'{value}'")
} else {
std::fmt::Display::fmt(&base64_display(bytes), fmt)
}
}
fn truncated_bytes_format(bytes: &[u8], fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const PRINTABLE_ASCII: RangeInclusive<u8> = 0x20..=0x7E;
const OVERALL_LIMIT: usize = 128;
const DISPLAY_ASCII_FULL_LIMIT: usize = OVERALL_LIMIT - 2;
const DISPLAY_ASCII_PREFIX_LIMIT: usize = OVERALL_LIMIT - 9;
const DISPLAY_BASE64_FULL_LIMIT: usize = OVERALL_LIMIT / 4 * 3;
const DISPLAY_BASE64_PREFIX_LIMIT: usize = (OVERALL_LIMIT - 8) / 4 * 3;
let len = bytes.len();
if bytes.iter().take(DISPLAY_ASCII_FULL_LIMIT).all(|ch| PRINTABLE_ASCII.contains(ch)) {
if len <= DISPLAY_ASCII_FULL_LIMIT {
let value = unsafe { std::str::from_utf8_unchecked(bytes) };
write!(fmt, "'{value}'")
} else {
let bytes = &bytes[..DISPLAY_ASCII_PREFIX_LIMIT];
let value = unsafe { std::str::from_utf8_unchecked(bytes) };
write!(fmt, "({len})'{value}'…")
}
} else if bytes.len() <= DISPLAY_BASE64_FULL_LIMIT {
std::fmt::Display::fmt(&base64_display(bytes), fmt)
} else {
let bytes = &bytes[..DISPLAY_BASE64_PREFIX_LIMIT];
let value = base64_display(bytes);
write!(fmt, "({len}){value}…")
}
}
#[cfg(test)]
macro_rules! do_test_bytes_formatting {
($type:ident, $consider_hash:expr, $truncate:expr) => {{
#[track_caller]
fn test(want: &str, slice: &[u8]) {
assert_eq!(want, $type(slice).to_string(), "unexpected formatting");
if !$truncate {
assert_eq!(&Bytes::from_str(want).expect("decode fail"), slice, "wrong decoding");
}
}
#[track_caller]
fn test2(cond: bool, want_true: &str, want_false: &str, slice: &[u8]) {
test(if cond { want_true } else { want_false }, slice);
}
test("''", b"");
test("'foo'", b"foo");
test("'foo bar'", b"foo bar");
test("WsOzxYJ3", "Zółw".as_bytes());
test("EGZvbyBiYXI=", b"\x10foo bar");
test("f2ZvbyBiYXI=", b"\x7Ffoo bar");
test2(
$consider_hash,
"`11111111111111111111111111111111`",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
&[0; 32],
);
let hash = CryptoHash::hash_bytes(b"foo");
test2(
$consider_hash,
"`3yMApqCuCjXDWPrbjfR5mjCPTHqFG8Pux1TxQrEM35jj`",
"LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=",
hash.as_bytes(),
);
let long_str = "rabarbar".repeat(16);
test2(
$truncate,
&format!("(128)'{}'…", &long_str[..119]),
&format!("'{long_str}'"),
long_str.as_bytes(),
);
test2(
$truncate,
&format!("(102){}…", &"deadbeef".repeat(15)),
&"deadbeef".repeat(17),
&b"u\xe6\x9dm\xe7\x9f".repeat(17),
);
}};
}
#[test]
fn test_bytes() {
do_test_bytes_formatting!(Bytes, false, false);
}
#[test]
fn test_truncated_bytes() {
do_test_bytes_formatting!(AbbrBytes, false, true);
}
#[test]
fn test_storage_key() {
do_test_bytes_formatting!(StorageKey, true, false);
}
#[test]
fn test_slice() {
macro_rules! test {
($want:literal, $fmt:literal, $len:expr) => {
assert_eq!(
$want,
format!($fmt, Slice(&[0u8, 11, 22, 33, 44, 55, 66, 77, 88, 99][..$len]))
)
};
}
test!("[]", "{:?}", 0);
test!("[0, 11, 22, 33]", "{:?}", 4);
test!("[0, b, 16, 21]", "{:x?}", 4);
test!("(10)[0, 11, …, 88, 99]", "{:?}", 10);
test!("(10)[0, b, …, 58, 63]", "{:x?}", 10);
}