From d5adcac992e000fc1e027392c8577c557f2462d9 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Wed, 19 Jul 2023 15:45:41 +0200 Subject: [PATCH] =?UTF-8?q?Introduce=20hex::display=20for=20Display?= =?UTF-8?q?=E2=80=99ing=20binary=20data=20in=20hex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit encode method allocates a new string which is wasteful if all one wants to do is display the binary data using format! or related macro. Introduce hex::Display wrapper with hex::display as its constructor which wraps the binary data in an object that implements Display, LowerHex, UpperHex and Debug and formats the data as hex. However, as it uses buffers on stack, it performs no allocations. --- src/lib.rs | 196 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 56 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5a2b93f..c4c37d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -364,55 +364,137 @@ pub fn encode_to_slice>(input: T, output: &mut [u8]) -> Result<() Ok(()) } +/// A wrapper around binary data which formats them as hex when [displaying +/// it][core::fmt::Display]. +/// +/// See [`display`] function. +pub struct Display<'a>(&'a [u8]); + +impl Display<'_> { + fn do_fmt(&self, fmtr: &mut core::fmt::Formatter<'_>, chars: &[u8; 16]) -> core::fmt::Result { + debug_assert!(chars.is_ascii()); + let mut buffer = [core::mem::MaybeUninit::::uninit(); 512]; + for chunk in self.0.chunks(buffer.len() / 2) { + // TODO: Use `array_chunks_mut` instead of `chunks_exact_mut` once + // it stabilises. + for (out, &byte) in buffer.chunks_exact_mut(2).zip(chunk.iter()) { + let (high, low) = byte2hex(byte, chars); + out[0].write(high); + out[1].write(low); + } + let len = chunk.len() * 2; + let chunk = (&buffer[..len]) as *const [_] as *const [u8]; + // SAFETY: we've just filled the buffer up to len with hex digits + // which are ASCII characters. + fmtr.write_str(unsafe { core::str::from_utf8_unchecked(&*chunk) })?; + } + Ok(()) + } +} + +impl core::fmt::Display for Display<'_> { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.do_fmt(fmtr, HEX_CHARS_LOWER) + } +} + +impl core::fmt::LowerHex for Display<'_> { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.do_fmt(fmtr, HEX_CHARS_LOWER) + } +} + +impl core::fmt::UpperHex for Display<'_> { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.do_fmt(fmtr, HEX_CHARS_UPPER) + } +} + +impl core::fmt::Debug for Display<'_> { + #[inline] + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.do_fmt(fmtr, HEX_CHARS_LOWER) + } +} + +/// Wraps the value in an object which can be formatted as hex. +/// +/// The benefit over using [`encode`] is that no memory allocations are done +/// when formatting the value. +/// +/// # Example +/// +/// ``` +/// assert_eq!(hex::display(b"kiwi").to_string(), "6b697769"); +/// assert_eq!(format!("{}", hex::display(b"kiwi")), "6b697769"); +/// assert_eq!(format!("{:X}", hex::display(b"kiwi")), "6B697769"); +/// ``` +#[inline] +pub fn display>(input: &T) -> Display<'_> { + Display(input.as_ref()) +} + #[cfg(test)] mod test { + extern crate alloc; + use super::*; - #[cfg(feature = "alloc")] - use alloc::string::ToString; - #[cfg(feature = "alloc")] - use alloc::vec; use pretty_assertions::assert_eq; #[test] #[cfg(feature = "alloc")] fn test_gen_iter() { - let result = vec![(0, 1), (2, 3)]; + let result = alloc::vec![(0, 1), (2, 3)]; assert_eq!(generate_iter(5).collect::>(), result); } + const TEST_CASES: [(&[u8], &str, &str); 5] = [ + (b"", "", ""), + (b"kiwi", "6b697769", "6B697769"), + (b"kiwis", "6b69776973", "6B69776973"), + (b"foobar", "666f6f626172", "666F6F626172"), + (b"\xef\xbb\xbf", "efbbbf", "EFBBBF"), + ]; + #[test] fn test_encode_to_slice() { - let mut output_1 = [0; 4 * 2]; - encode_to_slice(b"kiwi", &mut output_1).unwrap(); - assert_eq!(&output_1, b"6b697769"); - - let mut output_2 = [0; 5 * 2]; - encode_to_slice(b"kiwis", &mut output_2).unwrap(); - assert_eq!(&output_2, b"6b69776973"); + let mut buffer = [0; 16]; - let mut output_3 = [0; 100]; + for (bytes, lower, _upper) in TEST_CASES { + buffer.fill(0); + let output = &mut buffer[..lower.len()]; + encode_to_slice(bytes, output).unwrap(); + assert_eq!(output, lower.as_bytes()); + } assert_eq!( - encode_to_slice(b"kiwis", &mut output_3), + encode_to_slice(b"kiwis", &mut buffer), Err(FromHexError::InvalidStringLength) ); } #[test] fn test_decode_to_slice() { - let mut output_1 = [0; 4]; - decode_to_slice(b"6b697769", &mut output_1).unwrap(); - assert_eq!(&output_1, b"kiwi"); - - let mut output_2 = [0; 5]; - decode_to_slice(b"6b69776973", &mut output_2).unwrap(); - assert_eq!(&output_2, b"kiwis"); - - let mut output_3 = [0; 4]; + let mut buffer = [0; 8]; + + for (bytes, lower, upper) in TEST_CASES { + let output = &mut buffer[..bytes.len()]; + decode_to_slice(lower, output).unwrap(); + assert_eq!(output, bytes); + decode_to_slice(upper, output).unwrap(); + assert_eq!(output, bytes); + } assert_eq!( - decode_to_slice(b"6", &mut output_3), + decode_to_slice(b"kiwi", &mut buffer), + Err(FromHexError::InvalidStringLength) + ); + assert_eq!( + decode_to_slice(b"kiwis", &mut buffer), Err(FromHexError::OddLength) ); } @@ -420,30 +502,52 @@ mod test { #[test] #[cfg(feature = "alloc")] fn test_encode() { - assert_eq!(encode("foobar"), "666f6f626172"); + for (bytes, lower, upper) in TEST_CASES { + assert_eq!(encode(bytes), lower); + assert_eq!(encode_upper(bytes), upper); + } } #[test] #[cfg(feature = "alloc")] - fn test_decode() { - assert_eq!( - decode("666f6f626172"), - Ok(String::from("foobar").into_bytes()) - ); + fn test_to_hex() { + for (bytes, lower, upper) in TEST_CASES { + assert_eq!(bytes.encode_hex::(), lower); + assert_eq!(bytes.encode_hex_upper::(), upper); + } + } + + #[test] + fn test_display() { + for (bytes, lower, upper) in TEST_CASES { + let disp = display(&bytes); + assert_eq!(alloc::format!("{disp}"), lower); + assert_eq!(alloc::format!("{disp:?}"), lower); + assert_eq!(alloc::format!("{disp:x}"), lower); + assert_eq!(alloc::format!("{disp:X}"), upper); + } } #[test] #[cfg(feature = "alloc")] - pub fn test_from_hex_okay_str() { - assert_eq!(Vec::from_hex("666f6f626172").unwrap(), b"foobar"); - assert_eq!(Vec::from_hex("666F6F626172").unwrap(), b"foobar"); + fn test_decode() { + for (bytes, lower, upper) in TEST_CASES { + assert_eq!(decode(lower).unwrap(), bytes); + assert_eq!(decode(lower.as_bytes()).unwrap(), bytes); + assert_eq!(decode(upper).unwrap(), bytes); + assert_eq!(decode(upper.as_bytes()).unwrap(), bytes); + } } #[test] #[cfg(feature = "alloc")] - pub fn test_from_hex_okay_bytes() { - assert_eq!(Vec::from_hex(b"666f6f626172").unwrap(), b"foobar"); - assert_eq!(Vec::from_hex(b"666F6F626172").unwrap(), b"foobar"); + pub fn test_from_hex() { + for (bytes, lower, upper) in TEST_CASES { + assert_eq!(Vec::from_hex(lower).unwrap(), bytes); + assert_eq!(Vec::from_hex(lower.as_bytes()).unwrap(), bytes); + assert_eq!(Vec::from_hex(upper).unwrap(), bytes); + assert_eq!(Vec::from_hex(upper.as_bytes()).unwrap(), bytes); + } } #[test] @@ -465,12 +569,6 @@ mod test { ); } - #[test] - #[cfg(feature = "alloc")] - pub fn test_empty() { - assert_eq!(Vec::from_hex("").unwrap(), b""); - } - #[test] #[cfg(feature = "alloc")] pub fn test_from_hex_whitespace() { @@ -492,18 +590,4 @@ mod test { Err(FromHexError::InvalidStringLength) ); } - - #[test] - #[cfg(feature = "alloc")] - fn test_to_hex() { - assert_eq!( - [0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72].encode_hex::(), - "666f6f626172".to_string(), - ); - - assert_eq!( - [0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72].encode_hex_upper::(), - "666F6F626172".to_string(), - ); - } }