Overview
A string field with format: binary accepts values that are not base64, so validate() reports them as valid. Whether a non-base64 value is accepted depends only on how many base64-alphabet characters it happens to contain, which I don't think is the intended behavior for a Table Schema implementation.
docs/fields/string.md links the Table Schema standard for the binary format, which describes it as "A base64 encoded string representing binary data" (https://specs.frictionlessdata.io/table-schema/#string). The existing parametrized test in frictionless/fields/__spec__/test_string.py:24-27 also encodes the intent that non-base64 input is rejected:
("binary", "dGVzdA==", "dGVzdA=="),
("binary", "", None),
("binary", "string", None), # non-base64 -> None (error)
("binary", 0, None),
The "string" case passes, but only by accident: it has 6 base64-alphabet characters, and 6 is not a multiple of 4, so it trips a padding error. A junk value whose alphabet-character count is a multiple of 4 is accepted.
Reproduction
pip install frictionless==5.19.0
from frictionless import Schema, Resource, fields
f = fields.StringField(name="blob", format="binary")
# read_cell returns (value, notes); notes is None when the cell is accepted.
print(f.read_cell("!!!!")) # ('!!!!', None) -- accepted, not base64
print(f.read_cell("@@@@")) # ('@@@@', None) -- accepted, not base64
print(f.read_cell("()()")) # ('()()', None) -- accepted, not base64
print(f.read_cell("string")) # (None, {'type': 'type is "string/binary"'}) -- rejected
# Whole-resource validation of non-base64 data passes:
schema = Schema(fields=[fields.StringField(name="blob", format="binary")])
report = Resource(data=[["blob"], ["!!!!"], ["@@@@"], ["()()"]], schema=schema).validate()
print(report.valid) # True
Observed: !!!!, @@@@, ()() are accepted and report.valid is True.
Expected (per the linked spec and the test_string.py cases above): non-base64 values are rejected with a type error, the same way "string" is.
Cause
The binary value reader calls base64.b64decode without validate=True (frictionless/fields/string.py:69):
def value_reader(cell: Any):
if not isinstance(cell, str):
return None
try:
base64.b64decode(cell)
except Exception:
return None
return cell
base64.b64decode defaults to validate=False, which silently discards every character outside the base64 alphabet before decoding. So acceptance depends only on whether the count of alphabet characters that survive is a multiple of 4, not on whether the input is actually base64:
import base64
base64.b64decode("!!!!") # b'' (all four chars discarded, no error)
base64.b64decode("!!!!", validate=True) # raises binascii.Error: Only base64 data is allowed
Possible fix and a tradeoff
Passing validate=True makes the reader reject non-base64 input, which matches the existing test cases. One thing to weigh before doing that: validate=True also rejects otherwise-valid base64 that contains embedded whitespace/newlines (MIME-style, RFC 2045), e.g. "dGVz\ndA==", which is accepted today. There is no test covering that case, so the choice between strict RFC 4648 and lenient MIME decoding is a call for the maintainers. I'm happy to open a PR in whichever direction you prefer, with the corresponding tests.
Environment
- frictionless 5.19.0 (also present on
main at frictionless/fields/string.py:69)
- Python 3.14, reproduces independent of platform (pure
base64 behavior)
Disclosure: I found and verified this with AI assistance; the reproduction above was run and confirmed against 5.19.0 and current main.
Overview
A
stringfield withformat: binaryaccepts values that are not base64, sovalidate()reports them as valid. Whether a non-base64 value is accepted depends only on how many base64-alphabet characters it happens to contain, which I don't think is the intended behavior for a Table Schema implementation.docs/fields/string.mdlinks the Table Schema standard for thebinaryformat, which describes it as "A base64 encoded string representing binary data" (https://specs.frictionlessdata.io/table-schema/#string). The existing parametrized test infrictionless/fields/__spec__/test_string.py:24-27also encodes the intent that non-base64 input is rejected:The
"string"case passes, but only by accident: it has 6 base64-alphabet characters, and 6 is not a multiple of 4, so it trips a padding error. A junk value whose alphabet-character count is a multiple of 4 is accepted.Reproduction
Observed:
!!!!,@@@@,()()are accepted andreport.validisTrue.Expected (per the linked spec and the
test_string.pycases above): non-base64 values are rejected with a type error, the same way"string"is.Cause
The
binaryvalue reader callsbase64.b64decodewithoutvalidate=True(frictionless/fields/string.py:69):base64.b64decodedefaults tovalidate=False, which silently discards every character outside the base64 alphabet before decoding. So acceptance depends only on whether the count of alphabet characters that survive is a multiple of 4, not on whether the input is actually base64:Possible fix and a tradeoff
Passing
validate=Truemakes the reader reject non-base64 input, which matches the existing test cases. One thing to weigh before doing that:validate=Truealso rejects otherwise-valid base64 that contains embedded whitespace/newlines (MIME-style, RFC 2045), e.g."dGVz\ndA==", which is accepted today. There is no test covering that case, so the choice between strict RFC 4648 and lenient MIME decoding is a call for the maintainers. I'm happy to open a PR in whichever direction you prefer, with the corresponding tests.Environment
mainatfrictionless/fields/string.py:69)base64behavior)Disclosure: I found and verified this with AI assistance; the reproduction above was run and confirmed against 5.19.0 and current
main.