diff --git a/docs/native-investigation.md b/docs/native-investigation.md index b9b010ed8e..432f68efda 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -587,6 +587,10 @@ digests identify the original serialized bytes. ## Keyed archive integer precision The archive reader observes XML number element types and binary number markers. +XML values and their number facts are interpreted together from the validated +DOM. Hexadecimal `` text, including an optional sign, retains its value; +`` text and CDATA segments are combined without inferring text from nested +elements. The XML path does not serialize or reparse character references. Integers beyond the exact range of a JSON number are reported with the original decimal text as `{ "$plist_type": "integer", "decimal": "" }` when that source value can be associated unambiguously. Integral real values remain diff --git a/src/artifacts/apple/InterfaceBuilderAnalysis.ts b/src/artifacts/apple/InterfaceBuilderAnalysis.ts index 86e8941c96..d48fba88d9 100644 --- a/src/artifacts/apple/InterfaceBuilderAnalysis.ts +++ b/src/artifacts/apple/InterfaceBuilderAnalysis.ts @@ -39,7 +39,6 @@ import { decodeXmlPlistText } from "../../domain/propertyListXmlText.js"; import { decodeBinaryPlistNumbers, scanBinaryPlistNumberLiterals, - xmlPlistNumberLiterals, } from "./PlistNumberLiterals.js"; import type { PlistNumberLiterals } from "../../domain/apple/plistNumbers.js"; import { @@ -753,12 +752,13 @@ const decodePlist = ( const binaryNumberMetadata = binary ? scanBinaryPlistNumberLiterals(bytes) : undefined; - const { value, omittedPrototypeKeys } = binary + const { value, omittedPrototypeKeys, numberLiterals } = binary ? { value: parseBinary( decodeBinaryPlistNumbers(bytes, binaryNumberMetadata!), ), omittedPrototypeKeys: binaryPrototypeKeyObserved ? null : 0, + numberLiterals: binaryNumberMetadata!.literals, } : parseXmlWithinDecodeBudget(xmlText ?? decodeXmlPlistText(bytes)); const projected = projectPlistValue(value); @@ -766,8 +766,6 @@ const decodePlist = ( value: projected.value, omittedPrototypeKeys, unknownDateCount: projected.unknownDateCount, - numberLiterals: binary - ? binaryNumberMetadata!.literals - : xmlPlistNumberLiterals(xmlText ?? decodeXmlPlistText(bytes)), + numberLiterals, }; }; diff --git a/src/artifacts/apple/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts index e2340f3ad2..b7b0d109b8 100644 --- a/src/artifacts/apple/KeyedArchiveReader.ts +++ b/src/artifacts/apple/KeyedArchiveReader.ts @@ -11,7 +11,6 @@ import { createPlistNumberProjection } from "../../domain/apple/plistNumbers.js" import { decodeBinaryPlistNumbers, scanBinaryPlistNumberLiterals, - xmlPlistNumberLiterals, } from "./PlistNumberLiterals.js"; import { omittedPrototypeKeysLimitation, @@ -77,6 +76,7 @@ export const decodeKeyedArchiveBytes = ( value: parseBinary( decodeBinaryPlistNumbers(bytes, binaryNumberMetadata!), ), + numberLiterals: binaryNumberMetadata!.literals, omittedPrototypeKeys: binaryExpansion.binaryPrototypeKeyObserved ? null : 0, @@ -86,11 +86,7 @@ export const decodeKeyedArchiveBytes = ( parsed.value, selection, ); - const numbers = createPlistNumberProjection( - binary - ? binaryNumberMetadata!.literals - : xmlPlistNumberLiterals(xmlText ?? ""), - ); + const numbers = createPlistNumberProjection(parsed.numberLiterals); // Classify the serialized graph before projecting values to typed decimals. // Numeric IDs, node kinds, references and pagination retain their original meaning. const roots = Object.fromEntries( diff --git a/src/artifacts/apple/PlistNumberLiterals.ts b/src/artifacts/apple/PlistNumberLiterals.ts index b5293a0e0a..12b55ecb9b 100644 --- a/src/artifacts/apple/PlistNumberLiterals.ts +++ b/src/artifacts/apple/PlistNumberLiterals.ts @@ -1,125 +1,9 @@ -import type { PlistNumberLiterals } from "../../domain/apple/plistNumbers.js"; -import { decodeXmlText } from "../../domain/propertyListKeys.js"; +import { + createPlistNumberCollector, + type PlistNumberLiterals, +} from "../../domain/apple/plistNumbers.js"; import { hasBinaryPlistSignature } from "../../domain/apple/appleArchiveFormat.js"; -const collector = () => { - const integers = new Map>(); - const reals = new Set(); - let unsafeIntegerLiterals = 0; - let incompleteMetadata = false; - return { - integer: (literal: string) => { - if (!/^[+-]?\d+$/u.test(literal)) { - incompleteMetadata = true; - return; - } - const exact = BigInt(literal); - if ( - exact >= BigInt(Number.MIN_SAFE_INTEGER) && - exact <= BigInt(Number.MAX_SAFE_INTEGER) - ) - return; - unsafeIntegerLiterals += 1; - const rounded = Number(exact); - integers.set( - rounded, - (integers.get(rounded) ?? new Set()).add(exact.toString()), - ); - }, - real: (value: number) => { - reals.add(value); - }, - markIncomplete: () => { - incompleteMetadata = true; - }, - result: (incomplete = false): PlistNumberLiterals => ({ - incomplete: incomplete || incompleteMetadata, - integers, - reals, - unsafeIntegerLiterals, - }), - }; -}; -// Consume quoted tag attributes and opaque comment/CDATA tokens whole. -const XML_TOKEN = - /||<(?:[^>"']|"[^"]*"|'[^']*')*>/gu; -const OPAQUE_ELEMENTS = new Set(["string", "key", "data", "date"]); - -/** Observe plist number element types without interpreting strings or class code. */ -export const xmlPlistNumberLiterals = (xml: string): PlistNumberLiterals => { - const output = collector(); - const elements: string[] = []; - let number: - | { - kind: "integer" | "real"; - text: string; - depth: number; - plainText: boolean; - } - | undefined; - let cursor = 0; - for (const match of xml.matchAll(XML_TOKEN)) { - const index = match.index; - if (number !== undefined) number.text += xml.slice(cursor, index); - const token = match[0]; - cursor = index + token.length; - if (token.startsWith("1ignored.25", + ); + expect(parsed.value).toEqual([0, 1.25]); + expect(parsed.numberLiterals.incomplete).toBe(true); +}); + +it("interprets failures in omitted entries before reporting partial evidence", () => { + expect(() => + parseXmlPropertyList( + "__proto__", + ), + ).toThrow("Invalid PLIST tag"); +}); + +it("retains native decoder null and nested-envelope behavior", () => { + expect( + parseXmlPropertyList( + "value", + ).value, + ).toEqual([{ value: null }, [null]]); +}); diff --git a/src/domain/propertyListXml.ts b/src/domain/propertyListXml.ts new file mode 100644 index 0000000000..696f917ab4 --- /dev/null +++ b/src/domain/propertyListXml.ts @@ -0,0 +1,140 @@ +import type { Document, Node } from "@xmldom/xmldom"; + +import { + createPlistNumberCollector, + parsePlistIntegerLiteral, +} from "./apple/plistNumbers.js"; + +/** Interpret an already validated, depth-bounded DOM, owning values and number facts together. */ +export const decodeXmlPropertyList = (document: Document) => { + const root = document.documentElement; + if (root?.nodeName !== "plist") + throw new TypeError("malformed document. First element should be "); + const numbers = createPlistNumberCollector(); + const requireContent = (node: Node): Node => { + if (node.firstChild === null) + throw new TypeError(`Cannot parse "" as ${node.nodeName}`); + return node.firstChild; + }; + const textNode = (node: Node) => node.nodeType === node.TEXT_NODE; + const characterNode = (node: Node) => + textNode(node) || node.nodeType === node.CDATA_SECTION_NODE; + const content = (node: Node, accepts: (child: Node) => boolean) => { + let text = ""; + let complete = true; + for ( + let child = node.firstChild; + child !== null; + child = child.nextSibling + ) { + if (accepts(child)) text += child.nodeValue ?? ""; + else complete = false; + } + return { text, complete }; + }; + const dictionaryKey = (node: Node): string => { + for (let child = node.firstChild; child !== null; child = child.nextSibling) + if (child.nodeType === child.ELEMENT_NODE) + return String(node.firstChild?.nodeValue ?? null); + return content(node, characterNode).text; + }; + const members = function* (node: Node) { + for (let child = node.firstChild; child !== null; child = child.nextSibling) + if ( + child.nodeType !== child.TEXT_NODE && + child.nodeType !== child.CDATA_SECTION_NODE && + child.nodeType !== child.COMMENT_NODE + ) + yield child; + }; + const decode = (node: Node): unknown => { + switch (node.nodeName) { + case "plist": + return Array.from(members(node), decode); + case "array": { + const values: unknown[] = []; + for (const member of members(node)) { + const value = decode(member); + if (value !== null) values.push(value); + } + return values; + } + case "dict": { + const entries: [string, unknown][] = []; + let key: string | undefined; + for (const child of members(node)) { + if (key === undefined) { + if (child.nodeName !== "key") + throw new TypeError("Missing key while parsing "); + key = dictionaryKey(child); + } else { + if (child.nodeName === "key") + throw new TypeError( + "Unexpected while parsing . Keys and values must alternate.", + ); + entries.push([key, decode(child)]); + key = undefined; + } + } + if (key !== undefined) entries.push([key, ""]); + // Define own properties safely, including __proto__; the caller owns omission accounting. + return Object.fromEntries(entries); + } + case "key": { + // The pinned decoder also accepted key elements as scalars. Keep their + // first-child interpretation separate from dictionary key identities. + const value = node.firstChild === null ? "" : node.firstChild.nodeValue; + if (value === "__proto__") + throw new TypeError("__proto__ is not a scalar plist key"); + return value; + } + case "string": + return content(node, characterNode).text; + case "integer": { + const first = requireContent(node); + const literal = content(node, textNode); + const exact = literal.complete + ? parsePlistIntegerLiteral(literal.text) + : undefined; + if (exact === undefined) { + numbers.markIncomplete(); + return Number.parseInt(first.nodeValue ?? "", 10); + } + numbers.integer(exact); + const decoded = Number(exact); + // Keep a finite integer's identity when it cannot fit even a Number + // approximation; otherwise it would be mislabeled as a non-finite real. + return Number.isFinite(decoded) ? decoded : exact; + } + case "real": { + requireContent(node); + const literal = content(node, characterNode); + const value = Number.parseFloat(literal.text); + if (literal.complete) numbers.real(value); + else numbers.markIncomplete(); + return value; + } + case "data": { + const binary = atob(content(node, textNode).text.replace(/\s+/gu, "")); + return Uint8Array.from(binary, (character) => character.charCodeAt(0)); + } + case "date": { + const value = requireContent(node).nodeValue; + return new Date(value === null ? 0 : value); + } + case "null": + return null; + case "true": + return true; + case "false": + return false; + default: + throw new TypeError(`Invalid PLIST tag ${node.nodeName}`); + } + }; + const values = Array.from(members(root), decode); + return { + value: values.length === 1 ? values[0] : values, + numberLiterals: numbers.result(), + }; +}; diff --git a/src/native/parsers/plist.test.ts b/src/native/parsers/plist.test.ts index 8d6be612de..1ad64bc630 100644 --- a/src/native/parsers/plist.test.ts +++ b/src/native/parsers/plist.test.ts @@ -20,6 +20,24 @@ it("retains complete segmented XML keys in native plist fallback evidence", () = ); }); +it("does not relabel integral CDATA reals as integers in native JSON or XML evidence", () => { + const xml = + "integer9007199254740992real9007199254"; + for (const parsed of [ + parsePlistJson('{"integer":9007199254740992,"real":9007199254740992}', xml), + parsePlistXml(xml), + ]) { + if (!parsed.ok) throw parsed.error; + expect(parsed.value.value).toEqual({ + integer: 9007199254740992, + real: 9007199254740992, + }); + expect(parsed.value.limitations).toContainEqual( + expect.stringContaining("2 number(s)"), + ); + } +}); + it("keeps exact decimal text for JSON integers that XML shows are integers", () => { const parsed = parsePlistJson( '{"Max":18446744073709551615,"Min":-9223372036854775808,"Near":9007199254740993,"Edge":9007199254740992,"Safe":9007199254740991,"Real":1e21,"Ratio":0.5}', diff --git a/src/native/parsers/plist.ts b/src/native/parsers/plist.ts index a3b6c4fa5e..5fb0fecab2 100644 --- a/src/native/parsers/plist.ts +++ b/src/native/parsers/plist.ts @@ -14,6 +14,11 @@ import { } from "../../domain/propertyListKeys.js"; import { err, ok, type Result } from "../../domain/result.js"; +import { + incompletePlistNumberLimitation, + type PlistNumberLiterals, +} from "../../domain/apple/plistNumbers.js"; + const plistObject = z.record(z.string(), z.unknown()); /** Stable bundle metadata projected from one plist value. */ @@ -87,14 +92,15 @@ export const parsePlistJson = ( output: string, xmlConversion?: string, ): Result => { - const literals = - xmlConversion === undefined ? undefined : xmlNumberLiterals(xmlConversion); + let literals: PlistNumberLiterals | undefined; let exactIntegerCount = 0; let ambiguousNumberCount = 0; const classify = (item: number, digits: string): unknown => { const exact = BigInt(digits).toString(); - const integer = literals?.integers.get(item)?.has(exact) === true; - const real = literals?.reals.has(item) === true; + const integer = + literals?.incomplete === false && + literals.integers.get(item)?.has(exact) === true; + const real = literals?.incomplete === false && literals.reals.has(item); if (integer && !real) { exactIntegerCount += 1; return { $plist_type: "integer", decimal: digits }; @@ -106,6 +112,10 @@ export const parsePlistJson = ( }; let decoded: ReturnType; try { + literals = + xmlConversion === undefined + ? undefined + : parseXmlPropertyList(xmlConversion).numberLiterals; decoded = omitPrototypeKeys(decodePlistJson(output, classify)); } catch (cause: unknown) { return err( @@ -121,6 +131,9 @@ export const parsePlistJson = ( value, bundle: projectPlistBundle(value), limitations: [ + ...(literals?.incomplete === true + ? [incompletePlistNumberLimitation] + : []), ...numberLimitations(exactIntegerCount, ambiguousNumberCount), ...prototypeKeyLimitations(decoded.omittedPrototypeKeys), ], @@ -136,9 +149,11 @@ export const parsePlistXml = ( ): Result => { let projected: ProjectedPlistValue; let omittedPrototypeKeys: number; + let literals: PlistNumberLiterals; try { const decoded = parseXmlPropertyList(output); omittedPrototypeKeys = decoded.omittedPrototypeKeys; + literals = decoded.numberLiterals; projected = projectPlistValue(decoded.value); } catch (cause: unknown) { return err( @@ -151,7 +166,6 @@ export const parsePlistXml = ( } // The XML decoder turns and into numbers before REA sees // them, rounding integers beyond the exact range. - const literals = xmlNumberLiterals(output); let exactIntegerCount = 0; let ambiguousNumberCount = 0; const value = mapJsonNumbers(projected.value, (item) => { @@ -159,6 +173,7 @@ export const parsePlistXml = ( if (decimals === undefined) return item; const [decimal] = decimals; if ( + literals.incomplete || decimals.size !== 1 || decimal === undefined || literals.reals.has(item) @@ -182,41 +197,13 @@ export const parsePlistXml = ( ...(projected.unknownDateCount === 0 ? [] : [unknownDateLimitation(projected.unknownDateCount)]), + ...(literals.incomplete ? [incompletePlistNumberLimitation] : []), ...numberLimitations(exactIntegerCount, ambiguousNumberCount), ...prototypeKeyLimitations(omittedPrototypeKeys), ], }); }; -/** `` and `` element literals in plutil's XML conversion. */ -interface XmlNumberLiterals { - /** Exact integer literals beyond the safe range, keyed by decoded number. */ - readonly integers: ReadonlyMap>; - readonly reals: ReadonlySet; -} - -// plutil escapes "<" in keys and strings, so these element literals can be -// read from the XML text. -const xmlNumberLiterals = (xml: string): XmlNumberLiterals => { - const integers = new Map>(); - for (const [, decimal = ""] of xml.matchAll( - /\s*([+-]?\d+)\s*<\/integer>/gu, - )) { - const rounded = Number(decimal); - if (Number.isSafeInteger(rounded)) continue; - integers.set( - rounded, - (integers.get(rounded) ?? new Set()).add(BigInt(decimal).toString()), - ); - } - const reals = new Set( - [...xml.matchAll(/([^<]*)<\/real>/gu)].map(([, text = ""]) => - Number.parseFloat(text), - ), - ); - return { integers, reals }; -}; - const prototypeKeyLimitations = (count: number): string[] => count === 0 ? [] : [omittedPrototypeKeysLimitation(count)]; diff --git a/tests/acceptance/applications/interfaceBuilderPlistNumbers.test.ts b/tests/acceptance/applications/interfaceBuilderPlistNumbers.test.ts index 3c7049e3b9..b8e8395975 100644 --- a/tests/acceptance/applications/interfaceBuilderPlistNumbers.test.ts +++ b/tests/acceptance/applications/interfaceBuilderPlistNumbers.test.ts @@ -17,6 +17,8 @@ const scenarios = [ "integer collision", "integer-real collision", "real", + "CDATA real", + "hexadecimal", "mixed XML", "segmented keys", ] as const; @@ -66,9 +68,13 @@ cliTest.for(scenarios)( "__INTEGER__", scenario === "real" ? "9007199254740992" - : scenario === "mixed XML" - ? "90071992547409923" - : "9007199254740993", + : scenario === "CDATA real" + ? "9007199254" + : scenario === "hexadecimal" + ? "0x20000000000001" + : scenario === "mixed XML" + ? "90071992547409923" + : "9007199254740993", ) .replace( "__OTHER__", @@ -122,6 +128,7 @@ cliTest.for(scenarios)( large: scenario === "XML" || scenario === "binary" || + scenario === "hexadecimal" || scenario === "segmented keys" ? { $plist_type: "integer", decimal: "9007199254740993" } : 9007199254740992, @@ -142,12 +149,13 @@ cliTest.for(scenarios)( if ( scenario === "XML" || scenario === "binary" || + scenario === "hexadecimal" || scenario === "segmented keys" ) expect(result.limitations).toContainEqual( expect.stringContaining("1 unambiguous integer value"), ); - else if (scenario !== "real") + else if (scenario !== "real" && scenario !== "CDATA real") expect(result.limitations).toContainEqual( expect.stringContaining("integer precision loss remains possible"), ); diff --git a/tests/acceptance/applications/keyedArchiveXmlNumbers.test.ts b/tests/acceptance/applications/keyedArchiveXmlNumbers.test.ts new file mode 100644 index 0000000000..c53ffa252c --- /dev/null +++ b/tests/acceptance/applications/keyedArchiveXmlNumbers.test.ts @@ -0,0 +1,113 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { build } from "plist"; +import { expect } from "vitest"; + +import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for([ + { name: "plain real", element: "1.25", expected: 1.25 }, + { + name: "mixed real", + element: "1", + expected: 1.25, + }, + { + name: "CDATA real", + element: "", + expected: 1.25, + }, + { name: "hexadecimal", element: "0x2A", expected: 42 }, + { + name: "unsafe hexadecimal", + element: "0x20000000000001", + expected: { $plist_type: "integer", decimal: "9007199254740993" }, + }, + { + name: "integer beyond finite Number range", + element: `1${"0".repeat(400)}`, + expected: { $plist_type: "integer", decimal: `1${"0".repeat(400)}` }, + }, +])( + "preserves $name XML numbers through CLI and MCP", + async (scenario, { cli }) => { + const root = await createTestTempDirectory("rea-keyed-xml-numbers-"); + const bytes = Buffer.from( + build({ + $archiver: "NSKeyedArchiver", + $version: 100000, + $objects: ["$null", "__VALUE__"], + $top: { root: { CF$UID: 1 } }, + }).replace("__VALUE__", scenario.element), + ); + const path = join(root, "archive.plist"); + await writeFile(path, bytes); + const { call } = await connectLocalToolsMcp({ + HOME: root, + USERPROFILE: root, + }); + const output = await cli.run({ + arguments: [ + "inspect-keyed-archive", + path, + "--root", + "root", + "--offset", + "1", + "--limit", + "1", + "--json", + ], + environment: { HOME: root, USERPROFILE: root, REA_LOG_LEVEL: "silent" }, + }); + expect(output.exitCode, output.stderr).toBe(0); + const result = keyedArchiveResultSchema.parse( + parseEvidence(output.json).normalized_result, + ); + expect((await call("open_binary", { path })).isError).not.toBe(true); + const mcp = await call("inspect_keyed_archive", { + root: "root", + offset: 1, + limit: 1, + }); + expect(mcp.isError, JSON.stringify(mcp)).not.toBe(true); + expect( + keyedArchiveResultSchema.parse( + parseEvidence(mcp.structuredContent).normalized_result, + ), + ).toEqual(result); + expect(result.archive_sha256).toBe( + createHash("sha256").update(bytes).digest("hex"), + ); + expect(result.objects).toEqual([ + { + id: 1, + kind: "scalar", + class_id: null, + class_name: null, + status: "observed", + value: scenario.expected, + }, + ]); + expect(result.references).toEqual([ + expect.objectContaining({ + source: null, + path: ["root"], + target: 1, + status: "resolved", + }), + ]); + expect( + result.limitations.some((note) => + /non-finite|completely observed/u.test(note), + ), + ).toBe(false); + expect(await readFile(path)).toEqual(bytes); + }, +); diff --git a/tests/boundary/filesystem/keyedArchiveScalarIdentity.test.ts b/tests/boundary/filesystem/keyedArchiveScalarIdentity.test.ts index 47756552ff..1f1ba969ad 100644 --- a/tests/boundary/filesystem/keyedArchiveScalarIdentity.test.ts +++ b/tests/boundary/filesystem/keyedArchiveScalarIdentity.test.ts @@ -9,7 +9,12 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; -const cases = ["unsafe-integer", "integral-real"] as const; +const cases = [ + "unsafe-integer", + "integral-real", + "hexadecimal-integer", + "unsafe-hexadecimal-integer", +] as const; const fixture = async (kind: (typeof cases)[number]) => { const root = await createTestTempDirectory("rea-integer-scalar-"); const xml = build({ @@ -21,7 +26,11 @@ const fixture = async (kind: (typeof cases)[number]) => { "__VALUE__", kind === "unsafe-integer" ? "9007199254740993" - : "9007199254740992", + : kind === "hexadecimal-integer" + ? "0x2A" + : kind === "unsafe-hexadecimal-integer" + ? "0x20000000000001" + : "9007199254740992", ); const bytes = Buffer.from(xml); const path = join(root, "archive.plist"); @@ -65,8 +74,22 @@ const verify = ( expect(result.truncated).toBe(false); const value = result.objects.find(({ id }) => id === 1)?.value; expect(value).toBeDefined(); + const precisionNote = result.limitations.some( + (note) => + /integer|number/iu.test(note) && + /precision|exact range|rounded|completely observed/iu.test(note), + ); if (kind === "integral-real") expect(value).toBe(9007199254740992); - else { + else if (kind === "hexadecimal-integer") { + // Apple's parser reads 0x2A as 42; the literal is classified, not unknown. + expect(value).toBe(42); + expect(precisionNote).toBe(false); + } else if (kind === "unsafe-hexadecimal-integer") { + expect(value).toEqual({ + $plist_type: "integer", + decimal: "9007199254740993", + }); + } else { const exact = typeof value === "object" && value !== null && diff --git a/tests/fixtures/keyedArchiveNumberMetadata.ts b/tests/fixtures/keyedArchiveNumberMetadata.ts index f63b86fb0b..9369692c93 100644 --- a/tests/fixtures/keyedArchiveNumberMetadata.ts +++ b/tests/fixtures/keyedArchiveNumberMetadata.ts @@ -14,8 +14,6 @@ const ROUNDED = 9007199254740992; /** Accepted decoder representations whose supplemental metadata needs care. */ export const mixedArchiveNumberElements = [ - "9007199254740992", - "9007199254740992", "90071992547409923", "9007199254740992", "3", @@ -27,6 +25,23 @@ export const mixedArchiveNumberElements = [ /** Ordinary dictionaries and malformed numeric references at the same boundary. */ export const archiveNumberMetadataCases = [ + ...[ + { + element: "9007199254740992", + value: 90071992547409920, + }, + { + element: "9007199254740992", + value: Number("19007199254740992"), + }, + ].map(({ element, value }) => ({ + name: element, + source: { first: "__INTEGER__", other: "__OTHER__" }, + element, + expected: { first: EXACT_INTEGER, other: value }, + incomplete: false, + malformedReference: false, + })), ...mixedArchiveNumberElements.map((element) => ({ name: element, source: { first: "__INTEGER__", other: "__OTHER__" },