Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion base45/base45.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
65 changes: 65 additions & 0 deletions base45/base45_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package base45

import (
"bytes"
"testing"
)

Expand Down Expand Up @@ -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)
}
}