code wiki / (root) / nx_hex_codec.nx

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}