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}