Skip to content

Commit c5b972e

Browse files
sunnylqmclaude
andcommitted
feat(diff): record copiesCrc for every copied entry in package diffs
Same-path copies previously carried no content checksum, so a client applying a pdiff on a rebuilt binary could copy a path-matched but content-drifted resource unverified (BUNDLEHASH_MIGRATION.md §4.2.1). Existing clients only consult copiesCrc when the path lookup fails, so the extra entries are backward-compatible; verification lands with the client-side consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e5e43a2 commit c5b972e

2 files changed

Lines changed: 19 additions & 11 deletions

File tree

src/diff.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -663,11 +663,14 @@ async function diffFromPackage(
663663

664664
let originSource: BundleSource | undefined;
665665

666-
// Content checksum (CRC32) for entries that are copied from a *different*
667-
// path in the origin package ("moved" entries). On Android these are the
668-
// res/ drawables (images), whose on-device path differs between an APK
669-
// baseline and an AAB(split-apk) install due to resource path shortening,
670-
// so the client cannot locate them by path and must fall back to content.
666+
// Content checksum (CRC32) for every copied entry, keyed by target path.
667+
// Two consumers on the client: locating a file by content when its origin
668+
// path is not present verbatim on device (APK baseline diff applied on an
669+
// AAB/split-apk install whose res/ paths were shortened), and verifying
670+
// that a path-matched file still has the expected bytes before copying
671+
// (BUNDLEHASH_MIGRATION.md §4.2.1 — a rebuilt binary may keep the path but
672+
// drift the content; mismatch must fall back to full instead of silently
673+
// installing a wrong resource).
671674
const copiesCrc: Record<string, number> = {};
672675

673676
await enumZipEntries(origin, async (entry, zipFile) => {
@@ -723,15 +726,13 @@ async function diffFromPackage(
723726
// If same file.
724727
if (originEntries[entry.fileName] === entry.crc32) {
725728
copies[entry.fileName] = '';
729+
copiesCrc[entry.fileName] = entry.crc32;
726730
return;
727731
}
728732
// If moved from other place
729733
const movedFrom = originMap[entry.crc32];
730734
if (movedFrom) {
731735
copies[entry.fileName] = movedFrom;
732-
// Record the content checksum so the client can locate this file by
733-
// content when the origin path does not exist verbatim on device
734-
// (APK baseline -> AAB install path shortening).
735736
copiesCrc[entry.fileName] = entry.crc32;
736737
return;
737738
}

tests/diff.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ describe('diff commands', () => {
452452
expect(result.files['assets/new.txt']?.toString('utf-8')).toBe('new-file');
453453
});
454454

455-
test('hdiffFromApk emits copiesCrc for moved (res/) entries only', async () => {
455+
test('hdiffFromApk emits copiesCrc for every copied entry', async () => {
456456
const originPath = path.join(tempRoot, 'origin-crc.apk');
457457
const nextPath = path.join(tempRoot, 'next-crc.ppk');
458458
const outputPath = path.join(tempRoot, 'out', 'apk-crc-diff.ppk');
@@ -506,9 +506,16 @@ describe('diff commands', () => {
506506
'res/drawable-xhdpi-v4/x.webp',
507507
);
508508
expect(diffMeta.copiesCrc['drawable-xhdpi/x.webp']).toBe(originImageCrc);
509-
// same-path asset -> no crc needed (efficiency: only moved entries get one)
509+
// same-path asset -> crc recorded too, so the client can verify a
510+
// path-matched file before copying (rebuilt binary may drift content)
511+
let keepCrc = -1;
512+
await enumZipEntries(originPath, async (entry) => {
513+
if (entry.fileName === 'assets/keep.txt') {
514+
keepCrc = entry.crc32;
515+
}
516+
});
510517
expect(diffMeta.copies['assets/keep.txt']).toBe('');
511-
expect(diffMeta.copiesCrc['assets/keep.txt']).toBeUndefined();
518+
expect(diffMeta.copiesCrc['assets/keep.txt']).toBe(keepCrc);
512519
});
513520

514521
test('hdiffFromApk does not match OTA files against excluded native entries', async () => {

0 commit comments

Comments
 (0)