diff --git a/src/types/files/fileChecksums.ts b/src/types/files/fileChecksums.ts index 87db86e9e..01739e373 100644 --- a/src/types/files/fileChecksums.ts +++ b/src/types/files/fileChecksums.ts @@ -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'; @@ -17,6 +17,17 @@ export interface ChecksumProps { } export default class FileChecksums { + public static async hashData( + data: Buffer | string, + checksumBitmask: number, + ): Promise { + 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, diff --git a/src/types/patches/bpsPatch.ts b/src/types/patches/bpsPatch.ts index bde039f89..ca19ba512 100644 --- a/src/types/patches/bpsPatch.ts +++ b/src/types/patches/bpsPatch.ts @@ -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 { @@ -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); diff --git a/src/types/patches/upsPatch.ts b/src/types/patches/upsPatch.ts index debe5aa16..b2c28b4d3 100644 --- a/src/types/patches/upsPatch.ts +++ b/src/types/patches/upsPatch.ts @@ -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'; /** @@ -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) { diff --git a/test/types/patches/bpsPatch.test.ts b/test/types/patches/bpsPatch.test.ts index 16073913b..f99cd01a7 100644 --- a/test/types/patches/bpsPatch.test.ts +++ b/test/types/patches/bpsPatch.test.ts @@ -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([ diff --git a/test/types/patches/upsPatch.test.ts b/test/types/patches/upsPatch.test.ts index 3e1f47287..af06f6685 100644 --- a/test/types/patches/upsPatch.test.ts +++ b/test/types/patches/upsPatch.test.ts @@ -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([