Skip to content

feat: encode bytearray and memoryview buffers - #14

Merged
goblinmode2700 merged 3 commits into
mainfrom
release/0.3.0b5-buffer-encode
Aug 14, 2026
Merged

feat: encode bytearray and memoryview buffers#14
goblinmode2700 merged 3 commits into
mainfrom
release/0.3.0b5-buffer-encode

Conversation

@goblinmode2700

Copy link
Copy Markdown
Owner

Intent

Release msgspec-toon 0.3.0b5 to fix issue 13. Encode exact bytearray and C-contiguous memoryview values directly as the same standard padded-base64 TOON scalar as exact bytes, including at root, inside Structs, and inside compact arrays, before enc_hook. Match msgspec 0.21.1 by preserving BufferError for non-contiguous memoryviews and refusing bytes and bytearray subclasses unless an enc_hook explicitly converts them. Do not silently stringify non-string mapping keys: preserve the existing policy and make every root or nested refusal name msgspec.to_builtins(..., str_keys=True) as the supported conversion route. Record bytearray, memoryview, and bytes subclasses separately in the executable support matrix; update documentation, changelog, locks, version 0.3.0b5, and advance the release performance guard to v0.3.0b4. Preserve canonical bytes, token locks, full TOON conformance, G2, G3, G5, and payload-safe errors. Validate, push, open the PR, obtain green CI, merge, and release through the repository workflow without adding an ad hoc local performance run.

What Changed

  • Encode exact bytearray and C-contiguous memoryview values as padded-base64 TOON scalars before enc_hook, while preserving BufferError for non-contiguous views and hook/refusal behavior for binary subclasses.
  • Keep rejecting non-string mapping keys, with root and nested errors directing callers to msgspec.to_builtins(..., str_keys=True); document and record the expanded behavior in the executable support matrix.
  • Bump package metadata and locks to 0.3.0b5, update release documentation, and advance the performance guard baseline to v0.3.0b4.

Risk Assessment

🚨 High: The fix still permits the original oversized pre-validation allocation through a concrete race on the repository's supported free-threaded Python target.

Testing

The full 538-fixture TOON corpus, focused buffer regressions and support matrix, canonical byte/token locks, generated changelog contract, and isolated G2 proof all passed; API evidence demonstrates the intended base64, hook, refusal, and payload-safe error behavior, while no screenshot was applicable to this non-UI codec and no ad hoc G3/G5 benchmark was run, as required.

