Skip to content

Commit

Permalink
fix(json): allow primitives at top level without separator (#3466)
Browse files Browse the repository at this point in the history
  • Loading branch information
grimly authored Jun 27, 2023
1 parent 0d6701a commit deddc2a
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 6 deletions.
47 changes: 41 additions & 6 deletions json/concatenated_json_parse_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import type { JsonValue, ParseStreamOptions } from "./common.ts";
import { parse } from "./_common.ts";

const blank = new Set(" \t\r\n");
function isBrankChar(char: string) {
function isBlankChar(char: string) {
return blank.has(char);
}

const primitives = new Map(
(["null", "true", "false"] as const).map((v) => [v[0], v]),
);

/**
* Stream to parse [Concatenated JSON](https://en.wikipedia.org/wiki/JSON_streaming#Concatenated_JSON).
*
Expand Down Expand Up @@ -53,11 +57,35 @@ export class ConcatenatedJsonParseStream
let nestCount = 0;
let readingString = false;
let escapeNext = false;
let readingPrimitive: false | "null" | "true" | "false" = false;
let positionInPrimitive = 0;
for await (const string of src) {
let sliceStart = 0;
for (let i = 0; i < string.length; i++) {
const char = string[i];

// We're reading a primitive at the top level
if (readingPrimitive) {
if (char === readingPrimitive[positionInPrimitive]) {
positionInPrimitive++;

// Emit the primitive when done reading
if (positionInPrimitive === readingPrimitive.length) {
yield parse(targetString + string.slice(sliceStart, i + 1));
hasValue = false;
readingPrimitive = false;
positionInPrimitive = 0;
targetString = "";
sliceStart = i + 1;
}
} else {
// If the primitive is malformed, keep reading, maybe the next characters can be useful in the syntax error.
readingPrimitive = false;
positionInPrimitive = 0;
}
continue;
}

if (readingString) {
if (char === '"' && !escapeNext) {
readingString = false;
Expand All @@ -74,12 +102,13 @@ export class ConcatenatedJsonParseStream
continue;
}

// Parses number, true, false, null with a nesting level of 0.
// example: 'null["foo"]' => null, ["foo"]
// example: 'false{"foo": "bar"}' => false, {foo: "bar"}
// Parses number with a nesting level of 0.
// example: '0["foo"]' => 0, ["foo"]
// example: '3.14{"foo": "bar"}' => 3.14, {foo: "bar"}
if (
hasValue && nestCount === 0 &&
(char === "{" || char === "[" || char === '"' || char === " ")
(char === "{" || char === "[" || char === '"' || char === " " ||
char === "n" || char === "t" || char === "f")
) {
yield parse(targetString + string.slice(sliceStart, i));
hasValue = false;
Expand All @@ -105,6 +134,12 @@ export class ConcatenatedJsonParseStream
break;
}

if (nestCount === 0 && primitives.has(char)) {
// The first letter of a primitive at top level was found
readingPrimitive = primitives.get(char)!;
positionInPrimitive = 1;
}

// parse object or array
if (
hasValue && nestCount === 0 &&
Expand All @@ -117,7 +152,7 @@ export class ConcatenatedJsonParseStream
continue;
}

if (!hasValue && !isBrankChar(char)) {
if (!hasValue && !isBlankChar(char)) {
// We want to ignore the character string with only blank, so if there is a character other than blank, record it.
hasValue = true;
}
Expand Down
67 changes: 67 additions & 0 deletions json/concatenated_json_parse_stream_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ Deno.test({
['null null"foo"'],
[null, null, "foo"],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["nullnull"],
[null, null],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["nullnull0"],
[null, null, 0],
);
await assertValidParse(
ConcatenatedJsonParseStream,
['nullnull"foo"'],
[null, null, "foo"],
);

await assertValidParse(
ConcatenatedJsonParseStream,
Expand All @@ -140,6 +155,21 @@ Deno.test({
['true true"foo"'],
[true, true, "foo"],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["truetrue"],
[true, true],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["truetrue0"],
[true, true, 0],
);
await assertValidParse(
ConcatenatedJsonParseStream,
['truetrue"foo"'],
[true, true, "foo"],
);

await assertValidParse(
ConcatenatedJsonParseStream,
Expand All @@ -166,6 +196,27 @@ Deno.test({
['false false"foo"'],
[false, false, "foo"],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["falsefalse"],
[false, false],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["falsefalse0"],
[false, false, 0],
);
await assertValidParse(
ConcatenatedJsonParseStream,
['falsefalse"foo"'],
[false, false, "foo"],
);

await assertValidParse(
ConcatenatedJsonParseStream,
['nullfalsetrue0true"foo"falsenullnull'],
[null, false, true, 0, true, "foo", false, null, null],
);
},
});

Expand Down Expand Up @@ -212,6 +263,11 @@ Deno.test({
['{"foo": "bar"}{', '"foo": "bar"}'],
[{ foo: "bar" }, { foo: "bar" }],
);
await assertValidParse(
ConcatenatedJsonParseStream,
["tr", 'ue{"foo": "bar"}'],
[true, { foo: "bar" }],
);
},
});

Expand All @@ -237,6 +293,17 @@ Deno.test({
},
});

Deno.test({
name: "[json] ConcatenatedJsonParseStream: primitives in containers",
async fn() {
await assertValidParse(
ConcatenatedJsonParseStream,
["[ true ]"],
[[true]],
);
},
});

Deno.test({
name: "[json] ConcatenatedJsonParseStream: halfway chunk",
async fn() {
Expand Down

0 comments on commit deddc2a

Please sign in to comment.