diff --git a/base45/base45.go b/base45/base45.go index e305a98..6893489 100644 --- a/base45/base45.go +++ b/base45/base45.go @@ -147,7 +147,7 @@ func (enc *Encoding) Encode(bytes []byte) []byte { var builder strings.Builder for i, pair := range pairs { res := encodeBase45(pair) - if i + 1 == len(pairs) && res[2] == 0 { + if i + 1 == len(pairs) && len(bytes) % 2 != 0 { for _, b := range res[:2] { if len(enc.encode) > int(b) { builder.WriteByte(enc.encode[b]) diff --git a/base45/base45_test.go b/base45/base45_test.go index ff6c67d..9d0de39 100644 --- a/base45/base45_test.go +++ b/base45/base45_test.go @@ -1,6 +1,7 @@ package base45 import ( + "bytes" "testing" ) @@ -103,3 +104,67 @@ func TestDecodeMalformed(t *testing.T) { } } } + +// TestEncodeEvenLengthTrailingZeroChar is a regression test for the bug where +// Encode dropped the 3rd base45 digit (e) of a 2-byte trailing pair whenever +// e == 0, emitting 2 chars instead of 3 and breaking the round-trip +// (Decode(Encode([00 10])) returned [10], silently dropping the 0x00 byte). +// +// Per RFC 9285 §4, an even-length input MUST produce an output whose length +// is divisible by 3 (3 chars per pair); 2 chars are emitted ONLY for a +// trailing single byte (odd input length). +func TestEncodeEvenLengthTrailingZeroChar(t *testing.T) { + cases := []testPair{ + // 2-byte pairs whose 3rd base45 digit (e) is 0 — previously dropped. + {decoded: "\x00\x10", encoded: "G00"}, + {decoded: "\x00\x00", encoded: "000"}, + {decoded: "\x00\xff", encoded: "U50"}, + // 4-byte inputs whose last pair has e == 0. + {decoded: "\x01\x00\x00\x10", encoded: "V50G00"}, + {decoded: "\x00\x00\x00\x00", encoded: "000000"}, + // Controls: last pair has e != 0 (already correct). + {decoded: "AB", encoded: "BB8"}, + // Controls: odd-length inputs (trailing single byte) — 2 chars correct. + {decoded: "!", encoded: "X0"}, + {decoded: "\x00", encoded: "00"}, + } + for _, p := range cases { + got := StdEncoding.EncodeToString([]byte(p.decoded)) + testEqual(t, "Encode(%q) = %q, want %q", p.decoded, got, p.encoded) + } +} + +// TestEncodeRoundTripEvenLengthTrailingZeroChar verifies that Decode(Encode(x)) +// == x for inputs that previously broke the round-trip. +func TestEncodeRoundTripEvenLengthTrailingZeroChar(t *testing.T) { + inputs := [][]byte{ + {0x00, 0x10}, + {0x00, 0x00}, + {0x01, 0x00, 0x00, 0x10}, + {0x00, 0x00, 0x00, 0x00}, + } + for _, in := range inputs { + enc := StdEncoding.EncodeToString(in) + dec, err := StdEncoding.DecodeString(enc) + if err != nil { + t.Errorf("Decode(Encode(% x)) returned error: %v", in, err) + continue + } + if !bytes.Equal(dec, in) { + t.Errorf("Decode(Encode(% x)) = % x, want % x", in, dec, in) + } + } +} + +// TestEncodeRFC9285Vectors re-asserts the RFC 9285 test vectors. +func TestEncodeRFC9285Vectors(t *testing.T) { + cases := []testPair{ + {decoded: "AB", encoded: "BB8"}, + {decoded: "Hello!!", encoded: "%69 VD92EX0"}, + {decoded: "ietf!", encoded: "QED8WEX0"}, + } + for _, p := range cases { + got := StdEncoding.EncodeToString([]byte(p.decoded)) + testEqual(t, "Encode(%q) = %q, want %q", p.decoded, got, p.encoded) + } +}