fix(apple): preserve exact integer precision with post-table plist objects (#8910)

This commit is contained in:
highmachs authored and GitHub committed 2026-11-05 18:30:59 +08:00
1 parent 027c768f07
commit 281dbc8514
3 files changed
+113 -1

No files matched your search

@@ -723,6 +723,61 @@ describe("binary keyed archive dates", () => {
});
});
describe("binary keyed archive post-table objects", () => {
it("preserves exact integer decimals when an archive string follows the offset table", () => {
const original = Buffer.from(
buildBinary({
$archiver: "NSKeyedArchiver",
$version: 100000,
$top: { root: { UID: 2 } },
$objects: [
"$null",
"hello",
9007199254740993n as unknown as PlistValue,
],
}),
);
const intMarkerOffset = original.indexOf(0x13);
expect(intMarkerOffset).toBeGreaterThan(0);
// Overwrite the 8-byte integer payload with exact 9007199254740993n
original.writeBigInt64BE(9007199254740993n, intMarkerOffset + 1);
const stringBytes = Buffer.from("\x55hello", "latin1");
const stringOffset = original.indexOf(stringBytes);
expect(stringOffset).toBeGreaterThan(0);
const trailerOffset = original.length - 32;
const tableOffset = Number(original.readBigUInt64BE(trailerOffset + 24));
const count = Number(original.readBigUInt64BE(trailerOffset + 8));
const width = original[trailerOffset + 6] ?? 1;
const post = Buffer.concat([
original.subarray(0, trailerOffset),
stringBytes,
original.subarray(trailerOffset),
]);
const newTrailerOffset = post.length - 32;
for (let i = 0; i < count; i += 1) {
const offset = post.readUIntBE(tableOffset + i * width, width);
if (offset === stringOffset) {
post.writeUIntBE(trailerOffset, tableOffset + i * width, width);
}
}
post.writeBigUInt64BE(BigInt(tableOffset), newTrailerOffset + 24);
const graph = decodeKeyedArchiveBytes(post, { offset: 0, limit: 10 });
expect(graph.objects[2]).toMatchObject({
id: 2,
status: "observed",
value: { $plist_type: "integer", decimal: "9007199254740993" },
});
expect(
graph.limitations.some((lim) =>
lim.includes("could not be completely observed"),
),
).toBe(false);
});
});
describe("binary plist date object region", () => {
it("refuses a binary date that extends past the object region", () => {
// Strict bounds end objects at the offset table. Four payload bytes
+50
View File
@@ -620,3 +620,53 @@ it.each([4n, 0xffffffffffffffffn])(
).toThrow("binary plist trailer or object table is invalid");
},
);
it("preserves exact pre-table integer precision when a string follows the offset table", () => {
const stringRecord = Buffer.from("\x55hello", "latin1");
const intRecord = int64(9007199254740993n);
const intOffset = 8;
const tableOffset = intOffset + intRecord.length;
const stringOffset = tableOffset + 2;
const trailerOffset = stringOffset + stringRecord.length;
const bytes = Buffer.alloc(trailerOffset + 32);
Buffer.from("bplist00", "ascii").copy(bytes, 0);
intRecord.copy(bytes, intOffset);
stringRecord.copy(bytes, stringOffset);
bytes[tableOffset] = intOffset;
bytes[tableOffset + 1] = stringOffset;
bytes[trailerOffset + 6] = 1;
bytes[trailerOffset + 7] = 1;
bytes.writeBigUInt64BE(2n, trailerOffset + 8);
bytes.writeBigUInt64BE(0n, trailerOffset + 16);
bytes.writeBigUInt64BE(BigInt(tableOffset), trailerOffset + 24);
const metadata = scanBinaryPlistScalars(bytes);
expect(metadata.literals.incomplete).toBe(false);
expect(metadata.literals.unsafeIntegerLiterals).toBe(1);
const decimals = metadata.literals.integers.get(Number(9007199254740993n));
expect(decimals).toBeDefined();
expect([...decimals!]).toEqual(["9007199254740993"]);
});
it("marks literals incomplete when a numeric scalar follows the offset table", () => {
const intRecord = int64(9007199254740993n);
const intOffset = 8;
const postInt = int64(42n);
const tableOffset = intOffset + intRecord.length;
const postOffset = tableOffset + 2;
const trailerOffset = postOffset + postInt.length;
const bytes = Buffer.alloc(trailerOffset + 32);
Buffer.from("bplist00", "ascii").copy(bytes, 0);
intRecord.copy(bytes, intOffset);
postInt.copy(bytes, postOffset);
bytes[tableOffset] = intOffset;
bytes[tableOffset + 1] = postOffset;
bytes[trailerOffset + 6] = 1;
bytes[trailerOffset + 7] = 1;
bytes.writeBigUInt64BE(2n, trailerOffset + 8);
bytes.writeBigUInt64BE(0n, trailerOffset + 16);
bytes.writeBigUInt64BE(BigInt(tableOffset), trailerOffset + 24);
const metadata = scanBinaryPlistScalars(bytes);
expect(metadata.literals.incomplete).toBe(true);
});
+8 -1
View File
@@ -268,13 +268,19 @@ export const scanBinaryPlistScalars = (
const capacity = Math.floor((trailer - offsetTable) / offsetWidth);
if (countRaw > BigInt(capacity)) return result(true);
const count = Number(countRaw);
const tableEnd = offsetTable + count * offsetWidth;
if (referenceWidth < 1) return result(true);
const objectOffsets: number[] = [];
let incomplete = false;
for (let index = 0; index < count; index += 1) {
const rawOffset = sourceLayout.objectOffset(index);
const offset = rawOffset > safeMaximum ? undefined : Number(rawOffset);
if (offset === undefined || offset < 8 || offset >= offsetTable) {
if (
offset === undefined ||
offset < 8 ||
(offset >= offsetTable && offset < tableEnd) ||
offset >= trailer
) {
incomplete = true;
continue;
}
@@ -324,6 +330,7 @@ export const scanBinaryPlistScalars = (
const marker = bytes[offset] ?? 0;
const type = marker >> 4;
if (type === 3 && marker === 0x33) {
if (offset + 9 > offsetTable) incomplete = true;
continue;
}
if (type !== 1 && type !== 2) continue;