Skip to content

binascii.b2a_ascii85: wrapcol produces output that a2b_ascii85 cannot decode #148606

@raminfp

Description

@raminfp

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions