From 8fe9ac20578f52f444694779caa25927e7ab2df1 Mon Sep 17 00:00:00 2001 From: Borewit Date: Wed, 28 Aug 2024 17:37:22 +0200 Subject: [PATCH] Implement own custom `Error` using ADT style implementation --- README.md | 27 +++++++++++++++ lib/ParseError.ts | 52 +++++++++++++++++++++++++++++ lib/ParserFactory.ts | 20 +++++------ lib/aiff/AiffParser.ts | 8 ++--- lib/aiff/AiffToken.ts | 8 +++-- lib/apev2/APEv2Parser.ts | 10 ++++-- lib/asf/AsfObject.ts | 6 +++- lib/asf/AsfParser.ts | 4 ++- lib/common/CombinedTagMapper.ts | 3 +- lib/common/FourCC.ts | 5 +-- lib/common/Util.ts | 6 ++-- lib/core.ts | 2 ++ lib/dsdiff/DsdiffParser.ts | 11 ++++-- lib/dsf/DsfParser.ts | 6 +++- lib/ebml/EbmlIterator.ts | 8 +++-- lib/flac/FlacParser.ts | 8 +++-- lib/id3v2/FrameParser.ts | 10 +++++- lib/id3v2/ID3v2Parser.ts | 14 +++++--- lib/mp4/AtomToken.ts | 10 ++++-- lib/mp4/MP4Parser.ts | 7 ++-- lib/mpeg/MpegParser.ts | 12 ++++--- lib/musepack/MusepackConentError.ts | 4 +++ lib/musepack/index.ts | 7 ++-- lib/musepack/sv7/MpcSv7Parser.ts | 3 +- lib/musepack/sv8/MpcSv8Parser.ts | 5 +-- lib/ogg/OggParser.ts | 8 +++-- lib/ogg/opus/Opus.ts | 6 +++- lib/ogg/opus/OpusParser.ts | 3 +- lib/ogg/vorbis/VorbisParser.ts | 10 ++++-- lib/wav/WaveChunk.ts | 8 +++-- lib/wav/WaveParser.ts | 5 +-- lib/wavpack/WavPackParser.ts | 10 ++++-- test/test-mime.ts | 3 ++ 33 files changed, 238 insertions(+), 71 deletions(-) create mode 100644 lib/ParseError.ts create mode 100644 lib/musepack/MusepackConentError.ts diff --git a/README.md b/README.md index cbfef7a67..1a91e8ebd 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,33 @@ readMetadata(); - [AWS SDK for JavaScript](https://aws.amazon.com/sdk-for-javascript/) - Documentation on using the AWS SDK to interact with S3 and other AWS services. - [@tokenizer/s3](https://github.com/Borewit/tokenizer-s3) - Example of `ITokenizer` implementation. +### Handling Parse Errors + +`music-metadata` provides a robust and extensible error handling system with custom error classes that inherit from the standard JavaScript `Error`. +All possible parsing errors are part of a union type `UnionOfParseErrors`, ensuring that every error scenario is accounted for in your code. + +#### Union of Parse Errors + +All parsing errors extend from the base class `ParseError` and are included in the `UnionOfParseErrors` type: +```ts +export type UnionOfParseErrors = + | CouldNotDetermineFileTypeError + | UnsupportedFileTypeError + | UnexpectedFileContentError + | FieldDecodingError + | InternalParserError; +``` + +#### Error Types + +- `CouldNotDetermineFileTypeError`: Raised when the file type cannot be determined. +- `UnsupportedFileTypeError`: Raised when an unsupported file type is encountered. +- `UnexpectedFileContentError`: Raised when the file content does not match the expected format. +- `FieldDecodingError`: Raised when a specific field in the file cannot be decoded. +- `InternalParserError`: Raised for internal parser errors. + +### Other functions + #### `orderTags` function Utility to Converts the native tags to a dictionary index on the tag identifier diff --git a/lib/ParseError.ts b/lib/ParseError.ts new file mode 100644 index 000000000..0fecc5bbb --- /dev/null +++ b/lib/ParseError.ts @@ -0,0 +1,52 @@ +export type UnionOfParseErrors = + | CouldNotDetermineFileTypeError + | UnsupportedFileTypeError + | UnexpectedFileContentError + | FieldDecodingError + | InternalParserError; + +export const makeParseError = (name: Name) => { + return class ParseError extends Error { + name: Name + constructor(message: string) { + super(message); + this.name = name; + } + } +} + +// Concrete error class representing a file type determination failure. +export class CouldNotDetermineFileTypeError extends makeParseError('CouldNotDetermineFileTypeError') { +} + +// Concrete error class representing an unsupported file type. +export class UnsupportedFileTypeError extends makeParseError('UnsupportedFileTypeError') { +} + +// Concrete error class representing unexpected file content. +class UnexpectedFileContentError extends makeParseError('UnexpectedFileContentError') { + constructor(public readonly fileType: string, message: string) { + super(message); + } + + // Override toString to include file type information. + toString(): string { + return `${this.name} (FileType: ${this.fileType}): ${this.message}`; + } +} + +// Concrete error class representing a field decoding error. +export class FieldDecodingError extends makeParseError('FieldDecodingError') { +} + +export class InternalParserError extends makeParseError('InternalParserError') { +} + +// Factory function to create a specific type of UnexpectedFileContentError. +export const makeUnexpectedFileContentError = (fileType: FileType) => { + return class extends UnexpectedFileContentError { + constructor(message: string) { + super(fileType, message); + } + }; +}; diff --git a/lib/ParserFactory.ts b/lib/ParserFactory.ts index f5110ec10..2ea45142e 100644 --- a/lib/ParserFactory.ts +++ b/lib/ParserFactory.ts @@ -1,6 +1,6 @@ import { fileTypeFromBuffer } from 'file-type'; import ContentType from 'content-type'; -import {parse as mimeTypeParse, type MediaType} from 'media-typer'; +import { type MediaType, parse as mimeTypeParse } from 'media-typer'; import initDebug from 'debug'; import { type INativeMetadataCollector, MetadataCollector } from './common/MetadataCollector.js'; @@ -18,8 +18,9 @@ import { DsfParser } from './dsf/DsfParser.js'; import { DsdiffParser } from './dsdiff/DsdiffParser.js'; import { MatroskaParser } from './matroska/MatroskaParser.js'; -import type { IOptions, IAudioMetadata, ParserType } from './type.js'; +import type { IAudioMetadata, IOptions, ParserType } from './type.js'; import type { ITokenizer } from 'strtok3'; +import { CouldNotDetermineFileTypeError, InternalParserError, UnsupportedFileTypeError } from './ParseError.js'; const debug = initDebug('music-metadata:parser:factory'); @@ -79,23 +80,22 @@ export async function parseOnContentType(tokenizer: ITokenizer, opts?: IOptions) export async function parse(tokenizer: ITokenizer, parserId?: ParserType, opts?: IOptions): Promise { if (!parserId) { - // Parser could not be determined on MIME-type or extension - debug('Guess parser on content...'); - - const buf = new Uint8Array(4100); - await tokenizer.peekBuffer(buf, {mayBeLess: true}); if (tokenizer.fileInfo.path) { parserId = getParserIdForExtension(tokenizer.fileInfo.path); } if (!parserId) { + // Parser could not be determined on MIME-type or extension + debug('Guess parser on content...'); + const buf = new Uint8Array(4100); + await tokenizer.peekBuffer(buf, {mayBeLess: true}); const guessedType = await fileTypeFromBuffer(buf); if (!guessedType) { - throw new Error('Failed to determine audio format'); + throw new CouldNotDetermineFileTypeError('Failed to determine audio format'); } debug(`Guessed file type is mime=${guessedType.mime}, extension=${guessedType.ext}`); parserId = getParserIdForMimeType(guessedType.mime); if (!parserId) { - throw new Error(`Guessed MIME-type not supported: ${guessedType.mime}`); + throw new UnsupportedFileTypeError(`Guessed MIME-type not supported: ${guessedType.mime}`); } } } @@ -202,7 +202,7 @@ export async function loadParser(moduleName: ParserType): Promise case 'wavpack': return new WavPackParser(); case 'matroska': return new MatroskaParser(); default: - throw new Error(`Unknown parser type: ${moduleName}`); + throw new InternalParserError(`Unknown parser type: ${moduleName}`); } } diff --git a/lib/aiff/AiffParser.ts b/lib/aiff/AiffParser.ts index 2094d42e9..ce0928eb6 100644 --- a/lib/aiff/AiffParser.ts +++ b/lib/aiff/AiffParser.ts @@ -7,8 +7,8 @@ import { FourCcToken } from '../common/FourCC.js'; import { BasicParser } from '../common/BasicParser.js'; import * as AiffToken from './AiffToken.js'; +import { AiffContentError, type CompressionTypeCode, compressionTypes } from './AiffToken.js'; import * as iff from '../iff/index.js'; -import { type CompressionTypeCode, compressionTypes } from './AiffToken.js'; const debug = initDebug('music-metadata:parser:aiff'); @@ -27,7 +27,7 @@ export class AIFFParser extends BasicParser { const header = await this.tokenizer.readToken(iff.Header); if (header.chunkID !== 'FORM') - throw new Error('Invalid Chunk-ID, expected \'FORM\''); // Not AIFF format + throw new AiffContentError('Invalid Chunk-ID, expected \'FORM\''); // Not AIFF format const type = await this.tokenizer.readToken(FourCcToken); switch (type) { @@ -43,7 +43,7 @@ export class AIFFParser extends BasicParser { break; default: - throw new Error(`Unsupported AIFF type: ${type}`); + throw new AiffContentError(`Unsupported AIFF type: ${type}`); } this.metadata.setFormat('lossless', !this.isCompressed); @@ -70,7 +70,7 @@ export class AIFFParser extends BasicParser { case 'COMM': { // The Common Chunk if (this.isCompressed === null) { - throw new Error('Failed to parse AIFF.COMM chunk when compression type is unknown'); + throw new AiffContentError('Failed to parse AIFF.COMM chunk when compression type is unknown'); } const common = await this.tokenizer.readToken(new AiffToken.Common(header, this.isCompressed)); this.metadata.setFormat('bitsPerSample', common.sampleSize); diff --git a/lib/aiff/AiffToken.ts b/lib/aiff/AiffToken.ts index 3455faf78..407580599 100644 --- a/lib/aiff/AiffToken.ts +++ b/lib/aiff/AiffToken.ts @@ -4,6 +4,7 @@ import { FourCcToken } from '../common/FourCC.js'; import type * as iff from '../iff/index.js'; import type { IGetToken } from 'strtok3'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; export const compressionTypes = { NONE: 'not compressed PCM Apple Computer', @@ -19,6 +20,9 @@ export const compressionTypes = { export type CompressionTypeCode = keyof typeof compressionTypes; +export class AiffContentError extends makeUnexpectedFileContentError('AIFF'){ +} + /** * The Common Chunk. * Describes fundamental parameters of the waveform data such as sample rate, bit resolution, and how many channels of @@ -39,7 +43,7 @@ export class Common implements IGetToken { public constructor(header: iff.IChunkHeader, private isAifc: boolean) { const minimumChunkSize = isAifc ? 22 : 18; - if (header.chunkSize < minimumChunkSize) throw new Error(`COMMON CHUNK size should always be at least ${minimumChunkSize}`); + if (header.chunkSize < minimumChunkSize) throw new AiffContentError(`COMMON CHUNK size should always be at least ${minimumChunkSize}`); this.len = header.chunkSize; } @@ -65,7 +69,7 @@ export class Common implements IGetToken { if (23 + strLen + padding === this.len) { res.compressionName = new Token.StringType(strLen, 'latin1').get(buf, off + 23); } else { - throw new Error('Illegal pstring length'); + throw new AiffContentError('Illegal pstring length'); } } else { res.compressionName = undefined; diff --git a/lib/apev2/APEv2Parser.ts b/lib/apev2/APEv2Parser.ts index a6aa3ec52..dc2b8b1e6 100644 --- a/lib/apev2/APEv2Parser.ts +++ b/lib/apev2/APEv2Parser.ts @@ -17,6 +17,7 @@ import { TagFooter, TagItemHeader } from './APEv2Token.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:APEv2'); @@ -30,6 +31,9 @@ interface IApeInfo { const preamble = 'APETAGEX'; +export class ApeContentError extends makeUnexpectedFileContentError('APEv2'){ +} + export class APEv2Parser extends BasicParser { public static tryParseApeHeader(metadata: INativeMetadataCollector, tokenizer: strtok3.ITokenizer, options: IOptions) { @@ -72,7 +76,7 @@ export class APEv2Parser extends BasicParser { private static parseTagFooter(metadata: INativeMetadataCollector, buffer: Uint8Array, options: IOptions): Promise { const footer = TagFooter.get(buffer, buffer.length - TagFooter.len); - if (footer.ID !== preamble) throw new Error('Unexpected APEv2 Footer ID preamble value.'); + if (footer.ID !== preamble) throw new ApeContentError('Unexpected APEv2 Footer ID preamble value'); strtok3.fromBuffer(buffer); const apeParser = new APEv2Parser(); apeParser.init(metadata, strtok3.fromBuffer(buffer), options); @@ -110,7 +114,7 @@ export class APEv2Parser extends BasicParser { const descriptor = await this.tokenizer.readToken(DescriptorParser); - if (descriptor.ID !== 'MAC ') throw new Error('Unexpected descriptor ID'); + if (descriptor.ID !== 'MAC ') throw new ApeContentError('Unexpected descriptor ID'); this.ape.descriptor = descriptor; const lenExp = descriptor.descriptorBytes - DescriptorParser.len; const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader()); @@ -201,7 +205,7 @@ export class APEv2Parser extends BasicParser { this.metadata.setFormat('duration', APEv2Parser.calculateDuration(header)); if (!this.ape.descriptor) { - throw new Error('Missing APE descriptor'); + throw new ApeContentError('Missing APE descriptor'); } return { diff --git a/lib/asf/AsfObject.ts b/lib/asf/AsfObject.ts index 4ee36fab2..db590b227 100644 --- a/lib/asf/AsfObject.ts +++ b/lib/asf/AsfObject.ts @@ -8,6 +8,10 @@ import type { AnyTagValue, IPicture, ITag } from '../type.js'; import GUID from './GUID.js'; import { getParserForAttr, parseUnicodeAttr } from './AsfUtil.js'; import { AttachedPictureType } from '../id3v2/ID3v2Token.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; + +export class AsfContentParseError extends makeUnexpectedFileContentError('ASF'){ +} /** * Data Type: Specifies the type of information being stored. The following values are recognized. @@ -113,7 +117,7 @@ export abstract class State implements IGetToken { } else { const parseAttr = getParserForAttr(valueType); if (!parseAttr) { - throw new Error(`unexpected value headerType: ${valueType}`); + throw new AsfContentParseError(`unexpected value headerType: ${valueType}`); } tags.push({id: name, value: parseAttr(data as Uint8Array)}); } diff --git a/lib/asf/AsfParser.ts b/lib/asf/AsfParser.ts index cc7524312..c5acbfe4f 100644 --- a/lib/asf/AsfParser.ts +++ b/lib/asf/AsfParser.ts @@ -4,6 +4,8 @@ import { type ITag, TrackType } from '../type.js'; import GUID from './GUID.js'; import * as AsfObject from './AsfObject.js'; import { BasicParser } from '../common/BasicParser.js'; +import { AsfContentParseError } from './AsfObject.js'; + const debug = initDebug('music-metadata:parser:ASF'); const headerType = 'asf'; @@ -23,7 +25,7 @@ export class AsfParser extends BasicParser { public async parse() { const header = await this.tokenizer.readToken(AsfObject.TopLevelHeaderObjectToken); if (!header.objectId.equals(GUID.HeaderObject)) { - throw new Error(`expected asf header; but was not found; got: ${header.objectId.str}`); + throw new AsfContentParseError(`expected asf header; but was not found; got: ${header.objectId.str}`); } try { await this.parseObjectHeader(header.numberOfHeaderObjects); diff --git a/lib/common/CombinedTagMapper.ts b/lib/common/CombinedTagMapper.ts index 3cb828816..c8cea3f11 100644 --- a/lib/common/CombinedTagMapper.ts +++ b/lib/common/CombinedTagMapper.ts @@ -12,6 +12,7 @@ import type { ITag } from '../type.js'; import type { INativeMetadataCollector } from './MetadataCollector.js'; import { MatroskaTagMapper } from '../matroska/MatroskaTagMapper.js'; import { AiffTagMapper } from '../aiff/AiffTagMap.js'; +import { InternalParserError } from '../ParseError.js'; export class CombinedTagMapper { @@ -47,7 +48,7 @@ export class CombinedTagMapper { if (tagMapper) { return this.tagMappers[tagType].mapGenericTag(tag, warnings); } - throw new Error(`No generic tag mapper defined for tag-format: ${tagType}`); + throw new InternalParserError(`No generic tag mapper defined for tag-format: ${tagType}`); } private registerTagMapper(genericTagMapper: IGenericTagMapper) { diff --git a/lib/common/FourCC.ts b/lib/common/FourCC.ts index 546c93ac3..8899336e4 100644 --- a/lib/common/FourCC.ts +++ b/lib/common/FourCC.ts @@ -2,6 +2,7 @@ import type { IToken } from 'strtok3'; import { stringToUint8Array, uint8ArrayToString } from 'uint8array-extras'; import * as util from './Util.js'; +import { InternalParserError, FieldDecodingError } from '../ParseError.js'; const validFourCC = /^[\x21-\x7e©][\x20-\x7e\x00()]{3}/; @@ -15,7 +16,7 @@ export const FourCcToken: IToken = { get: (buf: Uint8Array, off: number): string => { const id = uint8ArrayToString(buf.slice(off, off + FourCcToken.len), 'latin1'); if (!id.match(validFourCC)) { - throw new Error(`FourCC contains invalid characters: ${util.a2hex(id)} "${id}"`); + throw new FieldDecodingError(`FourCC contains invalid characters: ${util.a2hex(id)} "${id}"`); } return id; }, @@ -23,7 +24,7 @@ export const FourCcToken: IToken = { put: (buffer: Uint8Array, offset: number, id: string) => { const str = stringToUint8Array(id); if (str.length !== 4) - throw new Error('Invalid length'); + throw new InternalParserError('Invalid length'); buffer.set(str, offset); return offset + 4; } diff --git a/lib/common/Util.ts b/lib/common/Util.ts index 350e7b279..7ad16d6ba 100644 --- a/lib/common/Util.ts +++ b/lib/common/Util.ts @@ -1,5 +1,6 @@ import { StringType } from 'token-types'; import type { IRatio } from '../type.js'; +import { FieldDecodingError } from '../ParseError.js'; export type StringEncoding = 'ascii' // Use 'utf-8' or latin1 instead @@ -45,7 +46,7 @@ export function trimRightNull(x: string): string { function swapBytes(uint8Array: T): T { const l = uint8Array.length; - if ((l & 1) !== 0) throw new Error('Buffer length must be even'); + if ((l & 1) !== 0) throw new FieldDecodingError('Buffer length must be even'); for (let i = 0; i < l; i += 2) { const a = uint8Array[i]; uint8Array[i] = uint8Array[i + 1]; @@ -54,7 +55,6 @@ function swapBytes(uint8Array: T): T { return uint8Array; } - /** * Decode string */ @@ -66,7 +66,7 @@ export function decodeString(uint8Array: Uint8Array, encoding: StringEncoding): }if (encoding === 'utf-16le' && uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) { // BOM, indicating big endian decoding if ((uint8Array.length & 1) !== 0) - throw new Error('Expected even number of octets for 16-bit unicode string'); + throw new FieldDecodingError('Expected even number of octets for 16-bit unicode string'); return decodeString(swapBytes(uint8Array), encoding); } return new StringType(uint8Array.length, encoding).get(uint8Array, 0); diff --git a/lib/core.ts b/lib/core.ts index b7d57c6ae..7c479fe45 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -16,6 +16,8 @@ export type { IFileInfo } from 'strtok3'; export { type IAudioMetadata, type IOptions, type ITag, type INativeTagDict, type ICommonTagsResult, type IFormat, type IPicture, type IRatio, type IChapter, type ILyricsTag, LyricsContentType, TimestampFormat, IMetadataEventTag, IMetadataEvent } from './type.js'; +export type * from './ParseError.js' + /** * Parse Web API File * Requires Blob to be able to stream using a ReadableStreamBYOBReader, only available since Node.js ≥ 20 diff --git a/lib/dsdiff/DsdiffParser.ts b/lib/dsdiff/DsdiffParser.ts index 22d1e5089..e5baa32c7 100644 --- a/lib/dsdiff/DsdiffParser.ts +++ b/lib/dsdiff/DsdiffParser.ts @@ -7,9 +7,14 @@ import { BasicParser } from '../common/BasicParser.js'; import { ID3v2Parser } from '../id3v2/ID3v2Parser.js'; import { ChunkHeader64, type IChunkHeader64 } from './DsdiffToken.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:aiff'); +export class DsdiffContentParseError extends makeUnexpectedFileContentError('DSDIFF'){ +} + + /** * DSDIFF - Direct Stream Digital Interchange File Format (Phillips) * @@ -21,7 +26,7 @@ export class DsdiffParser extends BasicParser { public async parse(): Promise { const header = await this.tokenizer.readToken(ChunkHeader64); - if (header.chunkID !== 'FRM8') throw new Error('Unexpected chunk-ID'); + if (header.chunkID !== 'FRM8') throw new DsdiffContentParseError('Unexpected chunk-ID'); const type = (await this.tokenizer.readToken(FourCcToken)).trim(); switch (type) { @@ -32,7 +37,7 @@ export class DsdiffParser extends BasicParser { return this.readFmt8Chunks(header.chunkSize - BigInt(FourCcToken.len)); default: - throw new Error(`Unsupported DSDIFF type: ${type}`); + throw new DsdiffContentParseError(`Unsupported DSDIFF type: ${type}`); } } @@ -61,7 +66,7 @@ export class DsdiffParser extends BasicParser { case 'PROP': { // 3.2 PROPERTY CHUNK const propType = await this.tokenizer.readToken(FourCcToken); - if (propType !== 'SND ') throw new Error('Unexpected PROP-chunk ID'); + if (propType !== 'SND ') throw new DsdiffContentParseError('Unexpected PROP-chunk ID'); await this.handleSoundPropertyChunks(header.chunkSize - BigInt(FourCcToken.len)); break; } diff --git a/lib/dsf/DsfParser.ts b/lib/dsf/DsfParser.ts index 97e1348fe..82b22b25f 100644 --- a/lib/dsf/DsfParser.ts +++ b/lib/dsf/DsfParser.ts @@ -3,9 +3,13 @@ import initDebug from 'debug'; import { AbstractID3Parser } from '../id3v2/AbstractID3Parser.js'; import { ChunkHeader, DsdChunk, FormatChunk, type IChunkHeader, type IDsdChunk, type IFormatChunk } from './DsfChunk.js'; import { ID3v2Parser } from "../id3v2/ID3v2Parser.js"; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:DSF'); +export class DsdContentParseError extends makeUnexpectedFileContentError('DSD'){ +} + /** * DSF (dsd stream file) File Parser * Ref: https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf @@ -16,7 +20,7 @@ export class DsfParser extends AbstractID3Parser { const p0 = this.tokenizer.position; // mark start position, normally 0 const chunkHeader = await this.tokenizer.readToken(ChunkHeader); - if (chunkHeader.id !== 'DSD ') throw new Error('Invalid chunk signature'); + if (chunkHeader.id !== 'DSD ') throw new DsdContentParseError('Invalid chunk signature'); this.metadata.setFormat('container', 'DSF'); this.metadata.setFormat('lossless', true); const dsdChunk = await this.tokenizer.readToken(DsdChunk); diff --git a/lib/ebml/EbmlIterator.ts b/lib/ebml/EbmlIterator.ts index 811279aea..5494d3517 100644 --- a/lib/ebml/EbmlIterator.ts +++ b/lib/ebml/EbmlIterator.ts @@ -5,9 +5,13 @@ import { EndOfStreamError, type ITokenizer } from 'strtok3'; import { DataType, type IElementType, type IHeader, type ITree, type ValueType } from './types.js'; import * as Token from 'token-types'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:ebml'); +export class EbmlContentError extends makeUnexpectedFileContentError('EBML'){ +} + export interface ILinkedElementType extends IElementType { id: number; parent: ILinkedElementType | undefined; @@ -150,7 +154,7 @@ export class EbmlIterator { // Calculate VINT_WIDTH while ((msb & mask) === 0) { if (oc > maxLength) { - throw new Error('VINT value exceeding maximum size'); + throw new EbmlContentError('VINT value exceeding maximum size'); } ++oc; mask >>= 1; @@ -181,7 +185,7 @@ export class EbmlIterator { case 10: return this.tokenizer.readNumber(Float64_BE); default: - throw new Error(`Invalid IEEE-754 float length: ${e.len}`); + throw new EbmlContentError(`Invalid IEEE-754 float length: ${e.len}`); } } diff --git a/lib/flac/FlacParser.ts b/lib/flac/FlacParser.ts index 4b2047329..3a0f6c868 100644 --- a/lib/flac/FlacParser.ts +++ b/lib/flac/FlacParser.ts @@ -11,9 +11,13 @@ import type { INativeMetadataCollector } from '../common/MetadataCollector.js'; import type { IOptions } from '../type.js'; import type { ITokenParser } from '../ParserFactory.js'; import { VorbisDecoder } from '../ogg/vorbis/VorbisDecoder.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:FLAC'); +class FlacContentError extends makeUnexpectedFileContentError('FLAC'){ +} + /** * FLAC supports up to 128 kinds of metadata blocks; currently the following are defined: * ref: https://xiph.org/flac/format.html#metadata_block @@ -50,7 +54,7 @@ export class FlacParser extends AbstractID3Parser { const fourCC = await this.tokenizer.readToken(FourCcToken); if (fourCC.toString() !== 'fLaC') { - throw new Error('Invalid FLAC preamble'); + throw new FlacContentError('Invalid FLAC preamble'); } let blockHeader: IBlockHeader; @@ -100,7 +104,7 @@ export class FlacParser extends AbstractID3Parser { private async parseBlockStreamInfo(dataLen: number): Promise { if (dataLen !== BlockStreamInfo.len) - throw new Error('Unexpected block-stream-info length'); + throw new FlacContentError('Unexpected block-stream-info length'); const streamInfo = await this.tokenizer.readToken(BlockStreamInfo); this.metadata.setFormat('container', 'FLAC'); diff --git a/lib/id3v2/FrameParser.ts b/lib/id3v2/FrameParser.ts index 6b10d3536..2c5dc9ba5 100644 --- a/lib/id3v2/FrameParser.ts +++ b/lib/id3v2/FrameParser.ts @@ -7,6 +7,7 @@ import { Genres } from '../id3v1/ID3v1Parser.js'; import type { IWarningCollector } from '../common/MetadataCollector.js'; import type { IComment, ILyricsTag } from '../type.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:id3v2:frame-parser'); @@ -204,7 +205,7 @@ export class FrameParser { break; default: - throw new Error(`Warning: unexpected major versionIndex: ${this.major}`); + throw makeUnexpectedMajorVersionError(this.major); } pic.format = FrameParser.fixPictureMimeType(pic.format); @@ -452,3 +453,10 @@ export class FrameParser { } } + +export class Id3v2ContentError extends makeUnexpectedFileContentError('id3v2'){ +} + +function makeUnexpectedMajorVersionError(majorVer: number) { + throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`); +} diff --git a/lib/id3v2/ID3v2Parser.ts b/lib/id3v2/ID3v2Parser.ts index 36eb680b9..48f2e00a7 100644 --- a/lib/id3v2/ID3v2Parser.ts +++ b/lib/id3v2/ID3v2Parser.ts @@ -3,7 +3,7 @@ import * as Token from 'token-types'; import * as util from '../common/Util.js'; import type { TagType } from '../common/GenericTagTypes.js'; -import { FrameParser, type ITextTag } from './FrameParser.js'; +import { FrameParser, Id3v2ContentError, type ITextTag } from './FrameParser.js'; import { ExtendedHeader, ID3v2Header, type ID3v2MajorVersion, type IID3v2header, UINT32SYNCSAFE } from './ID3v2Token.js'; import type { ITag, IOptions, AnyTagValue } from '../type.js'; @@ -58,7 +58,7 @@ export class ID3v2Parser { case 4: return 10; default: - throw new Error('header versionIndex is incorrect'); + throw makeUnexpectedMajorVersionError(majorVer); } } @@ -94,7 +94,7 @@ export class ID3v2Parser { } return frameParser.readData(uint8Array, frameHeader.id, includeCovers); default: - throw new Error(`Unexpected majorVer: ${majorVer}`); + throw makeUnexpectedMajorVersionError(majorVer); } } @@ -124,7 +124,7 @@ export class ID3v2Parser { const id3Header = await this.tokenizer.readToken(ID3v2Header); if (id3Header.fileIdentifier !== 'ID3') { - throw new Error('expected ID3-header file-identifier \'ID3\' was not found'); + throw new Id3v2ContentError('expected ID3-header file-identifier \'ID3\' was not found'); } this.id3Header = id3Header; @@ -225,9 +225,13 @@ export class ID3v2Parser { break; default: - throw new Error(`Unexpected majorVer: ${majorVer}`); + throw makeUnexpectedMajorVersionError(majorVer); } return header; } +} +function makeUnexpectedMajorVersionError(majorVer: number) { + throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`); } + diff --git a/lib/mp4/AtomToken.ts b/lib/mp4/AtomToken.ts index 3ea059008..2fe7d490a 100644 --- a/lib/mp4/AtomToken.ts +++ b/lib/mp4/AtomToken.ts @@ -4,9 +4,13 @@ import initDebug from 'debug'; import { FourCcToken } from '../common/FourCC.js'; import type { IToken, IGetToken } from 'strtok3'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:MP4:atom'); +export class Mp4ContentError extends makeUnexpectedFileContentError('MP4'){ +} + interface IVersionAndFlags { /** * A 1-byte specification of the version @@ -140,7 +144,7 @@ export const Header: IToken = { get: (buf: Uint8Array, off: number): IAtomHeader => { const length = Token.UINT32_BE.get(buf, off); if (length < 0) - throw new Error('Invalid atom header length'); + throw new Mp4ContentError('Invalid atom header length'); return { length: BigInt(length), @@ -208,7 +212,7 @@ export abstract class FixedLengthAtom { */ protected constructor(public len: number, expLen: number, atomId: string) { if (len < expLen) { - throw new Error(`Atom ${atomId} expected to be ${expLen}, but specifies ${len} bytes long.`); + throw new Mp4ContentError(`Atom ${atomId} expected to be ${expLen}, but specifies ${len} bytes long.`); }if (len > expLen) { debug(`Warning: atom ${atomId} expected to be ${expLen}, but was actually ${len} bytes long.`); } @@ -745,7 +749,7 @@ function readTokenTable(buf: Uint8Array, token: IGetToken, off: number, re if (remainingLen === 0) return []; - if (remainingLen !== numberOfEntries * token.len) throw new Error('mismatch number-of-entries with remaining atom-length'); + if (remainingLen !== numberOfEntries * token.len) throw new Mp4ContentError('mismatch number-of-entries with remaining atom-length'); const entries: T[] = []; // parse offset-table diff --git a/lib/mp4/MP4Parser.ts b/lib/mp4/MP4Parser.ts index f1562dc84..1880bb68c 100644 --- a/lib/mp4/MP4Parser.ts +++ b/lib/mp4/MP4Parser.ts @@ -9,6 +9,7 @@ import { type AnyTagValue, type IChapter, type ITrackInfo, TrackType } from '../ import type { IGetToken } from '@tokenizer/token'; import { uint8ArrayToHex, uint8ArrayToString } from 'uint8array-extras'; +import { Mp4ContentError } from './AtomToken.js'; const debug = initDebug('music-metadata:parser:MP4'); const tagFormat = 'iTunes'; @@ -137,7 +138,7 @@ export class MP4Parser extends BasicParser { const integerType = (signed ? 'INT' : 'UINT') + array.length * 8 + (array.length > 1 ? '_BE' : ''); const token: IGetToken = (Token as unknown as { [id: string]: IGetToken })[integerType]; if (!token) { - throw new Error(`Token for integer type not found: "${integerType}"`); + throw new Mp4ContentError(`Token for integer type not found: "${integerType}"`); } return Number(token.get(array, 0)); } @@ -318,7 +319,7 @@ export class MP4Parser extends BasicParser { const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len)); if (dataAtom.type.set !== 0) { - throw new Error(`Unsupported type-set != 0: ${dataAtom.type.set}`); + throw new Mp4ContentError(`Unsupported type-set != 0: ${dataAtom.type.set}`); } // Use well-known-type table @@ -568,7 +569,7 @@ export class MP4Parser extends BasicParser { const nextChunkLen = chunkOffset - this.tokenizer.position; const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i]; len -= nextChunkLen + sampleSize; - if (len < 0) throw new Error('Chapter chunk exceeding token length'); + if (len < 0) throw new Mp4ContentError('Chapter chunk exceeding token length'); await this.tokenizer.ignore(nextChunkLen); const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize)); debug(`Chapter ${i + 1}: ${title}`); diff --git a/lib/mpeg/MpegParser.ts b/lib/mpeg/MpegParser.ts index 251b9653b..e25b4c891 100644 --- a/lib/mpeg/MpegParser.ts +++ b/lib/mpeg/MpegParser.ts @@ -5,9 +5,13 @@ import initDebug from 'debug'; import * as common from '../common/Util.js'; import { AbstractID3Parser } from '../id3v2/AbstractID3Parser.js'; import { InfoTagHeaderTag, type IXingInfoTag, LameEncoderVersion, readXingHeader } from './XingTag.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:mpeg'); +export class MpegContentError extends makeUnexpectedFileContentError('MPEG'){ +} + /** * Cache buffer size used for searching synchronization preabmle */ @@ -214,14 +218,14 @@ class MpegFrameHeader { // Calculate bitrate const bitrateInKbps = this.calcBitrate(); if (!bitrateInKbps) { - throw new Error('Cannot determine bit-rate'); + throw new MpegContentError('Cannot determine bit-rate'); } this.bitrate = bitrateInKbps * 1000; // Calculate sampling rate this.samplingRate = this.calcSamplingRate(); if (this.samplingRate == null) { - throw new Error('Cannot determine sampling-rate'); + throw new MpegContentError('Cannot determine sampling-rate'); } } @@ -453,7 +457,7 @@ export class MpegParser extends AbstractID3Parser { } const slot_size = header.calcSlotSize(); if (slot_size === null) { - throw new Error('invalid slot_size'); + throw new MpegContentError('invalid slot_size'); } const samples_per_frame = header.calcSamplesPerFrame(); @@ -649,7 +653,7 @@ export class MpegParser extends AbstractID3Parser { } private async skipFrameData(frameDataLeft: number): Promise { - if (frameDataLeft < 0) throw new Error('frame-data-left cannot be negative'); + if (frameDataLeft < 0) throw new MpegContentError('frame-data-left cannot be negative'); await this.tokenizer.ignore(frameDataLeft); this.countSkipFrameData += frameDataLeft; } diff --git a/lib/musepack/MusepackConentError.ts b/lib/musepack/MusepackConentError.ts new file mode 100644 index 000000000..7fd3ba71c --- /dev/null +++ b/lib/musepack/MusepackConentError.ts @@ -0,0 +1,4 @@ +import { makeUnexpectedFileContentError } from '../ParseError.js'; + +export class MusepackContentError extends makeUnexpectedFileContentError('Musepack'){ +} diff --git a/lib/musepack/index.ts b/lib/musepack/index.ts index 6915daac2..49c1cfe83 100644 --- a/lib/musepack/index.ts +++ b/lib/musepack/index.ts @@ -6,6 +6,7 @@ import { AbstractID3Parser } from '../id3v2/AbstractID3Parser.js'; import { MpcSv8Parser } from './sv8/MpcSv8Parser.js'; import { MpcSv7Parser } from './sv7/MpcSv7Parser.js'; +import { MusepackContentError } from './MusepackConentError.js'; const debug = initDebug('music-metadata:parser:musepack'); @@ -16,17 +17,17 @@ class MusepackParser extends AbstractID3Parser { let mpcParser: ITokenParser; switch (signature) { case 'MP+': { - debug('Musepack stream-version 7'); + debug('Stream-version 7'); mpcParser = new MpcSv7Parser(); break; } case 'MPC': { - debug('Musepack stream-version 8'); + debug('Stream-version 8'); mpcParser = new MpcSv8Parser(); break; } default: { - throw new Error('Invalid Musepack signature prefix'); + throw new MusepackContentError('Invalid signature prefix'); } } mpcParser.init(this.metadata, this.tokenizer, this.options); diff --git a/lib/musepack/sv7/MpcSv7Parser.ts b/lib/musepack/sv7/MpcSv7Parser.ts index 2651de4ce..061982420 100644 --- a/lib/musepack/sv7/MpcSv7Parser.ts +++ b/lib/musepack/sv7/MpcSv7Parser.ts @@ -4,6 +4,7 @@ import { BasicParser } from '../../common/BasicParser.js'; import { APEv2Parser } from '../../apev2/APEv2Parser.js'; import { BitReader } from './BitReader.js'; import * as SV7 from './StreamVersion7.js'; +import { MusepackContentError } from '../MusepackConentError.js'; const debug = initDebug('music-metadata:parser:musepack'); @@ -17,7 +18,7 @@ export class MpcSv7Parser extends BasicParser { const header = await this.tokenizer.readToken(SV7.Header); - if (header.signature !== 'MP+') throw new Error('Unexpected magic number'); + if (header.signature !== 'MP+') throw new MusepackContentError('Unexpected magic number'); debug(`stream-version=${header.streamMajorVersion}.${header.streamMinorVersion}`); this.metadata.setFormat('container', 'Musepack, SV7'); this.metadata.setFormat('sampleRate', header.sampleFrequency); diff --git a/lib/musepack/sv8/MpcSv8Parser.ts b/lib/musepack/sv8/MpcSv8Parser.ts index 79cd3d828..2fdef54e1 100644 --- a/lib/musepack/sv8/MpcSv8Parser.ts +++ b/lib/musepack/sv8/MpcSv8Parser.ts @@ -4,6 +4,7 @@ import { BasicParser } from '../../common/BasicParser.js'; import { APEv2Parser } from '../../apev2/APEv2Parser.js'; import { FourCcToken } from '../../common/FourCC.js'; import * as SV8 from './StreamVersion8.js'; +import { MusepackContentError } from '../MusepackConentError.js'; const debug = initDebug('music-metadata:parser:musepack'); @@ -14,7 +15,7 @@ export class MpcSv8Parser extends BasicParser { public async parse(): Promise { const signature = await this.tokenizer.readToken(FourCcToken); - if (signature !== 'MPCK') throw new Error('Invalid Magic number'); + if (signature !== 'MPCK') throw new MusepackContentError('Invalid Magic number'); this.metadata.setFormat('container', 'Musepack, SV8'); return this.parsePacket(); } @@ -57,7 +58,7 @@ export class MpcSv8Parser extends BasicParser { return APEv2Parser.tryParseApeHeader(this.metadata, this.tokenizer, this.options); default: - throw new Error(`Unexpected header: ${header.key}`); + throw new MusepackContentError(`Unexpected header: ${header.key}`); } } while (true); } diff --git a/lib/ogg/OggParser.ts b/lib/ogg/OggParser.ts index eb2b34cec..336d809e6 100644 --- a/lib/ogg/OggParser.ts +++ b/lib/ogg/OggParser.ts @@ -12,6 +12,10 @@ import { SpeexParser } from './speex/SpeexParser.js'; import { TheoraParser } from './theora/TheoraParser.js'; import type * as Ogg from './Ogg.js'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; + +export class OggContentError extends makeUnexpectedFileContentError('Ogg'){ +} const debug = initDebug('music-metadata:parser:ogg'); @@ -83,7 +87,7 @@ export class OggParser extends BasicParser { do { header = await this.tokenizer.readToken(OggParser.Header); - if (header.capturePattern !== 'OggS') throw new Error('Invalid Ogg capture pattern'); + if (header.capturePattern !== 'OggS') throw new OggContentError('Invalid Ogg capture pattern'); this.metadata.setFormat('container', 'Ogg'); this.header = header; @@ -115,7 +119,7 @@ export class OggParser extends BasicParser { this.pageConsumer = new TheoraParser(this.metadata, this.options, this.tokenizer); break; default: - throw new Error(`gg audio-codec not recognized (id=${id})`); + throw new OggContentError(`gg audio-codec not recognized (id=${id})`); } } await this.pageConsumer.parsePage(header, pageData); diff --git a/lib/ogg/opus/Opus.ts b/lib/ogg/opus/Opus.ts index 2c6edca1d..c98f96a51 100644 --- a/lib/ogg/opus/Opus.ts +++ b/lib/ogg/opus/Opus.ts @@ -1,5 +1,6 @@ import * as Token from 'token-types'; import type { IGetToken } from 'strtok3'; +import { makeUnexpectedFileContentError } from '../../ParseError.js'; /** * Opus ID Header interface @@ -40,6 +41,9 @@ export interface IIdHeader { channelMapping: number } +export class OpusContentError extends makeUnexpectedFileContentError('Opus'){ +} + /** * Opus ID Header parser * Ref: https://wiki.xiph.org/OggOpus#ID_Header @@ -48,7 +52,7 @@ export class IdHeader implements IGetToken { constructor(public len: number) { if (len < 19) { - throw new Error("ID-header-page 0 should be at least 19 bytes long"); + throw new OpusContentError('ID-header-page 0 should be at least 19 bytes long'); } } diff --git a/lib/ogg/opus/OpusParser.ts b/lib/ogg/opus/OpusParser.ts index b661e96d9..ee4f89ada 100644 --- a/lib/ogg/opus/OpusParser.ts +++ b/lib/ogg/opus/OpusParser.ts @@ -7,6 +7,7 @@ import type {IOptions} from '../../type.js'; import type {INativeMetadataCollector} from '../../common/MetadataCollector.js'; import * as Opus from './Opus.js'; +import { OpusContentError } from './Opus.js'; /** * Opus parser @@ -32,7 +33,7 @@ export class OpusParser extends VorbisParser { // Parse Opus ID Header this.idHeader = new Opus.IdHeader(pageData.length).get(pageData, 0); if (this.idHeader.magicSignature !== "OpusHead") - throw new Error("Illegal ogg/Opus magic-signature"); + throw new OpusContentError("Illegal ogg/Opus magic-signature"); this.metadata.setFormat('sampleRate', this.idHeader.inputSampleRate); this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount); } diff --git a/lib/ogg/vorbis/VorbisParser.ts b/lib/ogg/vorbis/VorbisParser.ts index 73d939405..10ec6e7f0 100644 --- a/lib/ogg/vorbis/VorbisParser.ts +++ b/lib/ogg/vorbis/VorbisParser.ts @@ -7,9 +7,13 @@ import { CommonHeader, IdentificationHeader, type IVorbisPicture, VorbisPictureT import type { IPageConsumer, IPageHeader } from '../Ogg.js'; import type { IOptions } from '../../type.js'; import type { INativeMetadataCollector } from '../../common/MetadataCollector.js'; +import { makeUnexpectedFileContentError } from '../../ParseError.js'; const debug = debugInit('music-metadata:parser:ogg:vorbis1'); +export class VorbisContentError extends makeUnexpectedFileContentError('Vorbis'){ +} + /** * Vorbis 1 Parser. * Used by OggParser @@ -32,7 +36,7 @@ export class VorbisParser implements IPageConsumer { } else { if (header.headerType.continued) { if (this.pageSegments.length === 0) { - throw new Error('Cannot continue on previous page'); + throw new VorbisContentError('Cannot continue on previous page'); } this.pageSegments.push(pageData); } @@ -110,7 +114,7 @@ export class VorbisParser implements IPageConsumer { // Parse Vorbis common header const commonHeader = CommonHeader.get(pageData, 0); if (commonHeader.vorbis !== 'vorbis') - throw new Error('Metadata does not look like Vorbis'); + throw new VorbisContentError('Metadata does not look like Vorbis'); if (commonHeader.packetType === 1) { const idHeader = IdentificationHeader.get(pageData, CommonHeader.len); @@ -118,7 +122,7 @@ export class VorbisParser implements IPageConsumer { this.metadata.setFormat('bitrate', idHeader.bitrateNominal); this.metadata.setFormat('numberOfChannels', idHeader.channelMode); debug('sample-rate=%s[hz], bitrate=%s[b/s], channel-mode=%s', idHeader.sampleRate, idHeader.bitrateNominal, idHeader.channelMode); - } else throw new Error('First Ogg page should be type 1: the identification header'); + } else throw new VorbisContentError('First Ogg page should be type 1: the identification header'); } protected async parseFullPage(pageData: Uint8Array): Promise { diff --git a/lib/wav/WaveChunk.ts b/lib/wav/WaveChunk.ts index 7068d4b5c..c09092c47 100644 --- a/lib/wav/WaveChunk.ts +++ b/lib/wav/WaveChunk.ts @@ -1,6 +1,10 @@ import type { IGetToken } from 'strtok3'; import type { IChunkHeader } from '../iff/index.js'; import * as Token from 'token-types'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; + +export class WaveContentError extends makeUnexpectedFileContentError('Wave'){ +} /** * Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx @@ -55,7 +59,7 @@ export class Format implements IGetToken { public constructor(header: IChunkHeader) { if (header.chunkSize < 16) - throw new Error('Invalid chunk size'); + throw new WaveContentError('Invalid chunk size'); this.len = header.chunkSize; } @@ -86,7 +90,7 @@ export class FactChunk implements IGetToken { public constructor(header: IChunkHeader) { if (header.chunkSize < 4) { - throw new Error('Invalid fact chunk size.'); + throw new WaveContentError('Invalid fact chunk size.'); } this.len = header.chunkSize; } diff --git a/lib/wav/WaveParser.ts b/lib/wav/WaveParser.ts index 7ffba10d3..67b1b0ec6 100644 --- a/lib/wav/WaveParser.ts +++ b/lib/wav/WaveParser.ts @@ -10,6 +10,7 @@ import { FourCcToken } from '../common/FourCC.js'; import { BasicParser } from '../common/BasicParser.js'; import { BroadcastAudioExtensionChunk, type IBroadcastAudioExtensionChunk } from './BwfChunk.js'; import type { AnyTagValue } from '../type.js'; +import { WaveContentError } from './WaveChunk.js'; const debug = initDebug('music-metadata:parser:RIFF'); @@ -50,7 +51,7 @@ export class WaveParser extends BasicParser { case 'WAVE': return this.readWaveChunk(chunkSize - FourCcToken.len); default: - throw new Error(`Unsupported RIFF format: RIFF/${type}`); + throw new WaveContentError(`Unsupported RIFF format: RIFF/${type}`); } } @@ -184,7 +185,7 @@ export class WaveParser extends BasicParser { } if (chunkSize !== 0) { - throw new Error(`Illegal remaining size: ${chunkSize}`); + throw new WaveContentError(`Illegal remaining size: ${chunkSize}`); } } diff --git a/lib/wavpack/WavPackParser.ts b/lib/wavpack/WavPackParser.ts index 8c38a909f..702eb4766 100644 --- a/lib/wavpack/WavPackParser.ts +++ b/lib/wavpack/WavPackParser.ts @@ -7,9 +7,13 @@ import { BlockHeaderToken, type IBlockHeader, type IMetadataId, MetadataIdToken import initDebug from 'debug'; import { uint8ArrayToHex } from 'uint8array-extras'; +import { makeUnexpectedFileContentError } from '../ParseError.js'; const debug = initDebug('music-metadata:parser:WavPack'); +export class WavPackContentError extends makeUnexpectedFileContentError('WavPack'){ +} + /** * WavPack Parser */ @@ -36,7 +40,7 @@ export class WavPackParser extends BasicParser { break; const header = await this.tokenizer.readToken(BlockHeaderToken); - if (header.BlockID !== 'wvpk') throw new Error('Invalid WavPack Block-ID'); + if (header.BlockID !== 'wvpk') throw new WavPackContentError('Invalid WavPack Block-ID'); debug(`WavPack header blockIndex=${header.blockIndex}, len=${BlockHeaderToken.len}`); @@ -91,7 +95,7 @@ export class WavPackParser extends BasicParser { const mp = 1 << Token.UINT8.get(data, 0); const samplingRate = header.flags.samplingRate * mp * 8; // ToDo: second factor should be read from DSD-metadata block https://github.com/dbry/WavPack/issues/71#issuecomment-483094813 if (!header.flags.isDSD) - throw new Error('Only expect DSD block if DSD-flag is set'); + throw new WavPackContentError('Only expect DSD block if DSD-flag is set'); this.metadata.setFormat('sampleRate', samplingRate); this.metadata.setFormat('duration', header.totalSamples / samplingRate); break; @@ -119,7 +123,7 @@ export class WavPackParser extends BasicParser { if (id.isOddSize) this.tokenizer.ignore(1); } - if (remaining !== 0) throw new Error('metadata-sub-block should fit it remaining length'); + if (remaining !== 0) throw new WavPackContentError('metadata-sub-block should fit it remaining length'); } } diff --git a/test/test-mime.ts b/test/test-mime.ts index 8c63e3003..c089bf203 100644 --- a/test/test-mime.ts +++ b/test/test-mime.ts @@ -5,6 +5,7 @@ import fs from 'node:fs'; import * as mm from '../lib/index.js'; import { SourceStream, samplePath } from './util.js'; +import { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from '../lib/ParseError.js'; describe('MIME & extension mapping', () => { @@ -97,6 +98,7 @@ describe('MIME & extension mapping', () => { assert.fail('Should throw an Error'); } catch (err) { assert.equal(err.message, 'Failed to determine audio format'); + assert.instanceOf(err, CouldNotDetermineFileTypeError); } }); @@ -111,6 +113,7 @@ describe('MIME & extension mapping', () => { assert.fail('Should throw an Error'); } catch (err) { assert.equal(err.message, 'Guessed MIME-type not supported: image/jpeg'); + assert.instanceOf(err, UnsupportedFileTypeError); } });