Skip to main content

broadcast_common/
hex.rs

1//! Hexadecimal encoding of raw byte fields.
2//!
3//! Wire formats across this workspace render opaque byte fields as hex text —
4//! an HLS `#EXT-X-KEY:KEYID=0x…` attribute, an SDP `fmtp` codec-config
5//! parameter, a Smooth Streaming `QualityLevel@CodecPrivateData`. The encoder
6//! is identical in every one of them, so it lives here rather than being
7//! recopied per crate.
8//!
9//! Only the *encoder* is shared. Decoding needs an error type, and each
10//! consumer's is its own (and their input-validation policies genuinely
11//! differ — e.g. a manifest parser caps input length where a local helper
12//! does not), so decoders stay with their callers.
13
14use alloc::string::String;
15
16/// Hex-encode `data` as lowercase ASCII (two characters per byte).
17///
18/// The output is exactly `2 * data.len()` characters, zero-padded per byte,
19/// with no separators and no `0x` prefix — the form every wire format in this
20/// workspace uses.
21///
22/// ```
23/// use broadcast_common::hex::hex_encode;
24///
25/// assert_eq!(hex_encode(&[0x00, 0x0f, 0xff]), "000fff");
26/// assert_eq!(hex_encode(&[]), "");
27/// ```
28pub fn hex_encode(data: &[u8]) -> String {
29    const HEX: &[u8; 16] = b"0123456789abcdef";
30    let mut out = String::with_capacity(data.len() * 2);
31    for &b in data {
32        out.push(HEX[(b >> 4) as usize] as char);
33        out.push(HEX[(b & 0x0F) as usize] as char);
34    }
35    out
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use alloc::format;
42    use alloc::vec::Vec;
43
44    #[test]
45    fn encodes_lowercase_zero_padded() {
46        assert_eq!(hex_encode(&[0xDE, 0xAD, 0xBE, 0xEF]), "deadbeef");
47        // A byte below 0x10 must still occupy two characters.
48        assert_eq!(hex_encode(&[0x00, 0x01, 0x0A]), "00010a");
49    }
50
51    #[test]
52    fn empty_input_is_empty_output() {
53        assert_eq!(hex_encode(&[]), "");
54    }
55
56    #[test]
57    fn output_is_always_two_chars_per_byte() {
58        let all: Vec<u8> = (0..=255u8).collect();
59        let encoded = hex_encode(&all);
60        assert_eq!(encoded.len(), all.len() * 2);
61        // Every byte value round-trips to the same text `format!("{:02x}")`
62        // produces — the property callers actually depend on.
63        let expected: String = all.iter().map(|b| format!("{b:02x}")).collect();
64        assert_eq!(encoded, expected);
65    }
66}