code wiki / (root) / nx_hex_codec.nx

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}