fix(cli): reject invalid UTF-8 JSON file bytes (#613)
* fix(cli): reject invalid UTF-8 JSON file bytes Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> * test(browser): preserve unknown with-scope loader bindings Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> --------- Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
parent
1bd641d39f
commit
22acb0d5d7
|
|
@ -17,7 +17,11 @@ export const readJsonFile = async (
|
|||
const encoded = await readFile(canonicalPath);
|
||||
let decoded: unknown;
|
||||
try {
|
||||
decoded = JSON.parse(encoded.toString("utf8"));
|
||||
decoded = JSON.parse(
|
||||
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
encoded,
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(new EvidenceFileError("read", "invalid-json", { cause }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const parseCliJsonInput = async (
|
|||
if (["{", "["].includes(value.trimStart()[0] ?? ""))
|
||||
return { ok: false, error: inputError(operation) };
|
||||
try {
|
||||
const parsed = parseJson(await readFile(value, "utf8"));
|
||||
const parsed = parseJson(await readFile(value));
|
||||
return parsed === undefined
|
||||
? jsonFileError(value, operation, "invalid-json")
|
||||
: { ok: true, value: parsed };
|
||||
|
|
@ -25,9 +25,15 @@ export const parseCliJsonInput = async (
|
|||
}
|
||||
};
|
||||
|
||||
const parseJson = (value: string): unknown => {
|
||||
const parseJson = (value: string | Uint8Array): unknown => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
return JSON.parse(
|
||||
typeof value === "string"
|
||||
? value
|
||||
: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
value,
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readJsonFile } from "../../../src/application/JsonFiles.js";
|
||||
import { parseCliJsonInput } from "../../../src/cliJsonInput.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
describe("JSON file UTF-8 byte integrity", () => {
|
||||
it.each([0x80, 0xe9, 0xff])(
|
||||
"rejects invalid UTF-8 byte %s through both file input boundaries",
|
||||
async (invalidByte) => {
|
||||
const root = await createTestTempDirectory("rea-json-utf8-");
|
||||
const path = join(root, "input.json");
|
||||
await writeFile(
|
||||
path,
|
||||
Buffer.concat([
|
||||
Buffer.from('{"message":"'),
|
||||
Buffer.from([invalidByte]),
|
||||
Buffer.from('"}'),
|
||||
]),
|
||||
);
|
||||
const fileInput = await readJsonFile(path);
|
||||
const cliInput = await parseCliJsonInput(path, "compare_web_captures");
|
||||
expect(fileInput).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "invalid-json" },
|
||||
});
|
||||
expect(cliInput).toMatchObject({
|
||||
ok: false,
|
||||
error: { input_reason: "invalid-json" },
|
||||
});
|
||||
},
|
||||
);
|
||||
it("preserves a legitimate UTF-8 replacement character", async () => {
|
||||
const root = await createTestTempDirectory("rea-json-utf8-control-");
|
||||
const path = join(root, "input.json");
|
||||
const value = { message: "valid <20> and é and 😀" };
|
||||
await writeFile(path, JSON.stringify(value), "utf8");
|
||||
const fileInput = await readJsonFile(path);
|
||||
const cliInput = await parseCliJsonInput(path, "compare_web_captures");
|
||||
expect(fileInput).toMatchObject({ ok: true, value });
|
||||
expect(cliInput).toMatchObject({ ok: true, value });
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue