nx_hex_codec.nx source
↩ module page · 103 lines · 3488 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
22// nx_safety_envelope:
23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
24// sil_target: SIL1
25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
26// verdict: NOT_YET_EVALUATED
27
28import "nx_syscalls.nx"
29
30const HEX_ERR_ODD_LENGTH: i64 = -1
31const HEX_ERR_INVALID_CHAR: i64 = -2
32
33// Encode n bytes from `in_bytes` as lowercase hex into `out`.
34// Returns 2 * n (bytes written).
35func hex_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
36 var i: i64 = 0
37 while i < n {
38 let b: i64 = in_bytes[i]
39 let hi: i64 = (b >> 4) & 0xF
40 let lo: i64 = b & 0xF
41 var hc: i64 = 0x30 + hi
42 if hi > 9 { hc = 0x61 + hi - 10 } // 'a'..'f'
43 var lc: i64 = 0x30 + lo
44 if lo > 9 { lc = 0x61 + lo - 10 }
45 out[i * 2] = hc
46 out[i * 2 + 1] = lc
47 i = i + 1
48 }
49 return n * 2
50}
51
52// Decode one hex char to its 0..15 value, or -1 if invalid.
53func hex_nibble(c: i64) -> i64 {
54 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } } // 0-9
55 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } } // A-F
56 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } } // a-f
57 return -1
58}
59
60// Decode `n` hex chars into raw bytes; writes n/2 bytes to `out`.
61// Returns bytes written or a negative HEX_ERR_*.
62func hex_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
63 if (n & 1) != 0 { return HEX_ERR_ODD_LENGTH }
64 var i: i64 = 0
65 var out_pos: i64 = 0
66 while i < n {
67 let hi: i64 = hex_nibble(in_chars[i])
68 if hi < 0 { return HEX_ERR_INVALID_CHAR }
69 let lo: i64 = hex_nibble(in_chars[i + 1])
70 if lo < 0 { return HEX_ERR_INVALID_CHAR }
71 out[out_pos] = ((hi << 4) | lo) & 0xFF
72 out_pos = out_pos + 1
73 i = i + 2
74 }
75 return out_pos
76}
77
78// Compile-only smoke: encode 0x00, 0x0f, 0xff, 0xde 0xad 0xbe 0xef
79// -> "000fffdeadbeef" (14 chars), then decode back.
80func main() -> i64 {
81 let input: *u8 = sys_mmap(8)
82 input[0] = 0x00
83 input[1] = 0x0F
84 input[2] = 0xFF
85 input[3] = 0xDE
86 input[4] = 0xAD
87 input[5] = 0xBE
88 input[6] = 0xEF
89 let encoded: *u8 = sys_mmap(32)
90 let n_enc: i64 = hex_encode(input, 7, encoded)
91 if n_enc != 14 { return 1 }
92 // First char should be '0' = 0x30.
93 if encoded[0] != 0x30 { return 2 }
94 // Check 'd' of deadbeef at offset 6 = 0x64.
95 if encoded[6] != 0x64 { return 3 }
96
97 let decoded: *u8 = sys_mmap(16)
98 let n_dec: i64 = hex_decode(encoded, n_enc, decoded)
99 if n_dec != 7 { return 4 }
100 if decoded[0] != 0x00 { return 5 }
101 if decoded[3] != 0xDE { return 6 }
102 return 0
103}