Evidence: End-to-end buffer encode API transcript
distribution=0.3.0b5
msgspec=0.21.1
root-scalars={'bytes': b'AAEC+v8=', 'bytearray': b'AAEC+v8=', 'memoryview': b'AAEC+v8=', 'typed-memoryview': b'AQAAAAIAAAA='}
struct-scalars=[b'payload: YWI=', b'payload: YWI=', b'payload: YWI=']
compact-array=b'[3]: YWI=,Y2Q=,ZWY='
native-before-hook output=b'YWI=' hook_calls=[]
noncontiguous-refusal={'ours': ('BufferError', 'memoryview: underlying buffer is not C-contiguous'), 'msgspec': ('BufferError', 'memoryview: underlying buffer is not C-contiguous'), 'hook_calls': []}
subclass=BytesSubclass ours=('EncodeError', 'unsupported type: BytesSubclass') msgspec=('TypeError', 'Encoding objects of type BytesSubclass is unsupported') hook-conversion=b'YWI=' nested-refusals=['unsupported type: BytesSubclass', 'unsupported type: BytesSubclass']
subclass=BytearraySubclass ours=('EncodeError', 'unsupported type: BytearraySubclass') msgspec=('TypeError', 'Encoding objects of type BytearraySubclass is unsupported') hook-conversion=b'YWI=' nested-refusals=['unsupported type: BytearraySubclass', 'unsupported type: BytearraySubclass']
mapping-key-root=EncodeError: object keys must be strings; convert with msgspec.to_builtins(..., str_keys=True)
mapping-key-nested-object=EncodeError: object keys must be strings; convert with msgspec.to_builtins(..., str_keys=True)
mapping-key-compact-array=EncodeError: object keys must be strings; convert with msgspec.to_builtins(..., str_keys=True)
mapping-key-struct-field=EncodeError: object keys must be strings; convert with msgspec.to_builtins(..., str_keys=True)
documented-key-conversion={'1': 'a'} -> b'"1": a'
RESULT=PASS
Evidence: TOON 4.1.1 conformance results
{
  "summary": {
    "corpus": {
      "tag": "v4.1.1",
      "commit": "62f16b369408180f1faf1cba7da1b46d1f336f12",
      "tree_sha256": "fab1b09695cea627bb6f2f5791dd930023fb65c1d5ddae268dd006506f612c81",
      "total_tests": 538
    },
    "decode": {
      "total": 359,
      "pass": 359,
      "fail": 0,
      "unsupported_option": 0,
      "fail_nonstrict_only": 0
    },
    "encode": {
      "total": 179,
      "pass": 179,
      "fail": 0,
      "unsupported_option": 0
    },
    "strict_error_fixtures": {
      "total": 84,
      "pass": 84
    }
  },
  "results": [
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 0,
      "name": "parses list arrays for non-uniform objects",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 1,
      "name": "parses list arrays with empty items",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 2,
      "name": "parses list arrays with deeply nested objects",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 3,
      "name": "parses list arrays containing objects with nested properties",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 4,
      "name": "parses list items whose first field is a tabular array",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 5,
      "name": "parses single-field list-item object with tabular array",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 6,
      "name": "parses objects containing arrays (including empty arrays) in list form",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 7,
      "name": "parses arrays of arrays within objects",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 8,
      "name": "parses nested arrays of primitives",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 9,
      "name": "parses quoted strings and mixed lengths in nested arrays",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 10,
      "name": "parses empty inner arrays",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 11,
      "name": "parses mixed-length inner arrays",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 12,
      "name": "parses root-level primitive array inline",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 13,
      "name": "parses root-level array of uniform objects in tabular form",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 14,
      "name": "parses root-level array of non-uniform objects in list form",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 15,
      "name": "parses root-level array mixing primitive, object, and array of objects in list form",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 16,
      "name": "parses root-level array of arrays",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 17,
      "name": "parses empty root-level array",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 18,
      "name": "parses complex mixed object with arrays and nested objects",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 19,
      "name": "parses arrays mixing primitives, objects, and strings in list form",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 20,
      "name": "parses arrays mixing objects and arrays",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 21,
      "name": "parses quoted key with list-form array",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 22,
      "name": "accepts bare bracket pair as empty inner array list item",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 23,
      "name": "keeps every list item when the count mismatches in non-strict mode",
      "strict": false,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-nested.json",
      "index": 24,
      "name": "throws on a non-list-item line at item depth",
      "strict": true,
      "should_error": true,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-primitive.json",
      "index": 0,
      "name": "parses string arrays inline",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-primitive.json",
      "index": 1,
      "name": "parses number arrays inline",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-primitive.json",
      "index": 2,
      "name": "parses mixed primitive arrays inline",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-primitive.json",
      "index": 3,
      "name": "parses empty arrays",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "decode",
      "file": "arrays-primitive.json",
      "index": 4,
      "name": "parses single-item array with empty string",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {


... [120033 bytes truncated] ...

l character via \\uXXXX",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 14,
      "name": "escapes backslash in string",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 15,
      "name": "quotes string with array-like syntax",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 16,
      "name": "quotes string starting with hyphen-space",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 17,
      "name": "quotes single hyphen as object value",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 18,
      "name": "quotes string starting with hyphen as object value",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 19,
      "name": "quotes single hyphen in array",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 20,
      "name": "quotes leading-hyphen string in array",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 21,
      "name": "quotes string with bracket notation",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 22,
      "name": "quotes string with brace notation",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 23,
      "name": "encodes Unicode string without quotes",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 24,
      "name": "encodes Chinese characters without quotes",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 25,
      "name": "encodes emoji without quotes",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 26,
      "name": "encodes string with emoji and spaces",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 27,
      "name": "encodes emoji inside a quoted string",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 28,
      "name": "encodes positive integer",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 29,
      "name": "encodes decimal number",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 30,
      "name": "encodes negative integer",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 31,
      "name": "encodes zero",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 32,
      "name": "encodes negative zero as zero",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 33,
      "name": "encodes large integer without exponent notation",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 34,
      "name": "encodes small decimal without exponent notation",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 35,
      "name": "encodes large number",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 36,
      "name": "encodes MAX_SAFE_INTEGER",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 37,
      "name": "encodes repeating decimal with full precision",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 38,
      "name": "encodes true",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 39,
      "name": "encodes false",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 40,
      "name": "encodes null",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 41,
      "name": "quotes leading-plus numeric-like string",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 42,
      "name": "quotes string equal to hash",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "primitives.json",
      "index": 43,
      "name": "quotes string starting with hash",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "whitespace.json",
      "index": 0,
      "name": "produces no trailing newline at end of output",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "whitespace.json",
      "index": 1,
      "name": "maintains proper indentation for nested structures",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "whitespace.json",
      "index": 2,
      "name": "respects custom indentSize option",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    },
    {
      "category": "encode",
      "file": "whitespace.json",
      "index": 3,
      "name": "leaves non-ASCII whitespace unquoted",
      "strict": true,
      "should_error": false,
      "status": "pass",
      "detail": ""
    }
  ]
}
Evidence: G2 allocation proof
{
  "payload_records": 64,
  "build": "alloc-stats (instrumented; never the benchmarked wheel)",
  "counter_semantics": {
    "builtin_dicts": "dict built by the untyped builder",
    "builtin_lists": "list built by the untyped builder",
    "final_lists": "list or tuple the target type declared",
    "final_dicts": "dict the target type declared",
    "final_structs": "Struct instance constructed by the typed consumer"
  },
  "typed": {
    "builtin_dicts": 0,
    "builtin_lists": 0,
    "final_lists": 1,
    "final_dicts": 0,
    "final_structs": 129
  },
  "wrapper": {
    "builtin_dicts": 129,
    "builtin_lists": 1,
    "final_lists": 0,
    "final_dicts": 0,
    "final_structs": 0
  },
  "gate_G2_zero_builtin_containers": true,
  "probe_observed_the_typed_path": true,
  "note": "An `Any` field asks for a builtin tree; containers inside an `Any` subtree are requested output and are not a G2 violation. See tests/test_typed_allocations.py for that case."
}
Evidence: Version, lock, guard, and changelog contract transcript
source_revision=a3d33ffd774cec0154dc176a3ab3608640979faf
python_distribution=0.3.0b5
pyproject_version=0.3.0b5
uv_lock_version=0.3.0b5
cargo_package_version=0.3.0-beta.5
cargo_lock_version=0.3.0-beta.5
release_guard=v0.3.0b4
generated_changelog_contract:
<!-- release-compatibility:start -->
- Compatibility since `0.1.0b3`: 32 newly supported, 1 removed, 36 total support-status changes, and 0 shared canonical-wire changes.
<!-- release-compatibility:end -->
RESULT=PASS

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 error
  • 🚨 src/encode.rs:1278 - exact_buffer_bytes copies buffer contents before checked_base64 enforces the existing 2**32−1 limit. A larger bytearray is cloned here, while a memoryview is copied by tobytes and then cloned again, potentially allocating 4–8+ GiB or failing with MemoryError/process termination instead of the exact-bytes EncodeError. Check bytearray length or memoryview nbytes through a shared size guard before copying.

