mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 03:15:54 +00:00
fix(apple): preserve post-table plist objects (#7820)
This commit is contained in:
1 parent
bd687bbc40
commit
675c73fb4d
4 files changed
+95
-20
No files matched your search
@@ -4,6 +4,7 @@ import { parseBinary } from "plist";
|
||||
|
||||
import { createPlistNumberProjection } from "../../domain/apple/plistNumbers.js";
|
||||
import { projectPlistValue } from "../../domain/apple/plistValue.js";
|
||||
import { readBinaryPlistLayout } from "./BinaryPlistLayout.js";
|
||||
import {
|
||||
binaryDateRequiresExactSeconds,
|
||||
decodeBinaryPlistScalars,
|
||||
@@ -267,7 +268,12 @@ describe("binary plist UID widths", () => {
|
||||
plan,
|
||||
);
|
||||
if (normalizedWidth === undefined) expect(compatible).toBe(bytes);
|
||||
else expect(compatible[8 + width + 1]).toBe(0x80 | (normalizedWidth - 1));
|
||||
else {
|
||||
const layout = readBinaryPlistLayout(compatible);
|
||||
if (!layout.ok) throw new Error("Invalid compatible plist layout");
|
||||
const [offset] = layout.value.objectOffsets();
|
||||
expect(compatible[Number(offset)]).toBe(0x80 | (normalizedWidth - 1));
|
||||
}
|
||||
expect(parseBinary(compatible)).toEqual({
|
||||
UID: Number(value),
|
||||
});
|
||||
@@ -354,7 +360,10 @@ describe("binary plist number rewriting", () => {
|
||||
).toThrow(/budget/u);
|
||||
const decoded = decodeBinaryPlistScalars(bytes, metadata, plan);
|
||||
expect(decoded).not.toBe(bytes);
|
||||
expect(decoded.readDoubleBE(29)).toBeNaN();
|
||||
const layout = readBinaryPlistLayout(decoded);
|
||||
if (!layout.ok) throw new Error("Invalid compatible plist layout");
|
||||
const offsets = [...layout.value.objectOffsets()];
|
||||
expect(decoded.readDoubleBE(Number(offsets[2]) + 1)).toBeNaN();
|
||||
const parsed = projectPlistValue(parseBinary(decoded));
|
||||
expect(
|
||||
createPlistNumberProjection(metadata.literals).project(parsed.value),
|
||||
|
||||
@@ -146,7 +146,7 @@ const binaryPlistContainerRecord = (
|
||||
return record;
|
||||
};
|
||||
|
||||
/** Exact added offset-table representation size, shared with preflight. */
|
||||
/** Select a width that can address the appended clone records and table. */
|
||||
export const binaryPlistCloneOffsetWidth = (
|
||||
currentWidth: number,
|
||||
firstCloneOffset: number,
|
||||
@@ -461,31 +461,31 @@ const cloneBinaryPlistObjects = (
|
||||
const trailerOffset = sourceLayout.trailerOffset;
|
||||
const offsetSize = sourceLayout.offsetWidth;
|
||||
const objectCount = Number(sourceLayout.objectCount);
|
||||
const oldTableOffset = Number(sourceLayout.offsetTable);
|
||||
const offsets = [...sourceLayout.objectOffsets()];
|
||||
const cloneEntries = [...replacements.entries()];
|
||||
const replacementLengths = cloneEntries.map(([, record]) => record.length);
|
||||
const newOffsetSize =
|
||||
normalizedOffsetSize ??
|
||||
binaryPlistCloneOffsetWidth(offsetSize, oldTableOffset, replacementLengths);
|
||||
binaryPlistCloneOffsetWidth(offsetSize, trailerOffset, replacementLengths);
|
||||
if (newOffsetSize === undefined)
|
||||
throw new RangeError(
|
||||
"binary plist cloned scalar offset exceeds supported width",
|
||||
);
|
||||
const cloneStart = oldTableOffset;
|
||||
// Preserve every source offset, including objects after the old table. The
|
||||
// decoder copy keeps the old table as inert bytes and appends a new one.
|
||||
const cloneStart = trailerOffset;
|
||||
const cloneBytes = replacementLengths.reduce(
|
||||
(sum, length) => sum + length,
|
||||
0,
|
||||
);
|
||||
const newTableOffset = cloneStart + cloneBytes;
|
||||
const newLength =
|
||||
bytes.length + cloneBytes + objectCount * (newOffsetSize - offsetSize);
|
||||
const newLength = bytes.length + cloneBytes + objectCount * newOffsetSize;
|
||||
if (!Number.isSafeInteger(newLength))
|
||||
throw new RangeError(
|
||||
"binary plist cloned scalar representation is too large",
|
||||
);
|
||||
const compatible = Buffer.alloc(newLength);
|
||||
bytes.copy(compatible, 0, 0, oldTableOffset);
|
||||
bytes.copy(compatible, 0, 0, trailerOffset);
|
||||
const cloneOffsetBySource = new Map<number, bigint>();
|
||||
let cloneCursor = cloneStart;
|
||||
cloneEntries.forEach(([sourceOffset, replacement]) => {
|
||||
@@ -496,8 +496,6 @@ const cloneBinaryPlistObjects = (
|
||||
});
|
||||
const tableStart = newTableOffset;
|
||||
const tableEnd = tableStart + objectCount * newOffsetSize;
|
||||
const oldTableEnd = oldTableOffset + objectCount * offsetSize;
|
||||
const padding = bytes.subarray(oldTableEnd, trailerOffset);
|
||||
offsets.forEach((sourceOffset, index) => {
|
||||
const sourceNumber =
|
||||
sourceOffset <= BigInt(Number.MAX_SAFE_INTEGER)
|
||||
@@ -519,7 +517,6 @@ const cloneBinaryPlistObjects = (
|
||||
if (normalizedReferenceSize !== undefined)
|
||||
trailer[7] = normalizedReferenceSize;
|
||||
trailer.writeBigUInt64BE(BigInt(newTableOffset), 24);
|
||||
padding.copy(compatible, tableEnd);
|
||||
trailer.copy(compatible, tableEnd + padding.length);
|
||||
trailer.copy(compatible, tableEnd);
|
||||
return compatible;
|
||||
};
|
||||
@@ -561,9 +561,11 @@ export const estimateBinaryPlistExpansion = (
|
||||
},
|
||||
);
|
||||
const normalizeOffsetWidth = (cloneLengths: readonly number[]) => {
|
||||
if (cloneLengths.length === 0 && [1, 2, 4, 8].includes(offsetSize))
|
||||
return offsetSize;
|
||||
const width = binaryPlistCloneOffsetWidth(
|
||||
offsetSize,
|
||||
offsetTable,
|
||||
trailer,
|
||||
cloneLengths,
|
||||
);
|
||||
if (width === undefined)
|
||||
@@ -576,10 +578,13 @@ export const estimateBinaryPlistExpansion = (
|
||||
...containerCloneLengths,
|
||||
];
|
||||
const normalizedOffsetSize = normalizeOffsetWidth(cloneLengths);
|
||||
if (cloneLengths.length > 0 || normalizedOffsetSize !== offsetSize) {
|
||||
if (
|
||||
cloneLengths.length > 0 ||
|
||||
normalizedOffsetSize !== offsetSize ||
|
||||
normalizedReferenceSize !== referenceSize
|
||||
) {
|
||||
const cloneBytes = cloneLengths.reduce((sum, length) => sum + length, 0);
|
||||
estimate +=
|
||||
2 * (cloneBytes + objectCount * (normalizedOffsetSize - offsetSize));
|
||||
estimate += 2 * (cloneBytes + objectCount * normalizedOffsetSize);
|
||||
if (estimate > maxBytes)
|
||||
throw fail(
|
||||
"expansion",
|
||||
@@ -648,10 +653,13 @@ export const estimateBinaryPlistExpansion = (
|
||||
...Array.from(cloneOffsets, () => 9),
|
||||
];
|
||||
const normalizedOffsetSize = normalizeOffsetWidth(cloneLengths);
|
||||
if (cloneLengths.length > 0 || normalizedOffsetSize !== offsetSize) {
|
||||
if (
|
||||
cloneLengths.length > 0 ||
|
||||
normalizedOffsetSize !== offsetSize ||
|
||||
normalizedReferenceSize !== referenceSize
|
||||
) {
|
||||
const cloneBytes = cloneLengths.reduce((sum, length) => sum + length, 0);
|
||||
estimate +=
|
||||
2 * (cloneBytes + objectCount * (normalizedOffsetSize - offsetSize));
|
||||
estimate += 2 * (cloneBytes + objectCount * normalizedOffsetSize);
|
||||
if (estimate > maxBytes)
|
||||
throw fail(
|
||||
"expansion",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
// Inert NSKeyedArchiver fixture: the final string follows the offset table.
|
||||
const source = Buffer.from(
|
||||
"YnBsaXN0MDBVJG51bGzUAwUGBwQICQtZJGFyY2hpdmVyXxAPTlNLZXllZEFyY2hpdmVyWCRvYmplY3RzVCR0b3BYJHZlcnNpb26iAAHRCgxUcm9vdBIAAYaggAEAAAgAAIMAAA4AABcAACEAADMAADwAAEEAAEoAAE0AAFAAAFUAAFpVaGVsbG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAQAAAAAAAAANAAAAAAAAAAIAAAAAAAAAXA==",
|
||||
"base64",
|
||||
);
|
||||
|
||||
const fixture = (width: number, postTable: boolean): Buffer => {
|
||||
const oldTable = 92;
|
||||
const count = 13;
|
||||
const oldEnd = oldTable + count * 3;
|
||||
const trailer = source.length - 32;
|
||||
const content = source.subarray(oldEnd, trailer);
|
||||
const table = postTable ? oldTable : oldTable + content.length;
|
||||
const result = Buffer.alloc(trailer - count * 3 + count * width + 32);
|
||||
source.copy(result, 0, 0, oldTable);
|
||||
content.copy(result, postTable ? table + count * width : oldTable);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const value = source.readUIntBE(oldTable + i * 3, 3);
|
||||
const relocated =
|
||||
value === oldEnd ? (postTable ? table + count * width : oldTable) : value;
|
||||
result.writeUIntBE(relocated, table + i * width, width);
|
||||
}
|
||||
const newTrailer = result.length - 32;
|
||||
source.copy(result, newTrailer, trailer);
|
||||
result[newTrailer + 6] = width;
|
||||
result.writeBigUInt64BE(BigInt(table), newTrailer + 24);
|
||||
return result;
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"preserves post-table keyed archive strings through offset normalization",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-keyed-post-table-");
|
||||
for (const [width, postTable] of [
|
||||
[1, true],
|
||||
[3, true],
|
||||
[4, true],
|
||||
[3, false],
|
||||
] as const) {
|
||||
const path = join(root, `${width}-${String(postTable)}.bplist`);
|
||||
const original = fixture(width, postTable);
|
||||
await writeFile(path, original);
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-keyed-archive", path, "--format", "json"],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
const json = JSON.parse(result.stdout);
|
||||
expect(json.normalized_result.objects[1]).toMatchObject({
|
||||
status: "observed",
|
||||
value: "hello",
|
||||
});
|
||||
expect((await readFile(path)).equals(original)).toBe(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user