From a8561c03a0d79ec41767bfa044da70fd3dee7993 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 17 May 2026 07:07:57 +0900 Subject: [PATCH] lib/encoding/hex+test: Hare port (encode / decode / sizes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the 19-line placeholder. Five entrypoints per ref/hare/encoding/hex/hex.ha + README:13: - invalid (!void) — mirrors errors::invalid (hex.ha:175 decodestr). base32's !i32 is a pre-existing in-tree divergence; hex doesn't carry it forward. - encodedsize(n) = n*2 — derived from hex.ha:46-55 encode_writer lowercase 2-chars-per-byte. - decodedsize(n) = n/2 — inverse; hex.ha:158. - encode(dst, src) i32 — lowercase output per README:13 + hex.ha:91. - decode(dst, src) (i32 | invalid) — accepts lower / upper / mixed; returns invalid on odd length (hex.ha:154) or non-hex char (hex.ha:161-163). Deferred (cite-and-defer, same pattern as base32/base64): - newencoder / newdecoder — Hare's io::handle stream API; ww has no io::handle integration yet. - encodestr / decodestr — allocator-returning sum-result; needs os.alloc-backed memio dynamic, not wired. - dump — hexdump-with-ASCII view; needs io::handle + fmt::fprintf into a write sink. Test: lib/encoding/hex/hextest.ww 13 rows — sizes, encode_basic (Hare's CAFEBABEDEADF00D verbatim), encode_zero / encode_ff / encode_empty (nibble corners + sign-extend + table off-by-one), decode_{lower,upper,mixed} (case acceptance), decode_{empty, odd_length,bad_char,bad_char_mid} (error paths), roundtrip_all_bytes (0..255 full nibble+shift family — Class B exerciser). Driver test/wcc/979_hex_run.c slotted between 978_intdiv_signed and 980_memio_run. --- Makefile | 5 + lib/encoding/hex/hex.ww | 77 ++++++++++-- lib/encoding/hex/hextest.ww | 242 ++++++++++++++++++++++++++++++++++++ test/wcc/979_hex_run.c | 49 ++++++++ 4 files changed, 364 insertions(+), 9 deletions(-) create mode 100644 lib/encoding/hex/hextest.ww create mode 100644 test/wcc/979_hex_run.c diff --git a/Makefile b/Makefile index da6f6a9d..6f1f6f14 100644 --- a/Makefile +++ b/Makefile @@ -254,6 +254,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ $(BIN)/test_stat_run $(BIN)/test_time_run \ $(BIN)/test_intdiv_signed \ + $(BIN)/test_hex_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ @@ -593,6 +594,10 @@ $(BIN)/test_intdiv_signed: test/wcc/978_intdiv_signed.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_hex_run: test/wcc/979_hex_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/encoding/hex/hex.ww b/lib/encoding/hex/hex.ww index 5eaaf6c7..0ba484d8 100644 --- a/lib/encoding/hex/hex.ww +++ b/lib/encoding/hex/hex.ww @@ -1,19 +1,78 @@ -// encoding/hex — encode/decode hexadecimal pairs. +// encoding/hex — RFC 4648 base16 (hexadecimal) encode/decode, +// buffer-based. +// +// Mirrors Hare's encoding::hex surface, modulo Hare's stream-based +// encoder/decoder and memio-backed allocator helpers. ww ships the +// in-memory subset only — same shape as lib/encoding/base32: caller +// provides dst, fn returns the byte count or invalid. +// +// Output is always lowercase per ref/hare/encoding/hex/README:13; +// decode accepts both upper- and lower-case per the same line. +// invalid — input wasn't a valid hex sequence. Mirrors the +// `errors::invalid` (!void) that ref/hare/encoding/hex/hex.ha:175 +// returns from decodestr. lib/encoding/base32's local !i32 spelling +// is a pre-existing divergence; this module follows Hare. +export type invalid = !void; + +// encodedsize — bytes required to encode `n` source bytes. +// ref/hare/encoding/hex/hex.ha:91 writes 2 chars per input byte +// unconditionally. +export fn encodedsize(n: i32) i32 = { return n * 2; }; + +// decodedsize — bytes produced when decoding `n` encoded bytes. +// `n` must be even or decode returns invalid. +export fn decodedsize(n: i32) i32 = { return n / 2; }; + +// 4-bit value to lowercase hex char. '0'..'9' is +48; 'a'..'f' is +// +87 (= 'a' - 10). +fn nibble(v: u8) u8 = { + if (v < 10u8) { return v + 48u8; }; + return v + 87u8; +}; + +// Hex char to 4-bit value, accepting upper or lower case. Returns +// 255 on invalid char (sentinel; '=' isn't legal in hex). +fn denibble(c: u8) u8 = { + if (c >= 48u8) { if (c <= 57u8) { return c - 48u8; }; }; // '0'..'9' + if (c >= 65u8) { if (c <= 70u8) { return c - 55u8; }; }; // 'A'..'F' + if (c >= 97u8) { if (c <= 102u8) { return c - 87u8; }; }; // 'a'..'f' + return 255u8; +}; + +// encode — encode `src` into `dst` as lowercase hex pairs. Returns +// bytes written. `dst` must hold at least encodedsize(src.len) +// bytes. Mirrors ref/hare/encoding/hex/hex.ha:91 `encode(out, in)` +// (sans the io::handle wrapper). export fn encode(dst: []u8, src: []u8) i32 = { let i: i32 = 0; let j: i32 = 0; for (i < src.len) { let b: u8 = src[i]; - let hi: u8 = (b: i32 >> 4): u8 & ('\x0f': u8); - let lo: u8 = b & ('\x0f': u8); - if (hi < 10) { dst[j] = hi + ('0': u8); } - else { dst[j] = hi - 10 + ('a': u8); }; - j += 1; - if (lo < 10) { dst[j] = lo + ('0': u8); } - else { dst[j] = lo - 10 + ('a': u8); }; - j += 1; + dst[j] = nibble(b >> 4u8); + dst[j + 1] = nibble(b & 15u8); i += 1; + j += 2; + }; + return j; +}; + +// decode — decode hex pairs from `src` into `dst`. Returns count +// of bytes written or invalid on odd-length input or non-hex char. +// Mirrors ref/hare/encoding/hex/hex.ha:175 `decodestr(s)` (sans +// the allocator return). +export fn decode(dst: []u8, src: []u8) (i32 | invalid) = { + if ((src.len & 1) != 0) { let e: invalid; return e; }; + let i: i32 = 0; + let j: i32 = 0; + for (i < src.len) { + let hi: u8 = denibble(src[i]); + let lo: u8 = denibble(src[i + 1]); + if (hi == 255u8) { let e: invalid; return e; }; + if (lo == 255u8) { let e: invalid; return e; }; + dst[j] = (hi << 4u8) | lo; + i += 2; + j += 1; }; return j; }; diff --git a/lib/encoding/hex/hextest.ww b/lib/encoding/hex/hextest.ww new file mode 100644 index 00000000..2beafd90 --- /dev/null +++ b/lib/encoding/hex/hextest.ww @@ -0,0 +1,242 @@ +// hextest — exercises lib/encoding/hex. Run with +// `out/bin/ww run lib/encoding/hex/hextest.ww`. Same +// signalled-then-fail()-with-+10 pattern as the rest of the 9xx +// stdlib tests; non-zero exit pinpoints the failing scenario. + +use hex; +use os; + +let signalled: i32 = 0; +fn fail() void = { os.exit(signalled + 10); }; + +fn putstr(s: str, into: []u8, off: i32) i32 = { + let i: i32 = 0; + for (i < s.len) { + into[off + i] = s[i]; + i += 1; + }; + return off + s.len; +}; + +fn streq(buf: []u8, expect: str) bool = { + if (buf.len != expect.len) { return false; }; + let i: i32 = 0; + for (i < buf.len) { + if (buf[i] != expect[i]) { return false; }; + i += 1; + }; + return true; +}; + +fn beq(a: []u8, b: []u8) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// ---- encodedsize / decodedsize ----------------------------------------- + +@test fn sizes() void = { + if (hex.encodedsize(0) != 0) { fail(); }; + if (hex.encodedsize(1) != 2) { fail(); }; + if (hex.encodedsize(8) != 16) { fail(); }; + if (hex.decodedsize(0) != 0) { fail(); }; + if (hex.decodedsize(2) != 1) { fail(); }; + if (hex.decodedsize(16) != 8) { fail(); }; +}; + +// ---- encode: lowercase, all-bytes coverage ----------------------------- +// +// Hare test vector ref/hare/encoding/hex/hex.ha:82. + +@test fn encode_basic() void = { + let src: [8]u8; + src[0] = 0xCAu8; src[1] = 0xFEu8; src[2] = 0xBAu8; src[3] = 0xBEu8; + src[4] = 0xDEu8; src[5] = 0xADu8; src[6] = 0xF0u8; src[7] = 0x0Du8; + let dst: [16]u8; + let n: i32 = hex.encode(dst[0:16], src[0:8]); + if (n != 16) { fail(); }; + if (!streq(dst[0:16], "cafebabedeadf00d")) { fail(); }; +}; + +// 0x00 in / "00" out catches a sign-extend / signed-shift miscompile +// on the high nibble. + +@test fn encode_zero() void = { + let src: [1]u8; + src[0] = 0u8; + let dst: [2]u8; + let n: i32 = hex.encode(dst[0:2], src[0:1]); + if (n != 2) { fail(); }; + if (!streq(dst[0:2], "00")) { fail(); }; +}; + +// 0xFF in / "ff" out catches an off-by-one in the nibble lookup or +// a wrong-width shift. + +@test fn encode_ff() void = { + let src: [1]u8; + src[0] = 0xFFu8; + let dst: [2]u8; + let n: i32 = hex.encode(dst[0:2], src[0:1]); + if (n != 2) { fail(); }; + if (!streq(dst[0:2], "ff")) { fail(); }; +}; + +// Empty input is a no-op encode. + +@test fn encode_empty() void = { + let src: [1]u8; + let dst: [1]u8; + let n: i32 = hex.encode(dst[0:0], src[0:0]); + if (n != 0) { fail(); }; +}; + +// ---- decode: lowercase, uppercase, mixed ------------------------------- + +@test fn decode_lower() void = { + let inbuf: [16]u8; + let n: i32 = putstr("cafebabedeadf00d", inbuf[0:16], 0); + let dst: [8]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:8], inbuf[0:n]); + match (r) { + case let m: i32 => { + if (m != 8) { fail(); }; + let want: [8]u8; + want[0] = 0xCAu8; want[1] = 0xFEu8; want[2] = 0xBAu8; want[3] = 0xBEu8; + want[4] = 0xDEu8; want[5] = 0xADu8; want[6] = 0xF0u8; want[7] = 0x0Du8; + if (!beq(dst[0:8], want[0:8])) { fail(); }; + }; + case let e: hex.invalid => { fail(); }; + }; +}; + +@test fn decode_upper() void = { + let inbuf: [16]u8; + let n: i32 = putstr("CAFEBABEDEADF00D", inbuf[0:16], 0); + let dst: [8]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:8], inbuf[0:n]); + match (r) { + case let m: i32 => { + if (m != 8) { fail(); }; + let want: [8]u8; + want[0] = 0xCAu8; want[1] = 0xFEu8; want[2] = 0xBAu8; want[3] = 0xBEu8; + want[4] = 0xDEu8; want[5] = 0xADu8; want[6] = 0xF0u8; want[7] = 0x0Du8; + if (!beq(dst[0:8], want[0:8])) { fail(); }; + }; + case let e: hex.invalid => { fail(); }; + }; +}; + +// Mixed-case must decode too; Hare's encoder is lowercase-only but +// the decoder accepts both per ref/hare/encoding/hex/README:13. + +@test fn decode_mixed() void = { + let inbuf: [16]u8; + let n: i32 = putstr("CaFeBaBeDeAdF00d", inbuf[0:16], 0); + let dst: [8]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:8], inbuf[0:n]); + match (r) { + case let m: i32 => { + if (m != 8) { fail(); }; + let want: [8]u8; + want[0] = 0xCAu8; want[1] = 0xFEu8; want[2] = 0xBAu8; want[3] = 0xBEu8; + want[4] = 0xDEu8; want[5] = 0xADu8; want[6] = 0xF0u8; want[7] = 0x0Du8; + if (!beq(dst[0:8], want[0:8])) { fail(); }; + }; + case let e: hex.invalid => { fail(); }; + }; +}; + +@test fn decode_empty() void = { + let inbuf: [1]u8; + let dst: [1]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:0], inbuf[0:0]); + match (r) { + case let m: i32 => { if (m != 0) { fail(); }; }; + case let e: hex.invalid => { fail(); }; + }; +}; + +// ---- decode: error cases ----------------------------------------------- +// +// Odd length and non-hex chars both return invalid. Hare's +// decode_reader at ref/hare/encoding/hex/hex.ha:154 returns +// errors::invalid for both. + +@test fn decode_odd_length() void = { + let inbuf: [3]u8; + let n: i32 = putstr("abc", inbuf[0:3], 0); + let dst: [2]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:2], inbuf[0:n]); + match (r) { + case let m: i32 => { fail(); }; + case let e: hex.invalid => void; + }; +}; + +@test fn decode_bad_char() void = { + let inbuf: [4]u8; + let n: i32 = putstr("zz00", inbuf[0:4], 0); // 'z' isn't hex + let dst: [2]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:2], inbuf[0:n]); + match (r) { + case let m: i32 => { fail(); }; + case let e: hex.invalid => void; + }; +}; + +@test fn decode_bad_char_mid() void = { + let inbuf: [6]u8; + let n: i32 = putstr("aabbgg", inbuf[0:6], 0); // 'g' isn't hex + let dst: [3]u8; + let r: (i32 | hex.invalid) = hex.decode(dst[0:3], inbuf[0:n]); + match (r) { + case let m: i32 => { fail(); }; + case let e: hex.invalid => void; + }; +}; + +// ---- roundtrip: every byte value 0..255 -------------------------------- + +@test fn roundtrip_all_bytes() void = { + let src: [256]u8; + let i: i32 = 0; + for (i < 256) { + src[i] = i: u8; + i += 1; + }; + let enc: [512]u8; + let n: i32 = hex.encode(enc[0:512], src[0:256]); + if (n != 512) { fail(); }; + let dec: [256]u8; + let r: (i32 | hex.invalid) = hex.decode(dec[0:256], enc[0:n]); + match (r) { + case let m: i32 => { + if (m != 256) { fail(); }; + if (!beq(src[0:256], dec[0:256])) { fail(); }; + }; + case let e: hex.invalid => { fail(); }; + }; +}; + +export fn main() i32 = { + signalled = 1; sizes(); + signalled = 2; encode_basic(); + signalled = 3; encode_zero(); + signalled = 4; encode_ff(); + signalled = 5; encode_empty(); + signalled = 6; decode_lower(); + signalled = 7; decode_upper(); + signalled = 8; decode_mixed(); + signalled = 9; decode_empty(); + signalled = 10; decode_odd_length(); + signalled = 11; decode_bad_char(); + signalled = 12; decode_bad_char_mid(); + signalled = 13; roundtrip_all_bytes(); + return 0; +}; diff --git a/test/wcc/979_hex_run.c b/test/wcc/979_hex_run.c new file mode 100644 index 00000000..f6a605b9 --- /dev/null +++ b/test/wcc/979_hex_run.c @@ -0,0 +1,49 @@ +/* + * 979_hex_run — execute the lib/encoding/hex @test fixture under the + * C-side `ww run` driver and assert exit 0. + * + * Same thin-wrapper shape as 983_base32_run / 977_time_run: + * hextest.ww carries its own `export fn main()` that drives the + * @test fns and signals which case failed via the exit code. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/encoding/hex/hextest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "hex_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("hex_run: %s ok\n", src); + return 0; +}