Commit f3f554b9b8

Ian Johnson <ian@ianjohnson.dev>
2023-08-17 13:52:46
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.
1 parent 5395c27
Changed files (2)
lib/std/json/dynamic.zig
@@ -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]
lib/std/json/dynamic_test.zig
@@ -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);
+}