From ec8d897e668ba9dab8ffaba94e96ed8239e41536 Mon Sep 17 00:00:00 2001 From: Christian Emmer <10749361+emmercm@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:08:46 -0400 Subject: [PATCH] Refactor: don't log the stack trace of expected errors (#1203) --- index.ts | 5 ++++- package.ts | 9 +++++---- src/igir.ts | 7 ++++--- src/modules/argumentsParser.ts | 30 +++++++++++++++------------- src/polyfill/fsPoly.ts | 9 +++++---- src/types/dats/cmpro/cmProParser.ts | 6 ++++-- src/types/expectedError.ts | 4 ++++ src/types/files/archives/rar.ts | 3 ++- src/types/files/archives/sevenZip.ts | 5 +++-- src/types/files/archives/tar.ts | 3 ++- src/types/files/archives/zip.ts | 3 ++- src/types/files/fileFactory.ts | 5 +++-- src/types/options.ts | 5 +++-- src/types/outputFactory.ts | 3 ++- src/types/patches/apsGbaPatch.ts | 5 +++-- src/types/patches/apsN64Patch.ts | 7 ++++--- src/types/patches/bpsPatch.ts | 11 +++++----- src/types/patches/dpsPatch.ts | 5 +++-- src/types/patches/ipsPatch.ts | 3 ++- src/types/patches/ninjaPatch.ts | 9 +++++---- src/types/patches/patch.ts | 3 ++- src/types/patches/ppfPatch.ts | 9 +++++---- src/types/patches/upsPatch.ts | 11 +++++----- src/types/patches/vcdiffPatch.ts | 7 ++++--- 24 files changed, 99 insertions(+), 68 deletions(-) create mode 100644 src/types/expectedError.ts diff --git a/index.ts b/index.ts index 154a89b8c..b5284fce2 100644 --- a/index.ts +++ b/index.ts @@ -12,6 +12,7 @@ import Igir from './src/igir.js'; import ArgumentsParser from './src/modules/argumentsParser.js'; import EndOfLifeChecker from './src/modules/endOfLifeChecker.js'; import UpdateChecker from './src/modules/updateChecker.js'; +import ExpectedError from './src/types/expectedError.js'; import Options from './src/types/options.js'; // Monkey-patch 'fs' to help prevent Windows EMFILE errors @@ -69,7 +70,9 @@ gracefulFs.gracefulify(realFs); await ProgressBarCLI.stop(); } catch (error) { await ProgressBarCLI.stop(); - if (error instanceof Error && error.stack) { + if (error instanceof ExpectedError) { + logger.error(error); + } else if (error instanceof Error && error.stack) { // Log the stack trace to help with bug reports logger.error(error.stack); } else { diff --git a/package.ts b/package.ts index 088bc5226..fb0d7699a 100644 --- a/package.ts +++ b/package.ts @@ -12,6 +12,7 @@ import Logger from './src/console/logger.js'; import LogLevel from './src/console/logLevel.js'; import Package from './src/globals/package.js'; import FsPoly from './src/polyfill/fsPoly.js'; +import ExpectedError from './src/types/expectedError.js'; interface FileFilter extends GlobOptions { include?: string, @@ -25,7 +26,7 @@ const fileFilter = (filters: FileFilter[]): string[] => { const include = fg.globSync(filter.include.replace(/\\/g, '/'), filter) .map((file) => path.resolve(file)); if (include.length === 0) { - throw new Error(`glob pattern '${filter.include}' returned no paths`); + throw new ExpectedError(`glob pattern '${filter.include}' returned no paths`); } results = [...results, ...include]; } @@ -33,7 +34,7 @@ const fileFilter = (filters: FileFilter[]): string[] => { const exclude = new Set(fg.globSync(filter.exclude.replace(/\\/g, '/'), filter) .map((file) => path.resolve(file))); if (exclude.size === 0) { - throw new Error(`glob pattern '${filter.exclude}' returned no paths`); + throw new ExpectedError(`glob pattern '${filter.exclude}' returned no paths`); } results = results.filter((result) => !exclude.has(result)); } @@ -54,7 +55,7 @@ const fileFilter = (filters: FileFilter[]): string[] => { }) .check((_argv) => { if (!_argv.input || !fs.existsSync(_argv.input)) { - throw new Error(`input directory '${_argv.input}' doesn't exist`); + throw new ExpectedError(`input directory '${_argv.input}' doesn't exist`); } return true; }) @@ -121,7 +122,7 @@ const fileFilter = (filters: FileFilter[]): string[] => { }); if (!await FsPoly.exists(output)) { - throw new Error(`output file '${output}' doesn't exist`); + throw new ExpectedError(`output file '${output}' doesn't exist`); } logger.info(`Output: ${FsPoly.sizeReadable(await FsPoly.size(output))}`); diff --git a/src/igir.ts b/src/igir.ts index 50bcb1326..becdc2351 100644 --- a/src/igir.ts +++ b/src/igir.ts @@ -40,6 +40,7 @@ import Timer from './timer.js'; import DAT from './types/dats/dat.js'; import Parent from './types/dats/parent.js'; import DATStatus from './types/datStatus.js'; +import ExpectedError from './types/expectedError.js'; import File from './types/files/file.js'; import FileCache from './types/files/fileCache.js'; import { ChecksumBitmask } from './types/files/fileChecksums.js'; @@ -78,9 +79,9 @@ export default class Igir { this.logger.trace('checking Windows for symlink permissions'); if (!await FsPoly.canSymlink(Temp.getTempDir())) { if (!await isAdmin()) { - throw new Error(`${Package.NAME} does not have permissions to create symlinks, please try running as administrator`); + throw new ExpectedError(`${Package.NAME} does not have permissions to create symlinks, please try running as administrator`); } - throw new Error(`${Package.NAME} does not have permissions to create symlinks`); + throw new ExpectedError(`${Package.NAME} does not have permissions to create symlinks`); } this.logger.trace('Windows has symlink permissions'); } @@ -257,7 +258,7 @@ export default class Igir { const progressBar = await this.logger.addProgressBar('Scanning for DATs'); let dats = await new DATScanner(this.options, progressBar).scan(); if (dats.length === 0) { - throw new Error('No valid DAT files found!'); + throw new ExpectedError('No valid DAT files found!'); } if (dats.length === 1) { diff --git a/src/modules/argumentsParser.ts b/src/modules/argumentsParser.ts index 6adfc0231..02e66d3b8 100644 --- a/src/modules/argumentsParser.ts +++ b/src/modules/argumentsParser.ts @@ -7,6 +7,7 @@ import Defaults from '../globals/defaults.js'; import Package from '../globals/package.js'; import ArrayPoly from '../polyfill/arrayPoly.js'; import ConsolePoly from '../polyfill/consolePoly.js'; +import ExpectedError from '../types/expectedError.js'; import { ChecksumBitmask } from '../types/files/fileChecksums.js'; import ROMHeader from '../types/files/romHeader.js'; import Internationalization from '../types/internationalization.js'; @@ -147,13 +148,13 @@ export default class ArgumentsParser { ['extract', 'zip'].forEach((command) => { if (checkArgv._.includes(command) && ['copy', 'move'].every((write) => !checkArgv._.includes(write))) { - throw new Error(`Command "${command}" also requires the commands copy or move`); + throw new ExpectedError(`Command "${command}" also requires the commands copy or move`); } }); ['test', 'clean'].forEach((command) => { if (checkArgv._.includes(command) && ['copy', 'move', 'link', 'symlink'].every((write) => !checkArgv._.includes(write))) { - throw new Error(`Command "${command}" requires one of the commands: copy, move, or link`); + throw new ExpectedError(`Command "${command}" requires one of the commands: copy, move, or link`); } }); @@ -187,7 +188,7 @@ export default class ArgumentsParser { const needInput = ['copy', 'move', 'link', 'symlink', 'extract', 'zip', 'test', 'dir2dat', 'fixdat'].filter((command) => checkArgv._.includes(command)); if (!checkArgv.input && needInput.length > 0) { // TODO(cememr): print help message - throw new Error(`Missing required argument for command${needInput.length !== 1 ? 's' : ''} ${needInput.join(', ')}: --input `); + throw new ExpectedError(`Missing required argument for command${needInput.length !== 1 ? 's' : ''} ${needInput.join(', ')}: --input `); } return true; }) @@ -227,6 +228,7 @@ export default class ArgumentsParser { type: 'array', requiresArg: true, }) + // TODO(cemmer): don't allow dir2dat & --dat .option('dat-exclude', { group: groupDatInput, description: 'Path(s) to DAT files or archives to exclude from processing (supports globbing)', @@ -296,7 +298,7 @@ export default class ArgumentsParser { } const needDat = ['report'].filter((command) => checkArgv._.includes(command)); if ((!checkArgv.dat || checkArgv.dat.length === 0) && needDat.length > 0) { - throw new Error(`Missing required argument for commands ${needDat.join(', ')}: --dat`); + throw new ExpectedError(`Missing required argument for commands ${needDat.join(', ')}: --dat`); } return true; }) @@ -369,7 +371,7 @@ export default class ArgumentsParser { .check((checkArgv) => { // Re-implement `implies: 'dir-letter'`, which isn't possible with a default value if (checkArgv['dir-letter-count'] > 1 && !checkArgv['dir-letter']) { - throw new Error('Missing dependent arguments:\n dir-letter-count -> dir-letter'); + throw new ExpectedError('Missing dependent arguments:\n dir-letter-count -> dir-letter'); } return true; }) @@ -427,12 +429,12 @@ export default class ArgumentsParser { const needOutput = ['copy', 'move', 'link', 'symlink', 'extract', 'zip', 'clean'].filter((command) => checkArgv._.includes(command)); if (!checkArgv.output && needOutput.length > 0) { // TODO(cememr): print help message - throw new Error(`Missing required argument for command${needOutput.length !== 1 ? 's' : ''} ${needOutput.join(', ')}: --output `); + throw new ExpectedError(`Missing required argument for command${needOutput.length !== 1 ? 's' : ''} ${needOutput.join(', ')}: --output `); } const needClean = ['clean-exclude', 'clean-dry-run'].filter((option) => checkArgv[option]); if (!checkArgv._.includes('clean') && needClean.length > 0) { // TODO(cememr): print help message - throw new Error(`Missing required command for option${needClean.length !== 1 ? 's' : ''} ${needClean.join(', ')}: clean`); + throw new ExpectedError(`Missing required command for option${needClean.length !== 1 ? 's' : ''} ${needClean.join(', ')}: clean`); } return true; }) @@ -456,7 +458,7 @@ export default class ArgumentsParser { } const needZip = ['zip-exclude', 'zip-dat-name'].filter((option) => checkArgv[option]); if (!checkArgv._.includes('zip') && needZip.length > 0) { - throw new Error(`Missing required command for option${needZip.length !== 1 ? 's' : ''} ${needZip.join(', ')}: zip`); + throw new ExpectedError(`Missing required command for option${needZip.length !== 1 ? 's' : ''} ${needZip.join(', ')}: zip`); } return true; }) @@ -487,7 +489,7 @@ export default class ArgumentsParser { } const needLinkCommand = ['symlink'].filter((option) => checkArgv[option]); if (!checkArgv._.includes('link') && !checkArgv._.includes('symlink') && needLinkCommand.length > 0) { - throw new Error(`Missing required command for option${needLinkCommand.length !== 1 ? 's' : ''} ${needLinkCommand.join(', ')}: link`); + throw new ExpectedError(`Missing required command for option${needLinkCommand.length !== 1 ? 's' : ''} ${needLinkCommand.join(', ')}: link`); } return true; }) @@ -558,7 +560,7 @@ export default class ArgumentsParser { .check((checkArgv) => { const invalidLangs = checkArgv['filter-language']?.filter((lang) => !Internationalization.LANGUAGES.includes(lang)); if (invalidLangs !== undefined && invalidLangs.length > 0) { - throw new Error(`Invalid --filter-language language${invalidLangs.length !== 1 ? 's' : ''}: ${invalidLangs.join(', ')}`); + throw new ExpectedError(`Invalid --filter-language language${invalidLangs.length !== 1 ? 's' : ''}: ${invalidLangs.join(', ')}`); } return true; }) @@ -583,7 +585,7 @@ export default class ArgumentsParser { .check((checkArgv) => { const invalidRegions = checkArgv['filter-region']?.filter((lang) => !Internationalization.REGION_CODES.includes(lang)); if (invalidRegions !== undefined && invalidRegions.length > 0) { - throw new Error(`Invalid --filter-region region${invalidRegions.length !== 1 ? 's' : ''}: ${invalidRegions.join(', ')}`); + throw new ExpectedError(`Invalid --filter-region region${invalidRegions.length !== 1 ? 's' : ''}: ${invalidRegions.join(', ')}`); } return true; }) @@ -712,7 +714,7 @@ export default class ArgumentsParser { .check((checkArgv) => { const invalidLangs = checkArgv['prefer-language']?.filter((lang) => !Internationalization.LANGUAGES.includes(lang)); if (invalidLangs !== undefined && invalidLangs.length > 0) { - throw new Error(`Invalid --prefer-language language${invalidLangs.length !== 1 ? 's' : ''}: ${invalidLangs.join(', ')}`); + throw new ExpectedError(`Invalid --prefer-language language${invalidLangs.length !== 1 ? 's' : ''}: ${invalidLangs.join(', ')}`); } return true; }) @@ -728,7 +730,7 @@ export default class ArgumentsParser { .check((checkArgv) => { const invalidRegions = checkArgv['prefer-region']?.filter((lang) => !Internationalization.REGION_CODES.includes(lang)); if (invalidRegions !== undefined && invalidRegions.length > 0) { - throw new Error(`Invalid --prefer-region region${invalidRegions.length !== 1 ? 's' : ''}: ${invalidRegions.join(', ')}`); + throw new ExpectedError(`Invalid --prefer-region region${invalidRegions.length !== 1 ? 's' : ''}: ${invalidRegions.join(', ')}`); } return true; }) @@ -953,7 +955,7 @@ Example use cases: throw err; } this.logger.colorizeYargs(`${_yargs.help().toString().trimEnd()}\n`); - throw new Error(msg); + throw new ExpectedError(msg); }); const yargsArgv = yargsParser diff --git a/src/polyfill/fsPoly.ts b/src/polyfill/fsPoly.ts index cb227128f..a387e830e 100644 --- a/src/polyfill/fsPoly.ts +++ b/src/polyfill/fsPoly.ts @@ -9,6 +9,7 @@ import util from 'node:util'; import { isNotJunk } from 'junk'; import nodeDiskInfo from 'node-disk-info'; +import ExpectedError from '../types/expectedError.js'; import ArrayPoly from './arrayPoly.js'; export type FsWalkCallback = (increment: number) => void; @@ -103,7 +104,7 @@ export default class FsPoly { return await fs.promises.link(target, link); } catch (error) { if (this.onDifferentDrives(target, link)) { - throw new Error(`can't hard link files on different drives: ${error}`); + throw new ExpectedError(`can't hard link files on different drives: ${error}`); } throw error; } @@ -222,7 +223,7 @@ export default class FsPoly { return filePath; } } - throw new Error('failed to generate non-existent temp file'); + throw new ExpectedError('failed to generate non-existent temp file'); } static async mv(oldPath: string, newPath: string, attempt = 1): Promise { @@ -278,7 +279,7 @@ export default class FsPoly { static async readlink(pathLike: PathLike): Promise { if (!await this.isSymlink(pathLike)) { - throw new Error(`can't readlink of non-symlink: ${pathLike}`); + throw new ExpectedError(`can't readlink of non-symlink: ${pathLike}`); } return fs.promises.readlink(pathLike); } @@ -293,7 +294,7 @@ export default class FsPoly { static async realpath(pathLike: PathLike): Promise { if (!await this.exists(pathLike)) { - throw new Error(`can't get realpath of non-existent path: ${pathLike}`); + throw new ExpectedError(`can't get realpath of non-existent path: ${pathLike}`); } return fs.promises.realpath(pathLike); } diff --git a/src/types/dats/cmpro/cmProParser.ts b/src/types/dats/cmpro/cmProParser.ts index 9cbb5838c..31ded0eaf 100644 --- a/src/types/dats/cmpro/cmProParser.ts +++ b/src/types/dats/cmpro/cmProParser.ts @@ -1,3 +1,5 @@ +import ExpectedError from '../../expectedError.js'; + export interface DATProps extends CMProObject { clrmamepro?: ClrMameProProps, game?: GameProps | GameProps[], @@ -172,7 +174,7 @@ export default class CMProParser { private parseQuotedString(): string { if (this.contents.charAt(this.pos) !== '"') { - throw new Error('invalid quoted string'); + throw new ExpectedError('invalid quoted string'); } this.pos += 1; @@ -193,7 +195,7 @@ export default class CMProParser { } } - throw new Error('invalid quoted string'); + throw new ExpectedError('invalid quoted string'); } private parseUnquotedString(): string { diff --git a/src/types/expectedError.ts b/src/types/expectedError.ts new file mode 100644 index 000000000..31158d105 --- /dev/null +++ b/src/types/expectedError.ts @@ -0,0 +1,4 @@ +/** + * An {@link Error} thrown by application code due to expected reasons such as invalid inputs. + */ +export default class ExpectedError extends Error {} diff --git a/src/types/files/archives/rar.ts b/src/types/files/archives/rar.ts index 0917c6f59..e7d49ad82 100644 --- a/src/types/files/archives/rar.ts +++ b/src/types/files/archives/rar.ts @@ -5,6 +5,7 @@ import { Mutex } from 'async-mutex'; import unrar from 'node-unrar-js'; import Defaults from '../../../globals/defaults.js'; +import ExpectedError from '../../expectedError.js'; import Archive from './archive.js'; import ArchiveEntry from './archiveEntry.js'; @@ -73,7 +74,7 @@ export default class Rar extends Archive { files: [entryPath.replace(/[\\/]/g, '/')], }).files]; if (extracted.length === 0) { - throw new Error(`didn't find entry '${entryPath}'`); + throw new ExpectedError(`didn't find entry '${entryPath}'`); } }); } diff --git a/src/types/files/archives/sevenZip.ts b/src/types/files/archives/sevenZip.ts index 862b7b9b0..b1129aaf4 100644 --- a/src/types/files/archives/sevenZip.ts +++ b/src/types/files/archives/sevenZip.ts @@ -7,6 +7,7 @@ import { Mutex } from 'async-mutex'; import Defaults from '../../../globals/defaults.js'; import Temp from '../../../globals/temp.js'; import fsPoly from '../../../polyfill/fsPoly.js'; +import ExpectedError from '../../expectedError.js'; import Archive from './archive.js'; import ArchiveEntry from './archiveEntry.js'; @@ -124,9 +125,9 @@ export default class SevenZip extends Archive { if (process.platform === 'win32' && !await fsPoly.exists(tempFile)) { const files = await fsPoly.walk(tempDir); if (files.length === 0) { - throw new Error('failed to extract any files'); + throw new ExpectedError('failed to extract any files'); } else if (files.length > 1) { - throw new Error('extracted too many files'); + throw new ExpectedError('extracted too many files'); } [tempFile] = files; } diff --git a/src/types/files/archives/tar.ts b/src/types/files/archives/tar.ts index 6e2872a2b..7b8d343d7 100644 --- a/src/types/files/archives/tar.ts +++ b/src/types/files/archives/tar.ts @@ -5,6 +5,7 @@ import tar from 'tar'; import Defaults from '../../../globals/defaults.js'; import FsPoly from '../../../polyfill/fsPoly.js'; +import ExpectedError from '../../expectedError.js'; import FileChecksums from '../fileChecksums.js'; import Archive from './archive.js'; import ArchiveEntry from './archiveEntry.js'; @@ -94,7 +95,7 @@ export default class Tar extends Archive { }, }, [entryPath.replace(/[\\/]/g, '/')]); if (!await FsPoly.exists(extractedFilePath)) { - throw new Error(`didn't find entry '${entryPath}'`); + throw new ExpectedError(`didn't find extracted file '${entryPath}'`); } } } diff --git a/src/types/files/archives/zip.ts b/src/types/files/archives/zip.ts index a1662ed0f..520f3e81b 100644 --- a/src/types/files/archives/zip.ts +++ b/src/types/files/archives/zip.ts @@ -10,6 +10,7 @@ import unzipper, { Entry } from 'unzipper'; import Defaults from '../../../globals/defaults.js'; import fsPoly from '../../../polyfill/fsPoly.js'; import StreamPoly from '../../../polyfill/streamPoly.js'; +import ExpectedError from '../../expectedError.js'; import File from '../file.js'; import FileChecksums, { ChecksumBitmask, ChecksumProps } from '../fileChecksums.js'; import Archive from './archive.js'; @@ -120,7 +121,7 @@ export default class Zip extends Archive { .find((entryFile) => entryFile.path === entryPath.replace(/[\\/]/g, '/')); if (!entry) { // This should never happen, this likely means the zip file was modified after scanning - throw new Error(`didn't find entry '${entryPath}'`); + throw new ExpectedError(`didn't find entry '${entryPath}'`); } let stream: Entry; diff --git a/src/types/files/fileFactory.ts b/src/types/files/fileFactory.ts index 61697af9b..f47668a24 100644 --- a/src/types/files/fileFactory.ts +++ b/src/types/files/fileFactory.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import ExpectedError from '../expectedError.js'; import Archive from './archives/archive.js'; import ArchiveEntry from './archives/archiveEntry.js'; import ArchiveFile from './archives/archiveFile.js'; @@ -33,7 +34,7 @@ export default class FileFactory { return await this.entriesFromArchiveExtension(filePath, checksumBitmask); } catch (error) { if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { - throw new Error(`file doesn't exist: ${filePath}`); + throw new ExpectedError(`file doesn't exist: ${filePath}`); } if (typeof error === 'string') { throw new Error(error); @@ -87,7 +88,7 @@ export default class FileFactory { } else if (ZipX.getExtensions().some((ext) => filePath.toLowerCase().endsWith(ext))) { archive = new ZipX(filePath); } else { - throw new Error(`unknown archive type: ${path.extname(filePath)}`); + throw new ExpectedError(`unknown archive type: ${path.extname(filePath)}`); } return FileCache.getOrComputeEntries(archive, checksumBitmask); diff --git a/src/types/options.ts b/src/types/options.ts index 53fa5b711..3fa155953 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -19,6 +19,7 @@ import ArrayPoly from '../polyfill/arrayPoly.js'; import fsPoly, { FsWalkCallback } from '../polyfill/fsPoly.js'; import URLPoly from '../polyfill/urlPoly.js'; import DAT from './dats/dat.js'; +import ExpectedError from './expectedError.js'; import File from './files/file.js'; import { ChecksumBitmask } from './files/fileChecksums.js'; @@ -696,7 +697,7 @@ export default class Options implements OptionsProps { if (!requireFiles) { return []; } - throw new Error(`${inputPath}: directory doesn't contain any files`); + throw new ExpectedError(`${inputPath}: directory doesn't contain any files`); } return dirPaths; } @@ -725,7 +726,7 @@ export default class Options implements OptionsProps { if (!requireFiles) { return []; } - throw new Error(`${inputPath}: no files found`); + throw new ExpectedError(`no files found in directory: ${inputPath}`); } walkCallback(paths.length); return paths; diff --git a/src/types/outputFactory.ts b/src/types/outputFactory.ts index 905414bff..a6ca66b5d 100644 --- a/src/types/outputFactory.ts +++ b/src/types/outputFactory.ts @@ -7,6 +7,7 @@ import DAT from './dats/dat.js'; import Game from './dats/game.js'; import Release from './dats/release.js'; import ROM from './dats/rom.js'; +import ExpectedError from './expectedError.js'; import ArchiveEntry from './files/archives/archiveEntry.js'; import ArchiveFile from './files/archives/archiveFile.js'; import File from './files/file.js'; @@ -172,7 +173,7 @@ export default class OutputFactory { const leftoverTokens = result.match(/\{[a-zA-Z]+\}/g); if (leftoverTokens !== null && leftoverTokens.length > 0) { - throw new Error(`failed to replace output token${leftoverTokens.length !== 1 ? 's' : ''}: ${leftoverTokens.join(', ')}`); + throw new ExpectedError(`failed to replace output token${leftoverTokens.length !== 1 ? 's' : ''}: ${leftoverTokens.join(', ')}`); } return result; diff --git a/src/types/patches/apsGbaPatch.ts b/src/types/patches/apsGbaPatch.ts index 5b06563a6..b52ea7015 100644 --- a/src/types/patches/apsGbaPatch.ts +++ b/src/types/patches/apsGbaPatch.ts @@ -1,5 +1,6 @@ import FilePoly from '../../polyfill/filePoly.js'; import fsPoly from '../../polyfill/fsPoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -27,12 +28,12 @@ export default class APSGBAPatch extends Patch { return this.getFile().extractToTempFilePoly('r', async (patchFile) => { const header = await patchFile.readNext(APSGBAPatch.FILE_SIGNATURE.length); if (!header.equals(APSGBAPatch.FILE_SIGNATURE)) { - throw new Error(`APS (GBA) patch header is invalid: ${this.getFile().toString()}`); + throw new ExpectedError(`APS (GBA) patch header is invalid: ${this.getFile().toString()}`); } const originalSize = (await patchFile.readNext(4)).readUInt32LE(); if (inputRomFile.getSize() !== originalSize) { - throw new Error(`APS (GBA) patch expected ROM size of ${fsPoly.sizeReadable(originalSize)}: ${this.getFile().toString()}`); + throw new ExpectedError(`APS (GBA) patch expected ROM size of ${fsPoly.sizeReadable(originalSize)}: ${this.getFile().toString()}`); } patchFile.skipNext(4); // patched size diff --git a/src/types/patches/apsN64Patch.ts b/src/types/patches/apsN64Patch.ts index 32f4f3394..021bc11a2 100644 --- a/src/types/patches/apsN64Patch.ts +++ b/src/types/patches/apsN64Patch.ts @@ -1,4 +1,5 @@ import FilePoly from '../../polyfill/filePoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -46,7 +47,7 @@ export default class APSN64Patch extends Patch { patchFile.skipNext(5); // padding targetSize = (await patchFile.readNext(4)).readUInt32LE(); } else { - throw new Error(`APS (N64) patch type ${patchType} isn't supported: ${patchFile.getPathLike()}`); + throw new ExpectedError(`APS (N64) patch type ${patchType} isn't supported: ${patchFile.getPathLike()}`); } }); @@ -57,7 +58,7 @@ export default class APSN64Patch extends Patch { return this.getFile().extractToTempFilePoly('r', async (patchFile) => { const header = await patchFile.readNext(APSN64Patch.FILE_SIGNATURE.length); if (!header.equals(APSN64Patch.FILE_SIGNATURE)) { - throw new Error(`APS (N64) patch header is invalid: ${this.getFile().toString()}`); + throw new ExpectedError(`APS (N64) patch header is invalid: ${this.getFile().toString()}`); } if (this.patchType === APSN64PatchType.SIMPLE) { @@ -65,7 +66,7 @@ export default class APSN64Patch extends Patch { } else if (this.patchType === APSN64PatchType.N64) { patchFile.seek(78); } else { - throw new Error(`APS (N64) patch type ${this.patchType} isn't supported: ${patchFile.getPathLike()}`); + throw new ExpectedError(`APS (N64) patch type ${this.patchType} isn't supported: ${patchFile.getPathLike()}`); } return APSN64Patch.writeOutputFile(inputRomFile, outputRomPath, patchFile); diff --git a/src/types/patches/bpsPatch.ts b/src/types/patches/bpsPatch.ts index 89d0edb03..ef26385dd 100644 --- a/src/types/patches/bpsPatch.ts +++ b/src/types/patches/bpsPatch.ts @@ -1,5 +1,6 @@ import FilePoly from '../../polyfill/filePoly.js'; import fsPoly from '../../polyfill/fsPoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import FileChecksums, { ChecksumBitmask } from '../files/fileChecksums.js'; import Patch from './patch.js'; @@ -40,12 +41,12 @@ export default class BPSPatch extends Patch { 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()}`); + throw new ExpectedError(`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 ExpectedError(`couldn't parse base file CRC for patch: ${file.toString()}`); } return new BPSPatch(file, crcBefore, crcAfter, targetSize); @@ -55,12 +56,12 @@ export default class BPSPatch extends Patch { return this.getFile().extractToTempFilePoly('r', async (patchFile) => { const header = await patchFile.readNext(4); if (!header.equals(BPSPatch.FILE_SIGNATURE)) { - throw new Error(`BPS patch header is invalid: ${this.getFile().toString()}`); + throw new ExpectedError(`BPS patch header is invalid: ${this.getFile().toString()}`); } const sourceSize = await Patch.readUpsUint(patchFile); if (inputRomFile.getSize() !== sourceSize) { - throw new Error(`BPS patch expected ROM size of ${fsPoly.sizeReadable(sourceSize)}: ${this.getFile().toString()}`); + throw new ExpectedError(`BPS patch expected ROM size of ${fsPoly.sizeReadable(sourceSize)}: ${this.getFile().toString()}`); } await Patch.readUpsUint(patchFile); // target size @@ -123,7 +124,7 @@ export default class BPSPatch extends Patch { targetRelativeOffset += 1; } } else { - throw new Error(`BPS action ${action} isn't supported`); + throw new ExpectedError(`BPS action ${action} isn't supported`); } } } diff --git a/src/types/patches/dpsPatch.ts b/src/types/patches/dpsPatch.ts index e0ff85376..a1bf2bd36 100644 --- a/src/types/patches/dpsPatch.ts +++ b/src/types/patches/dpsPatch.ts @@ -1,5 +1,6 @@ import FilePoly from '../../polyfill/filePoly.js'; import fsPoly from '../../polyfill/fsPoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -25,7 +26,7 @@ export default class DPSPatch extends Patch { const originalSize = (await patchFile.readNext(4)).readUInt32LE(); if (inputRomFile.getSize() !== originalSize) { - throw new Error(`DPS patch expected ROM size of ${fsPoly.sizeReadable(originalSize)}: ${this.getFile().toString()}`); + throw new ExpectedError(`DPS patch expected ROM size of ${fsPoly.sizeReadable(originalSize)}: ${this.getFile().toString()}`); } return DPSPatch.writeOutputFile(inputRomFile, outputRomPath, patchFile); @@ -70,7 +71,7 @@ export default class DPSPatch extends Patch { const dataLength = (await patchFile.readNext(4)).readUInt32LE(); data = await patchFile.readNext(dataLength); } else { - throw new Error(`DPS patch mode type ${mode} isn't supported: ${patchFile.getPathLike()}`); + throw new ExpectedError(`DPS patch mode type ${mode} isn't supported: ${patchFile.getPathLike()}`); } await targetFile.writeAt(data, outputOffset); diff --git a/src/types/patches/ipsPatch.ts b/src/types/patches/ipsPatch.ts index b5f958cdf..4a469332d 100644 --- a/src/types/patches/ipsPatch.ts +++ b/src/types/patches/ipsPatch.ts @@ -1,4 +1,5 @@ import FilePoly from '../../polyfill/filePoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -21,7 +22,7 @@ export default class IPSPatch extends Patch { return this.getFile().extractToTempFilePoly('r', async (patchFile) => { const header = await patchFile.readNext(5); if (IPSPatch.FILE_SIGNATURES.every((fileSignature) => !header.equals(fileSignature))) { - throw new Error(`IPS patch header is invalid: ${this.getFile().toString()}`); + throw new ExpectedError(`IPS patch header is invalid: ${this.getFile().toString()}`); } let offsetSize = 3; diff --git a/src/types/patches/ninjaPatch.ts b/src/types/patches/ninjaPatch.ts index 2982113ce..95685da72 100644 --- a/src/types/patches/ninjaPatch.ts +++ b/src/types/patches/ninjaPatch.ts @@ -1,4 +1,5 @@ import FilePoly from '../../polyfill/filePoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -38,11 +39,11 @@ export default class NinjaPatch extends Patch { return this.getFile().extractToTempFilePoly('r', async (patchFile) => { const header = await patchFile.readNext(5); if (!header.equals(NinjaPatch.FILE_SIGNATURE)) { - throw new Error(`NINJA patch header is invalid: ${this.getFile().toString()}`); + throw new ExpectedError(`NINJA patch header is invalid: ${this.getFile().toString()}`); } const version = Number.parseInt((await patchFile.readNext(1)).toString(), 10); if (version !== 2) { - throw new Error(`NINJA v${version} isn't supported: ${this.getFile().toString()}`); + throw new ExpectedError(`NINJA v${version} isn't supported: ${this.getFile().toString()}`); } patchFile.skipNext(1); // encoding @@ -91,7 +92,7 @@ export default class NinjaPatch extends Patch { private async applyCommandOpen(patchFile: FilePoly, targetFile: FilePoly): Promise { const multiFile = (await patchFile.readNext(1)).readUInt8(); if (multiFile > 0) { - throw new Error(`Multi-file NINJA patches aren't supported: ${this.getFile().toString()}`); + throw new ExpectedError(`Multi-file NINJA patches aren't supported: ${this.getFile().toString()}`); } const fileNameLength = multiFile > 0 @@ -100,7 +101,7 @@ export default class NinjaPatch extends Patch { patchFile.skipNext(fileNameLength); // file name const fileType = (await patchFile.readNext(1)).readUInt8(); if (fileType > 0) { - throw new Error(`unsupported NINJA file type ${NinjaFileType[fileType]}: ${this.getFile().toString()}`); + throw new ExpectedError(`unsupported NINJA file type ${NinjaFileType[fileType]}: ${this.getFile().toString()}`); } const sourceFileSizeLength = (await patchFile.readNext(1)).readUInt8(); const sourceFileSize = (await patchFile.readNext(sourceFileSizeLength)) diff --git a/src/types/patches/patch.ts b/src/types/patches/patch.ts index 5c6e0d8b7..bea4c75e8 100644 --- a/src/types/patches/patch.ts +++ b/src/types/patches/patch.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import FilePoly from '../../polyfill/filePoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; export default abstract class Patch { @@ -25,7 +26,7 @@ export default abstract class Patch { return matches[2].toLowerCase(); } - throw new Error(`couldn't parse base file CRC for patch: ${fileBasename}`); + throw new ExpectedError(`couldn't parse base file CRC for patch: ${fileBasename}`); } getFile(): File { diff --git a/src/types/patches/ppfPatch.ts b/src/types/patches/ppfPatch.ts index b6d471808..267b46809 100644 --- a/src/types/patches/ppfPatch.ts +++ b/src/types/patches/ppfPatch.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line max-classes-per-file import FilePoly from '../../polyfill/filePoly.js'; import fsPoly from '../../polyfill/fsPoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -19,12 +20,12 @@ class PPFHeader { static async fromFilePoly(inputRomFile: File, patchFile: FilePoly): Promise { const header = (await patchFile.readNext(5)).toString(); if (!header.startsWith(PPFHeader.FILE_SIGNATURE.toString())) { - throw new Error(`PPF patch header is invalid: ${patchFile.getPathLike()}`); + throw new ExpectedError(`PPF patch header is invalid: ${patchFile.getPathLike()}`); } const encoding = (await patchFile.readNext(1)).readUInt8(); const version = encoding + 1; if (!header.endsWith(`${version}0`)) { - throw new Error(`PPF patch header has an invalid version: ${patchFile.getPathLike()}`); + throw new ExpectedError(`PPF patch header has an invalid version: ${patchFile.getPathLike()}`); } patchFile.skipNext(50); // description @@ -33,7 +34,7 @@ class PPFHeader { if (version === 2) { const sourceSize = (await patchFile.readNext(4)).readUInt32LE(); if (inputRomFile.getSize() !== sourceSize) { - throw new Error(`PPF patch expected ROM size of ${fsPoly.sizeReadable(sourceSize)}: ${patchFile.getPathLike()}`); + throw new ExpectedError(`PPF patch expected ROM size of ${fsPoly.sizeReadable(sourceSize)}: ${patchFile.getPathLike()}`); } blockCheckEnabled = true; } else if (version === 3) { @@ -42,7 +43,7 @@ class PPFHeader { undoDataAvailable = (await patchFile.readNext(1)).readUInt8() === 0x01; patchFile.skipNext(1); // dummy } else { - throw new Error(`PPF v${version} isn't supported: ${patchFile.getPathLike()}`); + throw new ExpectedError(`PPF v${version} isn't supported: ${patchFile.getPathLike()}`); } if (blockCheckEnabled) { patchFile.skipNext(1024); diff --git a/src/types/patches/upsPatch.ts b/src/types/patches/upsPatch.ts index 8750351ad..a04efaa27 100644 --- a/src/types/patches/upsPatch.ts +++ b/src/types/patches/upsPatch.ts @@ -1,5 +1,6 @@ import FilePoly from '../../polyfill/filePoly.js'; import fsPoly from '../../polyfill/fsPoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import FileChecksums, { ChecksumBitmask } from '../files/fileChecksums.js'; import Patch from './patch.js'; @@ -37,12 +38,12 @@ export default class UPSPatch extends Patch { 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()}`); + throw new ExpectedError(`UPS 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 ExpectedError(`couldn't parse base file CRC for patch: ${file.toString()}`); } return new UPSPatch(file, crcBefore, crcAfter, targetSize); @@ -52,12 +53,12 @@ export default class UPSPatch extends Patch { return this.getFile().extractToTempFilePoly('r', async (patchFile) => { const header = await patchFile.readNext(4); if (!header.equals(UPSPatch.FILE_SIGNATURE)) { - throw new Error(`UPS patch header is invalid: ${this.getFile().toString()}`); + throw new ExpectedError(`UPS patch header is invalid: ${this.getFile().toString()}`); } const sourceSize = await Patch.readUpsUint(patchFile); if (inputRomFile.getSize() !== sourceSize) { - throw new Error(`UPS patch expected ROM size of ${fsPoly.sizeReadable(sourceSize)}: ${patchFile.getPathLike()}`); + throw new ExpectedError(`UPS patch expected ROM size of ${fsPoly.sizeReadable(sourceSize)}: ${patchFile.getPathLike()}`); } await Patch.readUpsUint(patchFile); // target size @@ -122,6 +123,6 @@ export default class UPSPatch extends Patch { buffer.push(Buffer.of(sourceByte ^ xorByte)); } - throw new Error(`UPS patch failed to read 0x00 block termination: ${patchFile.getPathLike()}`); + throw new ExpectedError(`UPS patch failed to read 0x00 block termination: ${patchFile.getPathLike()}`); } } diff --git a/src/types/patches/vcdiffPatch.ts b/src/types/patches/vcdiffPatch.ts index 4b9419dfc..70ea4a14a 100644 --- a/src/types/patches/vcdiffPatch.ts +++ b/src/types/patches/vcdiffPatch.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line max-classes-per-file import FilePoly from '../../polyfill/filePoly.js'; import fsPoly from '../../polyfill/fsPoly.js'; +import ExpectedError from '../expectedError.js'; import File from '../files/file.js'; import Patch from './patch.js'; @@ -126,7 +127,7 @@ class VcdiffHeader { const header = await patchFile.readNext(3); if (!header.equals(VcdiffHeader.FILE_SIGNATURE)) { await patchFile.close(); - throw new Error(`Vcdiff patch header is invalid: ${patchFile.getPathLike()}`); + throw new ExpectedError(`Vcdiff patch header is invalid: ${patchFile.getPathLike()}`); } patchFile.skipNext(1); // version @@ -143,7 +144,7 @@ class VcdiffHeader { * bytes "59 5A", only the starting bytes "FD 37 7A 58 5A 00" (after the above number) */ await patchFile.close(); - throw new Error(`unsupported Vcdiff secondary decompressor ${VcdiffSecondaryCompression[secondaryDecompressorId]}: ${patchFile.getPathLike()}`); + throw new ExpectedError(`unsupported Vcdiff secondary decompressor ${VcdiffSecondaryCompression[secondaryDecompressorId]}: ${patchFile.getPathLike()}`); } } @@ -152,7 +153,7 @@ class VcdiffHeader { const codeTableLength = await Patch.readVcdiffUintFromFile(patchFile); if (codeTableLength) { await patchFile.close(); - throw new Error(`can't parse Vcdiff application-defined code table: ${patchFile.getPathLike()}`); + throw new ExpectedError(`can't parse Vcdiff application-defined code table: ${patchFile.getPathLike()}`); } }