short_hex_str/
lib.rs

1// Copyright (c) The Diem Core Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4// Copyright 2021 Conflux Foundation. All rights reserved.
5// Conflux is free software and distributed under GNU General Public License.
6// See http://www.gnu.org/licenses/
7
8use mirai_annotations::debug_checked_precondition;
9use serde::{Serialize, Serializer};
10use static_assertions::const_assert;
11use std::{fmt, str};
12use thiserror::Error;
13
14/// An efficient container for formatting a byte slice as a hex-formatted
15/// string, stored on the stack.
16///
17/// Using `ShortHexStr` instead of `hex::encode` is about 3-4x faster on a
18/// recent MBP 2019 (~48 ns/iter vs ~170 ns/iter) in an artifical micro
19/// benchmark.
20#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
21pub struct ShortHexStr([u8; ShortHexStr::LENGTH]);
22
23#[derive(Error, Debug)]
24#[error("Input bytes are too short")]
25pub struct InputTooShortError;
26
27impl ShortHexStr {
28    pub const LENGTH: usize = 2 * ShortHexStr::SOURCE_LENGTH;
29    pub const SOURCE_LENGTH: usize = 4;
30
31    /// Format a new `ShortHexStr` from a byte slice.
32    ///
33    /// Returns `Err(InputTooShortError)` if the input byte slice length is less
34    /// than `SOURCE_LENGTH` bytes.
35    pub fn try_from_bytes(
36        src_bytes: &[u8],
37    ) -> Result<ShortHexStr, InputTooShortError> {
38        if src_bytes.len() >= ShortHexStr::SOURCE_LENGTH {
39            let src_short_bytes = &src_bytes[0..ShortHexStr::SOURCE_LENGTH];
40            let mut dest_bytes = [0u8; ShortHexStr::LENGTH];
41
42            // We include a tiny hex encode here instead of using the `hex`
43            // crate's `encode_to_slice`, since the compiler seems
44            // unable to inline across the crate boundary.
45            hex_encode(&src_short_bytes, &mut dest_bytes);
46            Ok(Self(dest_bytes))
47        } else {
48            Err(InputTooShortError)
49        }
50    }
51
52    pub fn as_str(&self) -> &str {
53        // We could also do str::from_utf8_unchecked here to avoid the
54        // unnecessary runtime check. Shaves ~6-7 ns/iter in a micro
55        // bench but the unsafe is probably not worth the hassle.
56        str::from_utf8(&self.0).expect(
57            "This can never fail since &self.0 will only ever contain the \
58             following characters: '0123456789abcdef', which are all valid \
59             ASCII characters and therefore all valid UTF-8",
60        )
61    }
62}
63
64impl fmt::Debug for ShortHexStr {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(self.as_str())
67    }
68}
69
70impl fmt::Display for ShortHexStr {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.write_str(self.as_str())
73    }
74}
75
76impl Serialize for ShortHexStr {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where S: Serializer {
79        serializer.serialize_str(self.as_str())
80    }
81}
82
83/// Maps a nibble to its corresponding hex-formatted ASCII character.
84const HEX_CHARS_LOWER: &[u8; 16] = b"0123456789abcdef";
85
86/// Format a byte as hex. Returns a tuple containing the first character and
87/// then the second character as ASCII bytes.
88#[inline(always)]
89fn byte2hex(byte: u8) -> (u8, u8) {
90    let hi = HEX_CHARS_LOWER[((byte >> 4) & 0x0f) as usize];
91    let lo = HEX_CHARS_LOWER[(byte & 0x0f) as usize];
92    (hi, lo)
93}
94
95/// Hex encode a byte slice into the destination byte slice.
96#[inline(always)]
97fn hex_encode(src: &[u8], dst: &mut [u8]) {
98    debug_checked_precondition!(dst.len() == 2 * src.len());
99
100    for (byte, out) in src.iter().zip(dst.chunks_mut(2)) {
101        let (hi, lo) = byte2hex(*byte);
102        out[0] = hi;
103        out[1] = lo;
104    }
105}
106
107pub trait AsShortHexStr {
108    fn short_str(&self) -> ShortHexStr;
109}
110
111impl AsShortHexStr for [u8; 16] {
112    fn short_str(&self) -> ShortHexStr {
113        const_assert!(16 >= ShortHexStr::SOURCE_LENGTH);
114        ShortHexStr::try_from_bytes(self).expect(
115            "This can never fail since 16 >= ShortHexStr::SOURCE_LENGTH",
116        )
117    }
118}
119
120impl AsShortHexStr for [u8; 32] {
121    fn short_str(&self) -> ShortHexStr {
122        const_assert!(32 >= ShortHexStr::SOURCE_LENGTH);
123        ShortHexStr::try_from_bytes(self).expect(
124            "This can never fail since 32 >= ShortHexStr::SOURCE_LENGTH",
125        )
126    }
127}
128
129#[cfg(test)]
130mod test {
131    use super::*;
132    use proptest::prelude::*;
133    use std::{str, u8};
134
135    #[test]
136    fn test_hex_encode() {
137        let src = [0x12_u8, 0x34, 0xfe, 0xba];
138        let mut actual = [0u8; 8];
139        hex_encode(&src, &mut actual);
140        let expected = b"1234feba";
141        assert_eq!(&actual, expected);
142    }
143
144    #[test]
145    fn test_byte2hex_equivalence() {
146        for byte in 0..=u8::MAX {
147            let (hi, lo) = byte2hex(byte);
148            let formatted_bytes = [hi, lo];
149            let actual = str::from_utf8(&formatted_bytes[..]).unwrap();
150            let expected = hex::encode(&[byte][..]);
151            assert_eq!(actual, expected.as_str());
152        }
153    }
154
155    proptest! {
156        #[test]
157        fn test_address_short_str_equivalence(addr in any::<[u8; 16]>()) {
158            let short_str_old = hex::encode(&addr[0..ShortHexStr::SOURCE_LENGTH]);
159            let short_str_new = ShortHexStr::try_from_bytes(&addr).unwrap();
160            prop_assert_eq!(short_str_old.as_str(), short_str_new.as_str());
161        }
162    }
163}