fix: base45 Encode drops trailing zero char on even-length 2-byte pairs (round-trip broken) - #2
Open
gaoflow wants to merge 1 commit into
Open
fix: base45 Encode drops trailing zero char on even-length 2-byte pairs (round-trip broken)#2gaoflow wants to merge 1 commit into
gaoflow wants to merge 1 commit into
Conversation
In Encode, the condition
if i + 1 == len(pairs) && res[2] == 0
was intended to detect a trailing single byte (odd input length), for
which the 3rd base45 digit (e) is always 0 and only 2 chars should be
emitted. However, it also matches 2-byte trailing pairs whose value
n = (a*256) + b happens to satisfy e = n / (45*45) == 0 (i.e. n < 2025,
in particular any pair [0x00, x]). For those pairs the library emits 2
chars instead of 3, producing output whose length is no longer divisible
by 3 and breaking the round-trip: Decode(Encode([0x00, 0x10])) returns
[0x10], silently dropping the leading 0x00 byte.
Per RFC 9285 Section 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).
Fix: discriminate by the input length parity instead of by e == 0:
if i + 1 == len(pairs) && len(bytes) % 2 != 0
This always emits 3 chars for a 2-byte pair and 2 chars only for a
genuine trailing single byte, restoring the round-trip.
Add regression tests covering:
- Encode of even-length inputs whose last pair has e == 0
([00 10] -> G00, [00 00] -> 000, [00 ff] -> U50,
[01 00 00 10] -> V50G00, [00 00 00 00] -> 000000).
- Round-trip Decode(Encode(x)) == x for the previously broken inputs.
- Controls: AB -> BB8, ! -> X0, [00] -> 00.
- RFC 9285 vectors: AB -> BB8, Hello!! -> %69 VD92EX0, ietf! -> QED8WEX0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
base45.Encodedrops the 3rd base45 character for any trailing 2-byte pair whose 3rd base-45 digiteis0, emitting 2 chars instead of 3. This breaks the round-trip:Decode(Encode([0x00, 0x10]))returns[0x10], silently dropping the leading0x00byte. The library fails its ownEncode → Decodeinvariant.Smoking gun (round-trip broken)
7 inputs are affected (all even-length inputs whose last 2-byte pair has value
n < 2025, i.e.e = n / (45*45) == 0):00 000000000 10G0G0000 ffU5U5000 7f$2$2000 2d0101001 00 00 10V50G0V50G0000 00 00 0000000000000Root cause
In
base45.goEncode:res[2]is the 3rd base-45 digite = n / (45*45). The branch was intended to detect a trailing single byte (odd input length), whereencodePairsproduces a pair[0, a]soeis always0and only 2 chars should be emitted.But the condition
res[2] == 0is also true for any 2-byte trailing pair[a, b]withn = (a*256) + b < 2025(in particular any pair[0x00, x]). For those pairs the library emits 2 chars instead of 3, so the output length is no longer divisible by 3 andDecodereconstructs a single byte, dropping the leading0x00.Spec
Per RFC 9285 §4:
So 2 chars are emitted only for a trailing single byte (odd input length) — never because
e == 0.Fix (one line)
Discriminate by the input length parity instead of by
e == 0:This always emits 3 chars for a 2-byte pair and 2 chars only for a genuine trailing single byte, restoring the round-trip and RFC compliance.
Tests
Added regression tests in
base45/base45_test.go(existing tests preserved):TestEncodeEvenLengthTrailingZeroChar—Encodeof the previously broken inputs:[00 10] → "G00",[00 00] → "000",[00 ff] → "U50",[01 00 00 10] → "V50G00",[00 00 00 00] → "000000",plus controls
AB → "BB8",! → "X0",[00] → "00".TestEncodeRoundTripEvenLengthTrailingZeroChar—Decode(Encode(x)) == xfor the previously broken inputs.TestEncodeRFC9285Vectors— RFC 9285 test vectors:AB → "BB8",Hello!! → "%69 VD92EX0",ietf! → "QED8WEX0".Verification
Controls preserved:
Encode("AB") == "BB8",Encode([0xff, 0x00]) == "UAW",Encode([0x21]) == "X0",Encode([0x00]) == "00", and all RFC 9285 test vectors pass.The diff is minimal: 1 line changed in
base45/base45.go, plus regression tests.