df_hyperliquid_core/utils/
hex.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
2pub enum BadHex {
3 #[error("hex must have an even number of digits, found {0}")]
4 OddLength(usize),
5 #[error("expected {expected} bytes of hex, found {found}")]
6 WrongLength { expected: usize, found: usize },
7 #[error("`{0}` is not a hex digit")]
8 NotAHexDigit(char),
9}
10
11#[must_use]
12pub fn encode(bytes: &[u8]) -> String {
13 let mut text = String::with_capacity(bytes.len() * 2);
14 for byte in bytes {
15 text.push(nibble(byte >> 4));
16 text.push(nibble(byte & 0x0f));
17 }
18 text
19}
20
21#[must_use]
22pub fn encode_prefixed(bytes: &[u8]) -> String {
23 let mut text = String::with_capacity(bytes.len() * 2 + 2);
24 text.push_str("0x");
25 text.push_str(&encode(bytes));
26 text
27}
28
29pub fn decode(text: &str) -> Result<Vec<u8>, BadHex> {
31 let body = body_of(text);
32 if !body.len().is_multiple_of(2) {
33 return Err(BadHex::OddLength(body.len()));
34 }
35 let mut bytes = vec![0u8; body.len() / 2];
36 fill(body, &mut bytes)?;
37 Ok(bytes)
38}
39
40pub fn decode_into(text: &str, out: &mut [u8]) -> Result<(), BadHex> {
41 let body = body_of(text);
42 if !body.len().is_multiple_of(2) {
43 return Err(BadHex::OddLength(body.len()));
44 }
45 if body.len() / 2 != out.len() {
46 return Err(BadHex::WrongLength { expected: out.len(), found: body.len() / 2 });
47 }
48 fill(body, out)
49}
50
51pub fn decode_array<const N: usize>(text: &str) -> Result<[u8; N], BadHex> {
52 let mut bytes = [0u8; N];
53 decode_into(text, &mut bytes)?;
54 Ok(bytes)
55}
56
57fn body_of(text: &str) -> &str {
58 let raw = text.trim();
59 raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")).unwrap_or(raw)
60}
61
62fn fill(body: &str, out: &mut [u8]) -> Result<(), BadHex> {
63 let digits = body.as_bytes();
64 for &byte in digits {
65 digit(byte)?;
66 }
67 for (index, slot) in out.iter_mut().enumerate() {
68 let high = digit(digits[index * 2])?;
69 let low = digit(digits[index * 2 + 1])?;
70 *slot = (high << 4) | low;
71 }
72 Ok(())
73}
74
75fn digit(byte: u8) -> Result<u8, BadHex> {
76 match byte {
77 b'0'..=b'9' => Ok(byte - b'0'),
78 b'a'..=b'f' => Ok(byte - b'a' + 10),
79 b'A'..=b'F' => Ok(byte - b'A' + 10),
80 _ => Err(BadHex::NotAHexDigit(char::from(byte))),
81 }
82}
83
84const fn nibble(value: u8) -> char {
85 (if value < 10 { b'0' + value } else { b'a' + value - 10 }) as char
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn hex_is_lowercase_and_round_trips() {
94 assert_eq!(encode(b"hello world"), "68656c6c6f20776f726c64");
95 assert_eq!(encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
96 assert_eq!(encode_prefixed(&[0xde, 0xad, 0xbe, 0xef]), "0xdeadbeef");
97 assert_eq!(encode(&[]), "");
98 assert_eq!(encode_prefixed(&[]), "0x");
99 assert_eq!(decode("0xDEADBEEF").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
100 assert_eq!(decode("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
101 assert_eq!(decode(" 0xdeadbeef\n").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
102 }
103
104 #[test]
105 fn a_fixed_size_decode_refuses_the_wrong_length() {
106 let bytes: [u8; 4] = decode_array("0xdeadbeef").unwrap();
107 assert_eq!(bytes, [0xde, 0xad, 0xbe, 0xef]);
108 assert_eq!(decode_array::<32>("0xdeadbeef"), Err(BadHex::WrongLength { expected: 32, found: 4 }));
109 assert_eq!(decode("0xabc"), Err(BadHex::OddLength(3)));
110 assert_eq!(decode("0xzz"), Err(BadHex::NotAHexDigit('z')));
111 }
112
113 #[test]
114 fn a_rejected_secret_is_never_quoted_back() {
115 let error = decode_array::<32>("0xnot-a-key").unwrap_err();
116 let text = error.to_string();
117 assert!(!text.contains("not-a-key"), "{text} leaked the input");
118 }
119}