// Mirrors Hare's base64 @test fns (ref/hare/encoding/base64/base64.ha: // 315,514,601) over the RFC 4648 §10 vectors, table-driven (parallel // arrays; tuple-row arrays are blocked by #111). A failing row aborts // via the assert/abort builtin (task #5 @test conversion). // // The streaming decoder (newdecoder) is deferred (#247-sibling), so the // decode side is exercised through decodestr only. package base64_test; import encoding.base64; import bytes; import errors; import io; import memio; import test; fn streq(a: str, b: str) 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; }; // enc_check — encode `raw` two ways (the io.handle sink via base64.encode // and the string form via base64.encodestr) and assert both equal // `expect`. ref/hare/encoding/base64/base64.ha:315. fn enc_check(enc: *base64.encoding, raw: []u8, expect: str) void = { let out: memio.stream = memio.dynamic(); match (base64.encode(&out.vt, enc, raw)) { case let n: size => { assert(!(n: i32 != raw.len)); }; case let e: io.error => abort(); }; assert(!(!streq(memio.string(&out), expect))); assert(!(!streq(base64.encodestr(enc, raw), expect))); }; // dec_check — decodestr(`encoded`) must round-trip back to `raw`. // ref/hare/encoding/base64/base64.ha:514. fn dec_check(enc: *base64.encoding, encoded: str, raw: []u8) void = { match (base64.decodestr(enc, encoded)) { case let b: []u8 => { assert(!(!bytes.equal(b, raw))); }; case let e: errors.invalid => abort(); }; }; // inval_check — decodestr(`encoded`) must report errors.invalid. // ref/hare/encoding/base64/base64.ha:525. fn inval_check(enc: *base64.encoding, encoded: str) void = { match (base64.decodestr(enc, encoded)) { case let b: []u8 => abort(); case let e: errors.invalid => void; }; }; // Inputs are the prefixes of "foobar". The §10 expected encodings // contain no '+' / '/', so std and base64url agree on these vectors — // both alphabets are driven over the same table here; the std-vs-url // distinctness chars are covered separately by urlsafe_distinct(). @test fn rfc4648_std() void = { let foobar: [6]u8 = ['f', 'o', 'o', 'b', 'a', 'r']; let exp: [7]str = ["", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"]; let i: i32 = 0; for (i <= 6) { enc_check(&base64.std_encoding, foobar[0:i], exp[i]); dec_check(&base64.std_encoding, exp[i], foobar[0:i]); i += 1; }; }; @test fn rfc4648_url() void = { let foobar: [6]u8 = ['f', 'o', 'o', 'b', 'a', 'r']; let exp: [7]str = ["", "Zg==", "Zm8=", "Zm9v", "Zm9vYg==", "Zm9vYmE=", "Zm9vYmFy"]; let i: i32 = 0; for (i <= 6) { enc_check(&base64.url_encoding, foobar[0:i], exp[i]); dec_check(&base64.url_encoding, exp[i], foobar[0:i]); i += 1; }; }; // [0xFB, 0xFF, 0xBF] hits the 62/63 alphabet slots: std emits '+'/'/', // url emits '-'/'_'. The two encodings must differ, each round-trips // under its own alphabet, and each is INVALID under the other (std // decmap marks '-'/'_' 0xff and url marks '+'/'/' 0xff). @test fn urlsafe_distinct() void = { let raw: [3]u8 = [0xFBu8, 0xFFu8, 0xBFu8]; let s_std: str = base64.encodestr(&base64.std_encoding, raw[0:3]); let s_url: str = base64.encodestr(&base64.url_encoding, raw[0:3]); assert(!(streq(s_std, s_url))); dec_check(&base64.std_encoding, s_std, raw[0:3]); dec_check(&base64.url_encoding, s_url, raw[0:3]); // cross-alphabet decode must reject the foreign chars. inval_check(&base64.std_encoding, s_url); inval_check(&base64.url_encoding, s_std); }; // Table-driven (parallel-array idiom; tuple rows blocked by #111). Each // row exercises one malformed class the hand-written validation in // decodestr must reject. Cross-alphabet chars are covered separately by // urlsafe_distinct(); Hare's all-padding-but-not-multiple-of-4 rows // ("=", "===", "=====", base64.ha:527-531) collapse onto the length // guard here, so this table pins the multiple-of-4 survivors of every // class instead. @test fn decode_invalid() void = { let bad: [10]str = [ "Z", // len % 4 == 1 (lone trailing char) "Zg", // len % 4 == 2 "Zm9", // len % 4 == 3 "Z@==", // non-alphabet char in data region "@Zg=", // bad leading char "====", // all padding (no data) "A===", // excess '=' (3 pads in the final quad) "Z=g=", // '=' not in the trailing position "Zg==Zg==", // data after padding (embedded '=') "Zm8=Zm8=", // data after padding (embedded '=') ]; let i: i32 = 0; for (i < len(bad)) { inval_check(&base64.std_encoding, bad[i]); i += 1; }; }; @test fn sizes() void = { assert(!(base64.encodedsize(0) != 0)); assert(!(base64.encodedsize(1) != 4)); assert(!(base64.encodedsize(2) != 4)); assert(!(base64.encodedsize(3) != 4)); assert(!(base64.encodedsize(4) != 8)); assert(!(base64.encodedsize(10) != 16)); assert(!(base64.encodedsize(119) != 160)); assert(!(base64.encodedsize(120) != 160)); assert(!(base64.encodedsize(121) != 164)); assert(!(base64.encodedsize(122) != 164)); assert(!(base64.encodedsize(123) != 164)); assert(!(base64.decodedsize(0) != 0)); assert(!(base64.decodedsize(4) != 3)); assert(!(base64.decodedsize(8) != 6)); assert(!(base64.decodedsize(160) != 120)); assert(!(base64.decodedsize(164) != 123)); }; // A non-multiple-of-4 input is out of domain and must abort loudly, // never return garbage. Pins the restored Hare precondition // (ref/hare/encoding/base64/base64.ha:597, assert(sz % 4 == 0)). @test fn decodedsize_unaligned_aborts() void = { test.expectabort(); base64.decodedsize(5i32); }; @test fn roundtrip_all_bytes() void = { let src: [256]u8; let i: i32 = 0; for (i < 256) { src[i] = i: u8; i += 1; }; let s: str = base64.encodestr(&base64.std_encoding, src[0:256]); match (base64.decodestr(&base64.std_encoding, s)) { case let b: []u8 => { assert(!(b.len != 256)); assert(!(!bytes.equal(b, src[0:256]))); }; case let e: errors.invalid => abort(); }; let u: str = base64.encodestr(&base64.url_encoding, src[0:256]); match (base64.decodestr(&base64.url_encoding, u)) { case let b: []u8 => { assert(!(b.len != 256)); assert(!(!bytes.equal(b, src[0:256]))); }; case let e: errors.invalid => abort(); }; };