hex.nx source
↩ module page · 97 lines · 3331 B
1// hex.nx -- lowercase hex encoder + decoder.
2//
3// Complement to base64.nx for the common "print bytes as text"
4// use cases where readability matters more than density:
5// - SHA-256 / SHA-3 digest display
6// - Git-style object IDs
7// - TLS handshake transcripts for debugging
8// - URL query parameters carrying short binary values
9//
10// Deterministic output (lowercase always). Decoder tolerates
11// both upper and lower case. No "0x" prefix, no whitespace.
12//
13// Invariants:
14// H1 Encoder output is exactly 2*n chars for n input bytes.
15// H2 Decoder requires even-length input; rejects odd-length
16// with a negative return.
17// H3 Decoder rejects any non-hex char (not [0-9A-Fa-f]) with a
18// negative return -- no silent skip.
19// H4 Round-trip is exact: decode(encode(x)) == x for any byte
20// sequence.
21
22import "syscalls.nx"
23
24const HEX_ERR_ODD_LENGTH: i64 = -1
25const HEX_ERR_INVALID_CHAR: i64 = -2
26
27// Encode n bytes from `in_bytes` as lowercase hex into `out`.
28// Returns 2 * n (bytes written).
29func hex_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
30 var i: i64 = 0
31 while i < n {
32 let b: i64 = in_bytes[i]
33 let hi: i64 = (b >> 4) & 0xF
34 let lo: i64 = b & 0xF
35 var hc: i64 = 0x30 + hi
36 if hi > 9 { hc = 0x61 + hi - 10 } // 'a'..'f'
37 var lc: i64 = 0x30 + lo
38 if lo > 9 { lc = 0x61 + lo - 10 }
39 out[i * 2] = hc
40 out[i * 2 + 1] = lc
41 i = i + 1
42 }
43 return n * 2
44}
45
46// Decode one hex char to its 0..15 value, or -1 if invalid.
47func hex_nibble(c: i64) -> i64 {
48 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } } // 0-9
49 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } } // A-F
50 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } } // a-f
51 return -1
52}
53
54// Decode `n` hex chars into raw bytes; writes n/2 bytes to `out`.
55// Returns bytes written or a negative HEX_ERR_*.
56func hex_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
57 if (n & 1) != 0 { return HEX_ERR_ODD_LENGTH }
58 var i: i64 = 0
59 var out_pos: i64 = 0
60 while i < n {
61 let hi: i64 = hex_nibble(in_chars[i])
62 if hi < 0 { return HEX_ERR_INVALID_CHAR }
63 let lo: i64 = hex_nibble(in_chars[i + 1])
64 if lo < 0 { return HEX_ERR_INVALID_CHAR }
65 out[out_pos] = ((hi << 4) | lo) & 0xFF
66 out_pos = out_pos + 1
67 i = i + 2
68 }
69 return out_pos
70}
71
72// Compile-only smoke: encode 0x00, 0x0f, 0xff, 0xde 0xad 0xbe 0xef
73// -> "000fffdeadbeef" (14 chars), then decode back.
74func main() -> i64 {
75 let input: *u8 = sys_mmap(8)
76 input[0] = 0x00
77 input[1] = 0x0F
78 input[2] = 0xFF
79 input[3] = 0xDE
80 input[4] = 0xAD
81 input[5] = 0xBE
82 input[6] = 0xEF
83 let encoded: *u8 = sys_mmap(32)
84 let n_enc: i64 = hex_encode(input, 7, encoded)
85 if n_enc != 14 { return 1 }
86 // First char should be '0' = 0x30.
87 if encoded[0] != 0x30 { return 2 }
88 // Check 'd' of deadbeef at offset 6 = 0x64.
89 if encoded[6] != 0x64 { return 3 }
90
91 let decoded: *u8 = sys_mmap(16)
92 let n_dec: i64 = hex_decode(encoded, n_enc, decoded)
93 if n_dec != 7 { return 4 }
94 if decoded[0] != 0x00 { return 5 }
95 if decoded[3] != 0xDE { return 6 }
96 return 0
97}