Bug report
Bug description:
b2a_ascii85(data, wrapcol=N) inserts \n into the encoded output, but a2b_ascii85() rejects \n by default (ignorechars defaults to b''). This silently breaks the encode,
decode round-trip for any wrapcol value.
A secondary issue: unlike b2a_base85, which rounds wrapcol down to the nearest multiple of 5 so that newlines never fall inside a 5-char group,b2a_ascii85 uses the value as-is, corrupting group boundaries.
Proof of concept
import binascii
data = b'Hello, World!!'
# Any wrapcol that is not a multiple of 5 splits groups mid-stream
enc = binascii.b2a_ascii85(data, wrapcol=6)
print(enc)
# b'87cURD\n_*#4Df\nTZ)+X$'
# ^--- \n inserted inside a 5-char ASCII85 group
binascii.a2b_ascii85(enc)
# binascii.Error: Non-Ascii85 digit found: \n
# Even a multiple of 5 breaks the round-trip because \n is not ignored
enc2 = binascii.b2a_ascii85(data, wrapcol=5)
print(enc2)
# b'87cUR\nD_*#4\nDfTZ)\n+X$'
binascii.a2b_ascii85(enc2)
# binascii.Error: Non-Ascii85 digit found: \n
# b2a_base85 with the same wrapcol works correctly
enc3 = binascii.b2a_base85(data, wrapcol=6) # rounds to 5 internally
binascii.a2b_base85(enc3, ignorechars=b'\n') == data # True
Root cause in Modules/binascii.c
b2a_base85 (line 1462) aligns wrapcol to a multiple of 5 so that every line contains only complete groups, b2a_ascii85 (line 1218) does not:
/* b2a_base85 ----- correct (line 1462) */
if (wrapcol && out_len) {
/* Each line should encode a whole number of bytes. */
wrapcol = wrapcol < 5 ? 5 : wrapcol / 5 * 5; // present
out_len += (out_len - 1u) / wrapcol;
}
/* b2a_ascii85 ------ missing alignment (line 1218) */
if (wrapcol && out_len && out_len <= PY_SSIZE_T_MAX) {
out_len += (out_len - 1) / wrapcol; // no rounding
}
There is also an asymmetry with a2b_base64, which ignores whitespace
by default, making b2a_base64 => a2b_base64 always a valid round-trip:
Fix
/* Modules/binascii.c ------ b2a_ascii85_impl, line 1218 */
if (wrapcol && out_len && out_len <= PY_SSIZE_T_MAX) {
+ /* Each line should encode a whole number of bytes. */
+ wrapcol = wrapcol < 5 ? 5 : wrapcol / 5 * 5;
out_len += (out_len - 1) / wrapcol;
}
a2b_ascii85 should also default ignorechars to b'\n' (or at minimum b'\n\r') to match the convention set by a2b_base64.
Existing test
Lib/test/test_binascii.py has a test_ascii85_wrapcol test that exercises
wrapcol, but its internal assertDecode helper always passes
ignorechars=b"\n" explicitly:
# test_binascii.py line 574
def assertDecode(data, b_expected, adobe=False):
a = self.type2test(data)
b = binascii.a2b_ascii85(a, adobe=adobe, ignorechars=b"\n")
self.assertEqual(b, b_expected)
This means the test verifies the wrong contract. The correct contract is:
# should work with no extra parameters , currently raises binascii.Error
binascii.a2b_ascii85(binascii.b2a_ascii85(data, wrapcol=N)) == data
The test was written to accommodate the broken behavior rather than assert the round-trip works by default. This is inconsistent with b2a_base85 (which aligns wrapcol so no explicit ignorechars is needed) and b2a_base64 (whose decoder ignores whitespace by default).
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Bug report
Bug description:
b2a_ascii85(data, wrapcol=N)inserts\ninto the encoded output, buta2b_ascii85()rejects\nby default (ignorecharsdefaults tob''). This silently breaks the encode,decode round-trip for any
wrapcolvalue.A secondary issue: unlike
b2a_base85, which roundswrapcoldown to the nearest multiple of 5 so that newlines never fall inside a 5-char group,b2a_ascii85uses the value as-is, corrupting group boundaries.Proof of concept
Root cause in
Modules/binascii.cb2a_base85(line 1462) alignswrapcolto a multiple of 5 so that every line contains only complete groups,b2a_ascii85(line 1218) does not:There is also an asymmetry with
a2b_base64, which ignores whitespaceby default, making
b2a_base64=>a2b_base64always a valid round-trip:Fix
a2b_ascii85should also defaultignorecharstob'\n'(or at minimumb'\n\r') to match the convention set bya2b_base64.Existing test
Lib/test/test_binascii.pyhas atest_ascii85_wrapcoltest that exerciseswrapcol, but its internalassertDecodehelper always passesignorechars=b"\n"explicitly:This means the test verifies the wrong contract. The correct contract is:
The test was written to accommodate the broken behavior rather than assert the round-trip works by default. This is inconsistent with
b2a_base85(which alignswrapcolso no explicitignorecharsis needed) andb2a_base64(whose decoder ignores whitespace by default).CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux