std.json: avoid stale pointers when parsing Value (#16864)

Closes #16861

Using `alloc_if_needed` when parsing a `Value` allows receiving a token
which points to the buffer of the underlying `Reader`. This token will
no longer be valid after the `Reader`'s buffer is refilled, which will
happen with large values. Using `alloc_always` avoids this issue by
ensuring the returned tokens always own their data independently of the
underlying buffer.
This commit is contained in:
Ian Johnson 2023-08-17 07:52:46 -04:00 committed by GitHub
parent 5395c2786a
commit f3f554b9b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 47 additions and 7 deletions

View File

@ -93,11 +93,11 @@ pub const Value = union(enum) {
stack.items[stack.items.len - 1] == .array or
(stack.items[stack.items.len - 2] == .object and stack.items[stack.items.len - 1] == .string));
switch (try source.nextAlloc(allocator, .alloc_if_needed)) {
inline .string, .allocated_string => |s| {
switch (try source.nextAlloc(allocator, .alloc_always)) {
.allocated_string => |s| {
return try handleCompleteValue(&stack, allocator, source, Value{ .string = s }) orelse continue;
},
inline .number, .allocated_number => |slice| {
.allocated_number => |slice| {
return try handleCompleteValue(&stack, allocator, source, Value.parseFromNumberSlice(slice)) orelse continue;
},
@ -106,9 +106,9 @@ pub const Value = union(enum) {
.false => return try handleCompleteValue(&stack, allocator, source, Value{ .bool = false }) orelse continue,
.object_begin => {
switch (try source.nextAlloc(allocator, .alloc_if_needed)) {
switch (try source.nextAlloc(allocator, .alloc_always)) {
.object_end => return try handleCompleteValue(&stack, allocator, source, Value{ .object = ObjectMap.init(allocator) }) orelse continue,
inline .string, .allocated_string => |key| {
.allocated_string => |key| {
try stack.appendSlice(&[_]Value{
Value{ .object = ObjectMap.init(allocator) },
Value{ .string = key },
@ -152,7 +152,7 @@ fn handleCompleteValue(stack: *Array, allocator: Allocator, source: anytype, val
// This is an invalid state to leave the stack in,
// so we have to process the next token before we return.
switch (try source.nextAlloc(allocator, .alloc_if_needed)) {
switch (try source.nextAlloc(allocator, .alloc_always)) {
.object_end => {
// This object is complete.
value = stack.pop();
@ -160,7 +160,7 @@ fn handleCompleteValue(stack: *Array, allocator: Allocator, source: anytype, val
if (stack.items.len == 0) return value;
continue;
},
inline .string, .allocated_string => |next_key| {
.allocated_string => |next_key| {
// We've got another key.
try stack.append(Value{ .string = next_key });
// stack: [..., .object, .string]

View File

@ -15,6 +15,7 @@ const parseFromValueLeaky = @import("static.zig").parseFromValueLeaky;
const ParseOptions = @import("static.zig").ParseOptions;
const jsonReader = @import("scanner.zig").reader;
const JsonReader = @import("scanner.zig").Reader;
test "json.parser.dynamic" {
const s =
@ -288,3 +289,42 @@ test "polymorphic parsing" {
try testing.expect(tree.div.color == .blue);
try testing.expectEqualStrings("Cancel", tree.div.children[1].button.caption);
}
test "long object value" {
const value = "01234567890123456789";
const doc = "{\"key\":\"" ++ value ++ "\"}";
var fbs = std.io.fixedBufferStream(doc);
var reader = smallBufferJsonReader(testing.allocator, fbs.reader());
defer reader.deinit();
var parsed = try parseFromTokenSource(Value, testing.allocator, &reader, .{});
defer parsed.deinit();
try testing.expectEqualStrings(value, parsed.value.object.get("key").?.string);
}
test "many object keys" {
const doc =
\\{
\\ "k1": "v1",
\\ "k2": "v2",
\\ "k3": "v3",
\\ "k4": "v4",
\\ "k5": "v5"
\\}
;
var fbs = std.io.fixedBufferStream(doc);
var reader = smallBufferJsonReader(testing.allocator, fbs.reader());
defer reader.deinit();
var parsed = try parseFromTokenSource(Value, testing.allocator, &reader, .{});
defer parsed.deinit();
try testing.expectEqualStrings("v1", parsed.value.object.get("k1").?.string);
try testing.expectEqualStrings("v2", parsed.value.object.get("k2").?.string);
try testing.expectEqualStrings("v3", parsed.value.object.get("k3").?.string);
try testing.expectEqualStrings("v4", parsed.value.object.get("k4").?.string);
try testing.expectEqualStrings("v5", parsed.value.object.get("k5").?.string);
}
fn smallBufferJsonReader(allocator: Allocator, io_reader: anytype) JsonReader(16, @TypeOf(io_reader)) {
return JsonReader(16, @TypeOf(io_reader)).init(allocator, io_reader);
}