🔧 Fix: Guard buffer sizes before allocating copies
1 error still open:

  • 🚨 src/encode.rs:1283 - On the supported free-threaded Python build, len() and to_vec() are separate bytearray operations. Another thread can resize the exact bytearray beyond u32::MAX after this check but before the snapshot, recreating the original multi-gigabyte copy-before-error path. Observe the length and copy the bytes within one object critical section, then reject based on that same snapshot length.
✅ **Test** - passed

✅ No issues found.

  • uv run --no-sync python conformance/run.py --help (the script has no help mode, so this executed the complete pinned TOON 4.1.1 corpus)
  • uv run --no-sync pytest -q tests/test_native_encode_types.py
  • uv run --no-sync pytest -q with the four buffer-related tests/test_support_matrix.py::test_declared_behavior_is_actual_behavior[...] node IDs
  • uv run --no-sync pytest -q tests/test_release_report.py::test_current_compatibility_delta_records_support_changes (initial missing-benchmark-group setup error, then passed after uv sync --group bench --locked)
  • uv run --no-sync pytest -q tests/test_efficiency_lock.py
  • uv run --no-sync python scripts/release-report.py --check-changelog
  • make g2
  • Inline public msgspec_toon.encode probe covering root, Struct, compact arrays, hook precedence, non-contiguous memoryviews, binary subclasses, and root/nested mapping-key failures
  • Parsed release-contract probe for installed/Python/Rust/lock versions, v0.3.0b4 guard semantics, and generated changelog compatibility
  • Validated evidence JSON/transcript contracts and SHA-256 digests; confirmed a clean worktree after transient cleanup
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@goblinmode2700
goblinmode2700 merged commit f6ddaa1 into main Aug 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant