nx_hex_codec.nx source
↩ module page · 130 lines · 5134 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// RFC 4648 section 8 Base16: the alphabet is normatively "0123456789ABCDEF" -- UPPERCASE.
34// hex_encode() below emits LOWERCASE, so it is a correct hex encoder and NOT a Base16 encoder. Measured
35// against RFC 4648 section 10 (nx_base16_extvec_gate, document corroborated sovereign-vs-.NET):
36// exact=2/7 nocase=7/7
37// ★The only two that matched exactly are BASE16("") and BASE16("f")="66" -- the vectors containing NO
38// LETTERS. The split lands precisely on whether a vector contains A-F, which is the cleanest possible
39// confirmation that the nibble mapping and length are right and ONLY the alphabet case is wrong.
40//
41// ★★★★★THE FIX IS ADDITIVE, NOT A MUTATION OF hex_encode. Flipping hex_encode to uppercase would have
42// turned a conformance gap into a silent breakage for every existing caller that depends on lowercase
43// (URLs, digests, log lines, on-disk formats). ★WHEN AN IMPLEMENTATION IS CORRECT FOR ITS OWN CONTRACT AND
44// MERELY DOES NOT SATISFY A DIFFERENT PUBLISHED ONE, ADD THE SECOND CONTRACT -- DO NOT OVERWRITE THE FIRST.
45// And it is emphatically NOT fixed by lowercasing the RFC's expected values to match us: that would be
46// editing the authority to fit the subject.
47func base16_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
48 var i: i64 = 0
49 while i < n {
50 let b: i64 = in_bytes[i]
51 let hi: i64 = (b >> 4) & 0xF
52 let lo: i64 = b & 0xF
53 if hi < 10 { out[i * 2] = (48 + hi) as u8 } else { out[i * 2] = (55 + hi) as u8 }
54 if lo < 10 { out[i * 2 + 1] = (48 + lo) as u8 } else { out[i * 2 + 1] = (55 + lo) as u8 }
55 i = i + 1
56 }
57 return n * 2
58}
59
60// Encode n bytes from `in_bytes` as lowercase hex into `out`.
61// Returns 2 * n (bytes written).
62func hex_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
63 var i: i64 = 0
64 while i < n {
65 let b: i64 = in_bytes[i]
66 let hi: i64 = (b >> 4) & 0xF
67 let lo: i64 = b & 0xF
68 var hc: i64 = 0x30 + hi
69 if hi > 9 { hc = 0x61 + hi - 10 } // 'a'..'f'
70 var lc: i64 = 0x30 + lo
71 if lo > 9 { lc = 0x61 + lo - 10 }
72 out[i * 2] = hc
73 out[i * 2 + 1] = lc
74 i = i + 1
75 }
76 return n * 2
77}
78
79// Decode one hex char to its 0..15 value, or -1 if invalid.
80func hex_nibble(c: i64) -> i64 {
81 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } } // 0-9
82 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } } // A-F
83 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } } // a-f
84 return -1
85}
86
87// Decode `n` hex chars into raw bytes; writes n/2 bytes to `out`.
88// Returns bytes written or a negative HEX_ERR_*.
89func hex_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
90 if (n & 1) != 0 { return HEX_ERR_ODD_LENGTH }
91 var i: i64 = 0
92 var out_pos: i64 = 0
93 while i < n {
94 let hi: i64 = hex_nibble(in_chars[i])
95 if hi < 0 { return HEX_ERR_INVALID_CHAR }
96 let lo: i64 = hex_nibble(in_chars[i + 1])
97 if lo < 0 { return HEX_ERR_INVALID_CHAR }
98 out[out_pos] = ((hi << 4) | lo) & 0xFF
99 out_pos = out_pos + 1
100 i = i + 2
101 }
102 return out_pos
103}
104
105// Compile-only smoke: encode 0x00, 0x0f, 0xff, 0xde 0xad 0xbe 0xef
106// -> "000fffdeadbeef" (14 chars), then decode back.
107func main() -> i64 {
108 let input: *u8 = sys_mmap(8)
109 input[0] = 0x00
110 input[1] = 0x0F
111 input[2] = 0xFF
112 input[3] = 0xDE
113 input[4] = 0xAD
114 input[5] = 0xBE
115 input[6] = 0xEF
116 let encoded: *u8 = sys_mmap(32)
117 let n_enc: i64 = hex_encode(input, 7, encoded)
118 if n_enc != 14 { return 1 }
119 // First char should be '0' = 0x30.
120 if encoded[0] != 0x30 { return 2 }
121 // Check 'd' of deadbeef at offset 6 = 0x64.
122 if encoded[6] != 0x64 { return 3 }
123
124 let decoded: *u8 = sys_mmap(16)
125 let n_dec: i64 = hex_decode(encoded, n_enc, decoded)
126 if n_dec != 7 { return 4 }
127 if decoded[0] != 0x00 { return 5 }
128 if decoded[3] != 0xDE { return 6 }
129 return 0
130}