Skip to content

Commit

Permalink
Feature: validate BPS & UPS patch self checksums (#815)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmercm authored Nov 16, 2023
1 parent 5629ab6 commit b851854
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 6 deletions.
13 changes: 12 additions & 1 deletion src/types/files/fileChecksums.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from 'node:crypto';
import { Stream } from 'node:stream';
import { Readable, Stream } from 'node:stream';

import { crc32 } from '@node-rs/crc32';

Expand All @@ -17,6 +17,17 @@ export interface ChecksumProps {
}

export default class FileChecksums {
public static async hashData(
data: Buffer | string,
checksumBitmask: number,
): Promise<ChecksumProps> {
const readable = new Readable();
readable.push(data);
// eslint-disable-next-line unicorn/no-null
readable.push(null);
return this.hashStream(readable, checksumBitmask);
}

public static async hashStream(
stream: Stream,
checksumBitmask: number,
Expand Down
14 changes: 12 additions & 2 deletions src/types/patches/bpsPatch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FilePoly from '../../polyfill/filePoly.js';
import fsPoly from '../../polyfill/fsPoly.js';
import File from '../files/file.js';
import FileChecksums, { ChecksumBitmask } from '../files/fileChecksums.js';
import Patch from './patch.js';

enum BPSAction {
Expand All @@ -27,15 +28,24 @@ export default class BPSPatch extends Patch {
await file.extractToTempFilePoly('r', async (patchFile) => {
patchFile.seek(BPSPatch.FILE_SIGNATURE.length);
await Patch.readUpsUint(patchFile); // source size
targetSize = await Patch.readUpsUint(patchFile); // target size
targetSize = await Patch.readUpsUint(patchFile);

patchFile.seek(patchFile.getSize() - 12);
crcBefore = (await patchFile.readNext(4)).reverse().toString('hex');
crcAfter = (await patchFile.readNext(4)).reverse().toString('hex');

// Validate the patch contents
const patchChecksumExpected = (await patchFile.readNext(4)).reverse().toString('hex');
patchFile.seek(0);
const patchData = await patchFile.readNext(patchFile.getSize() - 4);
const patchChecksumsActual = await FileChecksums.hashData(patchData, ChecksumBitmask.CRC32);
if (patchChecksumsActual.crc32 !== patchChecksumExpected) {
throw new Error(`BPS patch is invalid, CRC of contents (${patchChecksumsActual.crc32}) doesn't match expected (${patchChecksumExpected}): ${file.toString()}`);
}
});

if (crcBefore.length !== 8 || crcAfter.length !== 8) {
throw new Error(`Couldn't parse base file CRC for patch: ${file.toString()}`);
throw new Error(`couldn't parse base file CRC for patch: ${file.toString()}`);
}

return new BPSPatch(file, crcBefore, crcAfter, targetSize);
Expand Down
12 changes: 11 additions & 1 deletion src/types/patches/upsPatch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FilePoly from '../../polyfill/filePoly.js';
import fsPoly from '../../polyfill/fsPoly.js';
import File from '../files/file.js';
import FileChecksums, { ChecksumBitmask } from '../files/fileChecksums.js';
import Patch from './patch.js';

/**
Expand All @@ -24,11 +25,20 @@ export default class UPSPatch extends Patch {
await file.extractToTempFilePoly('r', async (patchFile) => {
patchFile.seek(UPSPatch.FILE_SIGNATURE.length);
await Patch.readUpsUint(patchFile); // source size
targetSize = await Patch.readUpsUint(patchFile); // target size
targetSize = await Patch.readUpsUint(patchFile);

patchFile.seek(patchFile.getSize() - 12);
crcBefore = (await patchFile.readNext(4)).reverse().toString('hex');
crcAfter = (await patchFile.readNext(4)).reverse().toString('hex');

// Validate the patch contents
const patchChecksumExpected = (await patchFile.readNext(4)).reverse().toString('hex');
patchFile.seek(0);
const patchData = await patchFile.readNext(patchFile.getSize() - 4);
const patchChecksumsActual = await FileChecksums.hashData(patchData, ChecksumBitmask.CRC32);
if (patchChecksumsActual.crc32 !== patchChecksumExpected) {
throw new Error(`UPS patch is invalid, CRC of contents (${patchChecksumsActual.crc32}) doesn't match expected (${patchChecksumExpected}): ${file.toString()}`);
}
});

if (crcBefore.length !== 8 || crcAfter.length !== 8) {
Expand Down
2 changes: 1 addition & 1 deletion test/types/patches/bpsPatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('constructor', () => {
Buffer.from('foobar'),
])('should throw on bad patch: %s', async (patchContents) => {
const patchFile = await writeTemp('patch.bps', patchContents);
await expect(BPSPatch.patchFrom(patchFile)).rejects.toThrow(/couldn't parse/i);
await expect(BPSPatch.patchFrom(patchFile)).rejects.toThrow();
});

test.each([
Expand Down
2 changes: 1 addition & 1 deletion test/types/patches/upsPatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('constructor', () => {
Buffer.from('foobar'),
])('should throw on bad patch: %s', async (patchContents) => {
const patchFile = await writeTemp('patch.bps', patchContents);
await expect(UPSPatch.patchFrom(patchFile)).rejects.toThrow(/couldn't parse/i);
await expect(UPSPatch.patchFrom(patchFile)).rejects.toThrow();
});

test.each([
Expand Down

0 comments on commit b851854

Please sign in to comment.