fix(captures): admit schema-valid HAR numbers that evidence cannot publish exactly (#3159)
* fix(captures): admit schema-valid HAR numbers that evidence cannot publish exactly * fix(captures): judge overflowing HAR integers by their lexeme
This commit is contained in:
1 parent
f8f3593a8c
commit
35e17a7172
2 files changed
+107
-17
No files matched your search
@@ -290,7 +290,7 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps unsafe extension numeric lexemes and rejects unsafe mandatory schema numbers", () => {
|
||||
it("keeps unsafe numeric lexemes in extension and mandatory schema fields", () => {
|
||||
const text = JSON.stringify(historicalHar());
|
||||
const result = decodeHarCapture(
|
||||
text.replace('"cache":{}', '"cache":{},"_big":9007199254740993'),
|
||||
@@ -302,12 +302,17 @@ it("keeps unsafe extension numeric lexemes and rejects unsafe mandatory schema n
|
||||
literal: "9007199254740993",
|
||||
});
|
||||
expect(result.records[0]?.reported).toMatchObject({ _big: null });
|
||||
expect(() =>
|
||||
decodeHarCapture(
|
||||
text.replace('"bodySize":0', '"bodySize":9007199254740993'),
|
||||
[],
|
||||
),
|
||||
).toThrow("HAR schema validation failed");
|
||||
// The pinned schema admits a valid integer the evidence cannot publish exactly.
|
||||
const mandatory = decodeHarCapture(
|
||||
text.replace('"bodySize":0', '"bodySize":9007199254740993'),
|
||||
[],
|
||||
).records[0];
|
||||
expect(mandatory?.numeric_literals).toContainEqual({
|
||||
pointer: "/request/bodySize",
|
||||
producer_type: "json-number",
|
||||
literal: "9007199254740993",
|
||||
});
|
||||
expect(mandatory?.reported).toMatchObject({ request: { bodySize: null } });
|
||||
});
|
||||
|
||||
it.each(["container", "record"] as const)(
|
||||
@@ -693,3 +698,61 @@ it("ignores one leading byte-order mark, as HAR 1.2 requires of readers", () =>
|
||||
decodeCaptureText(Buffer.from([0x7b, 0xff, 0x7d]), "HAR"),
|
||||
).toThrow("HAR is not valid UTF-8 JSON.");
|
||||
});
|
||||
|
||||
const reportedAt = (reported: unknown, pointer: string): unknown =>
|
||||
pointer
|
||||
.split("/")
|
||||
.slice(1)
|
||||
.reduce<unknown>(
|
||||
(value, key) =>
|
||||
value !== null && typeof value === "object"
|
||||
? Reflect.get(value, key)
|
||||
: undefined,
|
||||
reported,
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
from: '"time":1.25',
|
||||
literal: "0.123456789012345678901",
|
||||
pointer: "/time",
|
||||
},
|
||||
{ from: '"bodySize":99', literal: "1e400", pointer: "/response/bodySize" },
|
||||
])(
|
||||
"admits a schema-valid number that evidence cannot publish exactly: $literal",
|
||||
({ from, literal, pointer }) => {
|
||||
const key = from.slice(0, from.indexOf(":") + 1);
|
||||
const text = JSON.stringify(historicalHar()).replace(
|
||||
from,
|
||||
`${key}${literal}`,
|
||||
);
|
||||
expect(text).toContain(`${key}${literal}`);
|
||||
const record = decodeHarCapture(text, []).records[0];
|
||||
// The exact producer lexeme is retained, and the published value stays unknown.
|
||||
expect(record?.numeric_literals).toContainEqual({
|
||||
pointer,
|
||||
producer_type: "json-number",
|
||||
literal,
|
||||
});
|
||||
expect(reportedAt(record?.reported, pointer)).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"9007199254740993.5",
|
||||
"1e-400",
|
||||
"1.0000000000000000001",
|
||||
`1${"0".repeat(309)}.5`,
|
||||
`-1${"0".repeat(309)}.5`,
|
||||
])(
|
||||
"judges an integer field by the lexeme when rounding or overflow would hide a fraction: %s",
|
||||
(literal) => {
|
||||
const text = JSON.stringify(historicalHar()).replace(
|
||||
'"bodySize":99',
|
||||
`"bodySize":${literal}`,
|
||||
);
|
||||
expect(() => decodeHarCapture(text, [])).toThrow(
|
||||
"HAR schema validation failed: type (must be integer).",
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -101,6 +101,7 @@ export const decodeHarCapture = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Evidence projection when `redact` is true; the upstream schema validation copy otherwise. */
|
||||
const projectHar = (
|
||||
value: unknown,
|
||||
sensitiveValues: readonly string[],
|
||||
@@ -109,6 +110,18 @@ const projectHar = (
|
||||
const redactor = new CaptureRedaction(sensitiveValues);
|
||||
const numeric_literals: Projection["numeric_literals"] = [];
|
||||
const binary_fields: Projection["binary_fields"] = [];
|
||||
const projectNumber = (item: LosslessNumber, pointer: string): JsonValue => {
|
||||
// Evidence publishes an unsafe number as null; the schema must still judge it as a number.
|
||||
if (!redact) return validationNumber(item.value);
|
||||
const number = redactor.number(item, pointer, redact);
|
||||
if (number === null) return null;
|
||||
numeric_literals.push({
|
||||
pointer,
|
||||
producer_type: "json-number",
|
||||
literal: number.literal,
|
||||
});
|
||||
return number.reported;
|
||||
};
|
||||
const visit = (
|
||||
item: unknown,
|
||||
pointer: string,
|
||||
@@ -127,16 +140,7 @@ const projectHar = (
|
||||
redactor.harCredential(pointer, parent)
|
||||
)
|
||||
return redactor.credential(pointer);
|
||||
if (item instanceof LosslessNumber) {
|
||||
const number = redactor.number(item, pointer, redact);
|
||||
if (number === null) return null;
|
||||
numeric_literals.push({
|
||||
pointer,
|
||||
producer_type: "json-number",
|
||||
literal: number.literal,
|
||||
});
|
||||
return number.reported;
|
||||
}
|
||||
if (item instanceof LosslessNumber) return projectNumber(item, pointer);
|
||||
if (typeof item === "string") {
|
||||
if (
|
||||
redact &&
|
||||
@@ -235,6 +239,29 @@ const projectHar = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A JSON number lexeme as the validation copy carries it: its nearest double, unless rounding or
|
||||
* overflow erased a fractional part, so the schema's `integer` checks the lexeme, not the double.
|
||||
*/
|
||||
const validationNumber = (literal: string): number => {
|
||||
const nearest = Number(literal);
|
||||
if (integerLexeme(literal)) return nearest;
|
||||
if (Number.isFinite(nearest) && !Number.isInteger(nearest)) return nearest;
|
||||
return literal.startsWith("-") ? -0.5 : 0.5;
|
||||
};
|
||||
|
||||
/** Whether a JSON number lexeme denotes an integer, decided from its digits and exponent. */
|
||||
const integerLexeme = (literal: string): boolean => {
|
||||
const match = /^-?(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(literal);
|
||||
if (match === null) return false;
|
||||
const [, whole = "", fraction = "", exponent = "0"] = match;
|
||||
const digits = `${whole}${fraction}`;
|
||||
const significant = digits.replace(/0+$/, "");
|
||||
if (/^0*$/.test(significant)) return true;
|
||||
const trailingZeros = digits.length - significant.length;
|
||||
return Number(exponent) - fraction.length + trailingZeros >= 0;
|
||||
};
|
||||
|
||||
const decodeHarBase64 = (value: string, pointer: string): Buffer => {
|
||||
const compact = value.replace(/[\t\n\r ]/g, "");
|
||||
const bytes = Buffer.from(compact, "base64");
|
||||
|
||||
Reference in new issue
Block a user