glaze 8.0.0 - #296758
Merged
Merged
Conversation
botantony
approved these changes
Aug 3, 2026
Contributor
|
🤖 An automated task has requested bottles to be published to this PR. Caution Please do not push to this PR branch before the bottle commits have been pushed, as this results in a state that is difficult to recover from. If you need to resolve a merge conflict, please use a merge commit. Do not force-push to this PR branch. |
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.
Created by
brew bumpCreated with
brew bump-formula-pr.Details
release notes
{ tag : id, ...members }structs_as_arrays/write_beve_untagged)[ id, value ]No
0x0Ebyte is ever emitted.Glaze v8 reads v1 data. The legacy
0x0Epath is retained verbatim, andskipandbeve_to_jsonkeep their v1 branches. Glaze v7 cannot read v8 variant data — a v2 variant is a structurally valid v1 BEVE value, so a generic v1 decoder parses it, but a pre-v2 Glaze reading it back into astd::variantfails because its reader requires the0x0Ebyte. If you have BEVE data at rest containing variants, or a mixed-version deployment, upgrade readers before writers.Untagged variants whose alternatives are genuinely indistinguishable on the wire collapse to the first alternative: two structs with identical field sets, two empty structs,
std::vector<int>versusstd::deque<int>, and understructs_as_arraysany two alternatives sharing a positional shape. Declare atag/idsdiscriminator — that now works in positional mode too, via the adjacent form. A map whose keys are all field names of some struct alternative resolves to the struct. Adding a map or pair alternative changes how struct data carrying an unknown key decodes: the foreign key is taken as evidence the map wrote it, a deliberate divergence from the JSON reader.Variant tagging is chosen per variant
Representation used to be picked inside the
std::visit, so an alternative that could not carry a merged discriminator fell back to no discriminator at all. One variant emitted several unrelated shapes, and two tag-less alternatives sharing a shape read back as the wrong one with no error:Representation is now decided once per variant type and every alternative obeys it:
glz::metadeclarestag{"type":"circle","radius":5}tag+content{"type":"vec","value":[1,2,3]}Three breaks follow:
tagalone with a non-object alternative is astatic_assert, naming the variant and the offending alternative and pointing atcontent. It fires on exactly the code that is silently wrong today.std::monostateunder internal tagging writes as the discriminator alone —{"type":"NONE"}, wasnull. Both readers still accept a barenull, so the break is write-only and existing data keeps parsing. This is the one change no compile error announces.glaze_object_talternative declaring a member named like the tag no longer emits the key twice. It supplies the discriminator itself, asreflectablealternatives already did; the member must hold a declared id for the round trip to work.YAML and JSONB implement internal tagging only and reject
contentat compile time. MsgPack and CBOR already encode every variant as a two-element array and are unaffected. TOML has no discriminator support either way.Out-of-range integers are rejected instead of wrapped
Range checks ran after the operation that destroyed the value they were checking. This reaches ordinary JSON integers with no exponent involved:
699 of the 744 values in [256, 999] were accepted as
uint8_t. Also fixed:1e256decoding to1through the unsignedatoiandglz::stouipaths, negative zero accepted or rejected depending on integer width, and a zero mantissa such as0e19wrongly rejected — that one is now accepted, since zero stays zero however far it is scaled.Measured against a
__int128reference over 103,135 inputs: accepted-with-a-wrong-value went from 17,714 to 0, and wrongly-rejected from 150 to 0. If you relied on lenient truncation, widen the destination type.Reader recursion is bounded at 256 levels
The BEVE, CBOR, MessagePack and JSON readers enforced no depth limit, so a small hostile buffer overflowed the stack and crashed the process. Two bytes of input bought a BEVE nesting level; one byte bought a CBOR or MessagePack level.
std::variant<A,B>exceeded_max_recursive_deptherror_on_unknown_keys = falseexceeded_max_recursive_deptherror_on_unknown_keys = falseexceeded_max_recursive_depthexceeded_max_recursive_depthexceeded_max_recursive_depthLegitimate documents nested deeper than
max_recursive_depth_limit(256) now error. Bounding the recursion turned the overflow into a hang, so variant speculation is bounded too: each rejected alternative charges what it parsed against a per-read budget ofmax(8 × input, 1 MB). BEVE went from 189 bytes → 55 s to a constant ~8 ms at any depth; JSON from 339 bytes → 40 s to the same.Truncated non-null-terminated input reports
unexpected_enderror_code::end_reachedis documented as a non-error code and was never meant to escape a read. It did:Every registry read now runs non-null-terminated, so this reached the wire: a truncated REPE body answered its client with
end_reached, telling it the request parsed and merely stopped early.end_reachedno longer escapes a read at all.json_stream_reader, which raises it itself to signal end of stream, is unaffected.Input holding no value reports
no_read_inputglz::read<{.null_terminated = false}>(v, " ")andglz::read<{.null_terminated = false, .comments = true}>(v, "// hi\n")returned success with the destination untouched. Both now reportno_read_input, the code an empty buffer already reported. Through the registry this had been answering a malformed request with a success response, and would have turned a RESTPUTwith a blank body from 400 into 204.repe::read_paramsreturnsbool, notsize_tThis one is quiet — the old form still compiles and inverts:
The byte count was wrong to begin with. Without a terminator, a variant alternative that resolves at the end of the buffer rewinds its iterator, so a completed read can report zero bytes consumed, and the registry took that for an error and returned without writing any response at all. Audit any custom REPE call handler for this.
Streaming into a non-owning view is a compile error
A refill moves the streaming window, so a
std::string_viewproduced before one addresses bytes that have since been overwritten:std::vector<std::string_view> views{}; glz::read_json(views, buffer); // 512 byte window // v7: ec == none, and 28 of 40 elements hold the wrong textThere is no runtime signal to check — the views are valid pointers into a live buffer that address the wrong bytes, and ASan does not fire because the stale bytes are still inside the buffer's own allocation. A bigger buffer does not remove the problem; it just changes which elements are wrong.
The guard sits next to each assignment that hands out a pointer into the window —
string_view_t,basic_raw_json<T>/basic_text<T>over a view, and the zero-copystd::span<const T>BEVE reader — so it fires wherever the view sits, behind a tuple, a map key, aglz::customsetter, ten structs down, and never on a type that merely looks like it holds one. NDJSON andjson_stream_readerare covered for free.Buffered reads are untouched. Zero-copy reads into views from a buffer holding the whole document remain fully supported.
Streaming buffers no longer bind to the buffered read path
A streaming buffer satisfies
contiguous, so it bound to the bufferedglz::readand was parsed as a flat span withnull_terminatedleft on, over memory that carries no terminator. ASan reported a heap-buffer-overflow read.read_jsonhadis_input_streamingoverloads and was safe;read_jsonc,validate_json,validate_jsonc,read_beveand directglz::readall overran. Fixed atreadrather than per format, so a format added later inherits the behavior. The buffered overloads changed fromcontiguous auto&& bufferto a namedcontiguous Bufso the constraint can name it; the template parameter order is unchanged.Lazy APIs take
template <auto Opts>glz::lazy_jsonandglz::lazy_bevedeclared their options NTTP astemplate <opts Opts>, so a user options struct deriving fromglz::opts— the patternglz::optsitself documents — was sliced, silently dropping every derived field:Ordinary call sites are unaffected and mangling is byte-identical for
glz::opts-valued instantiations, so there is no ODR/ABI hazard across mixed-version TUs. Two patterns break: a user forward declaration written astemplate <glz::opts Opts> struct lazy_document;no longer redeclares (Glaze's own headers used exactly that form), andglz::lazy_document<glz::opts{}> d = *glz::lazy_json<derived_opts>(buf);no longer converts — that is the slicing going away.Smaller source-level breaks
skip_string_optsandskip_until_closed_optsdirect constructors require every argument.validate_utf8_lands before the pre-existingnull_terminated_; a caller that omitted either would silently get behavior it did not ask for.null_terminated = true(static constexpr bool null_terminated = true, the shapeopts_csvuses) is now astatic_assert. That bound is what keeps the registry inside the caller's buffer, so it must not be opted out of quietly.detail::handle_sliceanddetail::seek_array_indexno longer defaultOpts. Internal tojmespath.hpp; a call site that forgot<Opts>used to compile and silently reset the whole option set.read_paramswas writing a full error response for a malformed one.std::chrono::durationand count-based time points encode differently in BEVE: now a numeric typed array of therep, byte-for-byte identical to a range of therepitself, rather than a generic array with a type tag per element. Duration and time-point map and pair keys encode as numeric keys.Improvements
validate_utf8option to disable UTF-8 validation by @stephenberry in Add a compile-time option to disable UTF-8 validation stephenberry/glaze#2756std::chronoserialization across all formats by @stephenberry in Generalize std::chrono serialization across all formats (#2671) stephenberry/glaze#2678 — durations and count-based clocks now work in BEVE, MsgPack and BSON, not just JSON/CBOR/TOML, through a single generic conversionsystem_clocktime points andyear_month_dayby @stephenberry in Add YAML support for system_clock time points and year_month_day stephenberry/glaze#2715 — also fixes the libstdc++ trap wherehigh_resolution_clockaliasessystem_clockglz::http_headers) by @annihilatorq in Add HTTP headers API stephenberry/glaze#2709 — astd::ranges::forward_rangeheader store that preserves repeated fields, original case and field order, withadd/set/fields/values/contains_token/serializeformat_supports_streaming<NDJSON>is nowtrue, and a record wider than the window reportsstreaming_unsupportednaming the buffer rather than blaming the documentread_streaminginglz::readby @stephenberry in Route streaming buffers through read_streaming in glz::read stephenberry/glaze#2734lazy_streaming_cursorrecords a consumed value's extent so the next++jumps rather than re-scanning. A 9 MB array of three-field objects withread_intoper row goes 575 → 955 MB/s (+66%) on an Apple M1 with clang-O3. Twosize_tonlazy_documentwhen enabled, nothing when disabledlazy_wide_number_skip, off by default: +34% on long numeric runs, −2% to −6% elsewhere, and within 0.1% of the previous skip path when offglz::simd_info(detected,utf8_validation,string_escape,float_write), reflectable andconstexpr. A struct rather than one name because the fields genuinely disagree: an AVX-512 build escapes strings with AVX2, and a plain SSE2 build validates UTF-8 with the scalar validatorautoso derived opts are not sliced by @stephenberry in Take lazy JSON/BEVE options asautoso derived opts are not sliced stephenberry/glaze#2744repe::read_paramsand itsboolreturn by @stephenberry in Document repe::read_params and its bool return stephenberry/glaze#2738Fixes
unexpected_endby @stephenberry in Report truncated non-null-terminated input as unexpected_end stephenberry/glaze#2735atoiby @uwezkhan in fix exponent overflow in unsigned atoi stephenberry/glaze#2719%YAMLdirective version parser by @stephenberry in fix signed overflow in the %YAML directive version parser stephenberry/glaze#2722.size()rather than decaying toconst char*std::string_viewwhen reading YAML scalars by @stephenberry in Fix dangling std::string_view when reading YAML scalars stephenberry/glaze#2716 — the reader decoded into a localstd::stringand assigned it to the view, producing a read of freed memory with a correct length and no error codewrite_jsonof a lazy view skipped with defaultopts{}, so on anull_terminated = falsedocument it scanned for a sentinel that is not there ("42"emitted"429187"). Also fixes writing a root scalar, which failed with a spuriousunexpected_endin both modesshrink_to_fiton ahas_shrink_to_fitconcept by @stephenberry in guard shrink_to_fit on a has_shrink_to_fit concept stephenberry/glaze#2712, superseding Call shrink_to_fit after growing JSON array reads when requested stephenberry/glaze#2711 by @ays7 —resizabledoes not imply the member, sostd::list/std::forward_listfailed to compile with the option on. 17 call sites across JSON, NDJSON, BEVE, CBOR and EETFhandle_slicein runtimeread_jmespathslices by @uwezkhan in forward opts to handle_slice in runtime read_jmespath slices stephenberry/glaze#2710 — the two runtime slice call sites boundopts{}, compiling out the end guards and running past the end of a non-null-terminated bufferOptson jmespath slice/index helpers by @stephenberry in require explicit Opts on jmespath slice/index helpers stephenberry/glaze#2713_MSC_VERand the fine-grained__AVX512*__macros, so an AVX-512F-only build selected the AVX-512BW validator without the featureMigration
The first four have no compiler diagnostic:
read_paramsnow returnsbool.== 0still compiles and inverts.std::monostatein an internally-tagged variant? Writes as{"tag":"ID"}now, notnull. Readers still acceptnull, so only new output is affected.std::chrono::durationin BEVE? The encoding changed to a packed numeric array.validate_utf8 = falseon your options struct.exceeded_max_recursive_depth.end_reachedat a call site? It no longer escapes a read; checkunexpected_end.std::string_viewfrom a streaming buffer? Now a compile error naming the owning equivalent. Buffered reads into views are unchanged.glz::lazy_document? Changetemplate <glz::opts Opts>totemplate <auto Opts>.Several of these breaks announce themselves as
static_asserts naming the offending type, so a clean rebuild will find most of what applies to you.Full Changelog: stephenberry/glaze@v7.9.1...v8.0.0
View the full release notes at https://github.com/stephenberry/glaze/releases/tag/v8.0.0.