Keep bytes past an embedded NUL in the cffi backend - #178
Open
youdie006 wants to merge 1 commit into
Open
Conversation
yajl passes the string callbacks an explicit length, but ffi.string treats the pointer as a NUL-terminated C string and stops at the first zero byte, so yajl2_cffi truncated any string or key containing \u0000. The pure Python and yajl2_c backends keep it.
Reviewer's guide (collapsed on small PRs)Reviewer's GuideFix the YAJL2 CFFI backend’s use of NUL-terminated decoding by honoring YAJL’s explicit string lengths, preventing silent truncation of values and keys; add regression coverage that exercises both cases across the parser matrix. Sequence diagram for preserving embedded NUL bytessequenceDiagram
participant YAJL
participant CFFI as yajl2_cffi
participant Parser
YAJL->>CFFI: string(val, length)
CFFI->>CFFI: ffi.buffer(val, length)[:]
CFFI->>Parser: append_event_to_ctx(string)
YAJL->>CFFI: map_key(key, length)
CFFI->>CFFI: ffi.buffer(key, length)[:]
CFFI->>Parser: append_event_to_ctx(map_key)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
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.
The
yajl2_cffibackend silently truncates every string and key at an embedded NUL. The other threeparsers keep it.
\u0000is legal JSON (RFC 8259 section 7), so this is silent data loss rather than a rejectedinput. It truncates keys too, so
{"a\u0000b": 1}and{"a\u0000c": 2}collapse onto the same key.Cause
yajl hands its string callbacks an explicit
(pointer, length)pair, butsrc/ijson/backends/yajl2_cffi.py:120discards the length semantics:ffi.stringis NUL-terminated-C-string semantics -maxlenis a cap, not the length - so it stopsat the first zero byte. Same at
map_key(:132).The change
ffi.buffer(val, length)[:], which uses the length yajl actually gave. Two sites, four lines.number()(:114) makes the same call, but a JSON number cannot contain a NUL, so I left it alonerather than widen the diff.
Observable change:
yajl2_cffinow returns strings containing\x00where it previouslyreturned a prefix. Only documents containing a NUL are affected, and those were losing data.
Tests
EMBEDDED_NUL_JSONintests/test_base.pyplustest_embedded_nulintests/test_basic_parse.py,following the existing
test_surrogate_pairspattern so it runs across every backend and adaptor.No existing test used
\u0000- I greppedtests/andsrc/.Reverting only
yajl2_cffi.pyand rebuilding:exactly the six
yajl2_cffivariants;pythonandyajl2_cpass either way. Restored:24 passed.I also mutation-checked the length itself -
ffi.buffer(val, length + 1)andffi.buffer(val, length - 1)both fail those six - so the test pins the length rather than just theabsence of truncation.
Both CI commands pass:
pytest -vvgives2161 passed, 64 skipped, andpytest --doctest-modules --doctest-ignore-import-errors srcgives1 passed, 1 skipped. Built withIJSON_EMBED_YAJL=1and a forced--reinstall-package ijsonbefore every measurement, so none ofthe numbers above came from a stale extension.
Unrelated, noting rather than bundling
The
pythonbackend disagrees with itself on float overflow withuse_float=True:[-1e309]gives-infbut[1e309]raisesUnexpectedSymbol, whileyajl2_c/yajl2_cffiraiseIncompleteJSONErrorfor both. Which of the three is intended isn't obvious to me, so I left it outDisclosure: found and prepared with AI assistance (Claude). Every figure above is from a run on this
branch, and the root cause is one I traced and verified by hand.
Summary by Sourcery
Preserve complete JSON strings and map keys containing embedded NUL bytes in the yajl2_cffi backend.
Bug Fixes:
Tests: