music-metadata 12.1.0 → 12.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/lib/ParserFactory.js +3 -1
- package/lib/aiff/AiffParser.js +8 -7
- package/lib/apev2/APEv2Parser.js +4 -26
- package/lib/asf/AsfParser.js +15 -17
- package/lib/common/Allocation.d.ts +14 -0
- package/lib/common/Allocation.js +70 -0
- package/lib/common/BasicParser.d.ts +2 -0
- package/lib/common/BasicParser.js +5 -0
- package/lib/core.js +4 -7
- package/lib/dsdiff/DsdiffParser.d.ts +3 -0
- package/lib/dsdiff/DsdiffParser.js +70 -30
- package/lib/ebml/EbmlIterator.d.ts +4 -1
- package/lib/ebml/EbmlIterator.js +19 -27
- package/lib/flac/FlacParser.js +10 -7
- package/lib/id3v2/ID3v2Parser.js +2 -1
- package/lib/index.js +3 -4
- package/lib/matroska/MatroskaParser.js +1 -1
- package/lib/mp4/MP4Parser.d.ts +2 -1
- package/lib/mp4/MP4Parser.js +3 -5
- package/lib/ogg/OggParser.js +2 -1
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +1 -0
- package/lib/ogg/vorbis/VorbisDecoder.js +12 -0
- package/lib/ogg/vorbis/VorbisStream.d.ts +2 -1
- package/lib/ogg/vorbis/VorbisStream.js +14 -6
- package/lib/type.d.ts +6 -0
- package/lib/wav/WaveParser.js +13 -12
- package/lib/wavpack/WavPackParser.js +5 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -611,6 +611,17 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
611
611
|
Files, buffers, and Blobs support chapters whether `moov` appears before or after `mdat`.
|
|
612
612
|
For forward-only streams, `moov` must appear before the chapter data.
|
|
613
613
|
|
|
614
|
+
- `maxMemoryAllocation`: `number` (default: `67108864`, 64 MiB)
|
|
615
|
+
|
|
616
|
+
Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
|
|
617
|
+
Must be a positive safe integer. Parsing rejects payloads above this limit before allocating them.
|
|
618
|
+
The parser configures this limit once on its `strtok3` tokenizers, including tokenizers for embedded tags.
|
|
619
|
+
Oversized tokenizer allocations or stream lookahead reject with `strtok3.BufferSizeError`.
|
|
620
|
+
When using `parseFromTokenizer()`, also configure `maxBufferSize` on the supplied tokenizer at creation.
|
|
621
|
+
Increase it for files with unusually large metadata or cover art, or lower it for constrained environments.
|
|
622
|
+
Audio/video data that is skipped does not count toward this limit. This does not cap total process memory:
|
|
623
|
+
decoded metadata, multiple retained tags, and temporary copies can require additional memory.
|
|
624
|
+
|
|
614
625
|
- `mkvUseIndex`: `boolean` (default: `false`)
|
|
615
626
|
|
|
616
627
|
When `true`, the parser uses the SeekHead index in Matroska (MKV) files to skip segment and cluster elements.
|
package/lib/ParserFactory.js
CHANGED
|
@@ -5,6 +5,7 @@ import { parse as mimeTypeParse } from 'media-typer';
|
|
|
5
5
|
import { aiffParserLoader } from './aiff/AiffLoader.js';
|
|
6
6
|
import { apeParserLoader } from './apev2/Apev2Loader.js';
|
|
7
7
|
import { asfParserLoader } from './asf/AsfLoader.js';
|
|
8
|
+
import { getMaxMemoryAllocation } from './common/Allocation.js';
|
|
8
9
|
import { MetadataCollector } from './common/MetadataCollector.js';
|
|
9
10
|
import { createAudioTrackInfo } from './common/TrackInfo.js';
|
|
10
11
|
import { scanAppendingHeaders } from './core.js';
|
|
@@ -56,6 +57,7 @@ export class ParserFactory {
|
|
|
56
57
|
this.parsers.push(parser);
|
|
57
58
|
}
|
|
58
59
|
async parse(tokenizer, parserLoader, opts = {}) {
|
|
60
|
+
getMaxMemoryAllocation(opts);
|
|
59
61
|
if (tokenizer.supportsRandomAccess()) {
|
|
60
62
|
debug('tokenizer supports random-access, scanning for appending headers');
|
|
61
63
|
await scanAppendingHeaders(tokenizer, opts);
|
|
@@ -90,7 +92,7 @@ export class ParserFactory {
|
|
|
90
92
|
debug(`Loading ${parserLoader.parserType} parser...`);
|
|
91
93
|
const metadata = new MetadataCollector(opts);
|
|
92
94
|
const ParserImpl = await parserLoader.load();
|
|
93
|
-
const parser = new ParserImpl(metadata, tokenizer, opts
|
|
95
|
+
const parser = new ParserImpl(metadata, tokenizer, opts);
|
|
94
96
|
debug(`Parser ${parserLoader.parserType} loaded`);
|
|
95
97
|
await parser.parse();
|
|
96
98
|
// Audio-only parsers describe their single stream through the format fields.
|
package/lib/aiff/AiffParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import * as strtok3 from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { getTokenizerOptions } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { FourCcToken } from '../common/FourCC.js';
|
|
6
7
|
import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
|
|
@@ -18,11 +19,11 @@ const debug = initDebug('music-metadata:parser:aiff');
|
|
|
18
19
|
export class AIFFParser extends BasicParser {
|
|
19
20
|
isCompressed = null;
|
|
20
21
|
async parse() {
|
|
21
|
-
const header = await this.
|
|
22
|
+
const header = await this.readToken(iff.Header);
|
|
22
23
|
if (header.chunkID !== 'FORM') {
|
|
23
24
|
throw new AiffContentError("Invalid Chunk-ID, expected 'FORM'"); // Not AIFF format
|
|
24
25
|
}
|
|
25
|
-
const type = await this.
|
|
26
|
+
const type = await this.readToken(FourCcToken);
|
|
26
27
|
switch (type) {
|
|
27
28
|
case 'AIFF':
|
|
28
29
|
this.metadata.setFormat('container', type);
|
|
@@ -41,7 +42,7 @@ export class AIFFParser extends BasicParser {
|
|
|
41
42
|
while (!this.tokenizer.fileInfo.size ||
|
|
42
43
|
this.tokenizer.fileInfo.size - this.tokenizer.position >= iff.Header.len) {
|
|
43
44
|
debug(`Reading AIFF chunk at offset=${this.tokenizer.position}`);
|
|
44
|
-
const chunkHeader = await this.
|
|
45
|
+
const chunkHeader = await this.readToken(iff.Header);
|
|
45
46
|
const nextChunk = 2 * Math.round(chunkHeader.chunkSize / 2);
|
|
46
47
|
const bytesRead = await this.readData(chunkHeader);
|
|
47
48
|
await this.tokenizer.ignore(nextChunk - bytesRead);
|
|
@@ -63,7 +64,7 @@ export class AIFFParser extends BasicParser {
|
|
|
63
64
|
if (this.isCompressed === null) {
|
|
64
65
|
throw new AiffContentError('Failed to parse AIFF.COMM chunk when compression type is unknown');
|
|
65
66
|
}
|
|
66
|
-
const common = await this.
|
|
67
|
+
const common = await this.readToken(new AiffToken.Common(header, this.isCompressed));
|
|
67
68
|
this.metadata.setFormat('bitsPerSample', common.sampleSize);
|
|
68
69
|
this.metadata.setFormat('sampleRate', common.sampleRate);
|
|
69
70
|
this.metadata.setFormat('numberOfChannels', common.numChannels);
|
|
@@ -76,8 +77,8 @@ export class AIFFParser extends BasicParser {
|
|
|
76
77
|
}
|
|
77
78
|
case 'ID3 ': {
|
|
78
79
|
// ID3-meta-data
|
|
79
|
-
const id3_data = await this.
|
|
80
|
-
const rst = strtok3.fromBuffer(id3_data);
|
|
80
|
+
const id3_data = await this.readToken(new Token.Uint8ArrayType(header.chunkSize));
|
|
81
|
+
const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
|
|
81
82
|
await new ID3v2Parser().parse(this.metadata, rst, this.options);
|
|
82
83
|
return header.chunkSize;
|
|
83
84
|
}
|
|
@@ -97,7 +98,7 @@ export class AIFFParser extends BasicParser {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
async readTextChunk(header) {
|
|
100
|
-
const value = await this.
|
|
101
|
+
const value = await this.readToken(new Token.StringType(header.chunkSize, 'ascii'));
|
|
101
102
|
const values = value
|
|
102
103
|
.split('\0')
|
|
103
104
|
.map(v => v.trim())
|
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { textDecode } from '@borewit/text-codec';
|
|
|
2
2
|
import initDebug from 'debug';
|
|
3
3
|
import * as strtok3 from 'strtok3';
|
|
4
4
|
import { StringType } from 'token-types';
|
|
5
|
+
import { getTokenizerOptions, readBuffer } from '../common/Allocation.js';
|
|
5
6
|
import { BasicParser } from '../common/BasicParser.js';
|
|
6
7
|
import * as util from '../common/Util.js';
|
|
7
8
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
@@ -60,8 +61,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
60
61
|
if (footer.ID !== preamble) {
|
|
61
62
|
throw new ApeContentError('Unexpected APEv2 Footer ID preamble value');
|
|
62
63
|
}
|
|
63
|
-
strtok3.fromBuffer(buffer);
|
|
64
|
-
const apeParser = new APEv2Parser(metadata, strtok3.fromBuffer(buffer), options);
|
|
64
|
+
const apeParser = new APEv2Parser(metadata, strtok3.fromBuffer(buffer, getTokenizerOptions(options)), options);
|
|
65
65
|
return apeParser.parseTags(footer);
|
|
66
66
|
}
|
|
67
67
|
ape = {};
|
|
@@ -82,8 +82,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
82
82
|
if (this.tokenizer.fileInfo.size) {
|
|
83
83
|
// Try to read the APEv2 header using just the footer-header
|
|
84
84
|
const remaining = this.tokenizer.fileInfo.size - this.tokenizer.position; // ToDo: take ID3v1 into account
|
|
85
|
-
const buffer =
|
|
86
|
-
await this.tokenizer.readBuffer(buffer);
|
|
85
|
+
const buffer = await this.readTagValue(remaining);
|
|
87
86
|
return APEv2Parser.parseTagFooter(this.metadata, buffer, this.options);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
@@ -167,28 +166,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
168
|
async readTagValue(size) {
|
|
170
|
-
|
|
171
|
-
if ((!this.tokenizer.supportsRandomAccess() || this.tokenizer.fileInfo.size === undefined) && size > chunkSize) {
|
|
172
|
-
// Stream sizes may be unknown or untrustworthy. Only allocate the full
|
|
173
|
-
// value after all of its bytes have actually been read.
|
|
174
|
-
const chunks = [];
|
|
175
|
-
for (let remaining = size; remaining > 0;) {
|
|
176
|
-
const chunk = new Uint8Array(Math.min(remaining, chunkSize));
|
|
177
|
-
await this.tokenizer.readBuffer(chunk);
|
|
178
|
-
chunks.push(chunk);
|
|
179
|
-
remaining -= chunk.length;
|
|
180
|
-
}
|
|
181
|
-
const value = new Uint8Array(size);
|
|
182
|
-
let offset = 0;
|
|
183
|
-
for (const chunk of chunks) {
|
|
184
|
-
value.set(chunk, offset);
|
|
185
|
-
offset += chunk.length;
|
|
186
|
-
}
|
|
187
|
-
return value;
|
|
188
|
-
}
|
|
189
|
-
const value = new Uint8Array(size);
|
|
190
|
-
await this.tokenizer.readBuffer(value);
|
|
191
|
-
return value;
|
|
169
|
+
return readBuffer(this.tokenizer, size, this.options, undefined, message => new ApeContentError(message));
|
|
192
170
|
}
|
|
193
171
|
async parseDescriptorExpansion(lenExp) {
|
|
194
172
|
await this.tokenizer.ignore(lenExp);
|
package/lib/asf/AsfParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import { EndOfStreamError } from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { validateAllocation } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { TrackType } from '../type.js';
|
|
6
7
|
import AsfGuid from './AsfGuid.js';
|
|
@@ -8,7 +9,6 @@ import * as AsfObject from './AsfObject.js';
|
|
|
8
9
|
import { AsfContentParseError } from './AsfObject.js';
|
|
9
10
|
const debug = initDebug('music-metadata:parser:ASF');
|
|
10
11
|
const headerType = 'asf';
|
|
11
|
-
const maxAsfMetadataObjectSize = 16 * 1024 * 1024;
|
|
12
12
|
/**
|
|
13
13
|
* Windows Media Metadata Usage Guidelines
|
|
14
14
|
* - Ref: https://msdn.microsoft.com/en-us/library/ms867702.aspx
|
|
@@ -24,7 +24,7 @@ export class AsfParser extends BasicParser {
|
|
|
24
24
|
codecs = [];
|
|
25
25
|
streamBitrates = new Map();
|
|
26
26
|
async parse() {
|
|
27
|
-
const header = await this.
|
|
27
|
+
const header = await this.readToken(AsfObject.TopLevelHeaderObjectToken);
|
|
28
28
|
if (!header.objectId.equals(AsfGuid.HeaderObject)) {
|
|
29
29
|
throw new AsfContentParseError(`expected asf header; but was not found; got: ${header.objectId.str}`);
|
|
30
30
|
}
|
|
@@ -89,7 +89,7 @@ export class AsfParser extends BasicParser {
|
|
|
89
89
|
case AsfObject.FilePropertiesObject.guid.str: {
|
|
90
90
|
// 3.2
|
|
91
91
|
this.validateAllocationSize(payloadSize, 'File Properties Object');
|
|
92
|
-
const fpo = await this.
|
|
92
|
+
const fpo = await this.readToken(new AsfObject.FilePropertiesObject(header));
|
|
93
93
|
this.metadata.setFormat('duration', Number(fpo.playDuration / BigInt(1000)) / 10000 - Number(fpo.preroll) / 1000);
|
|
94
94
|
this.metadata.setFormat('bitrate', fpo.maximumBitrate);
|
|
95
95
|
break;
|
|
@@ -97,7 +97,7 @@ export class AsfParser extends BasicParser {
|
|
|
97
97
|
case AsfObject.StreamPropertiesObject.guid.str: {
|
|
98
98
|
// 3.3
|
|
99
99
|
this.validateAllocationSize(payloadSize, 'Stream Properties Object');
|
|
100
|
-
const spo = await this.
|
|
100
|
+
const spo = await this.readToken(new AsfObject.StreamPropertiesObject(header));
|
|
101
101
|
this.metadata.setFormat('container', `ASF/${spo.streamType}`);
|
|
102
102
|
if (spo.streamType === 'audio') {
|
|
103
103
|
this.metadata.setFormat('hasAudio', true);
|
|
@@ -114,7 +114,7 @@ export class AsfParser extends BasicParser {
|
|
|
114
114
|
if (payloadSize < extensionHeaderToken.len) {
|
|
115
115
|
throw new AsfContentParseError(`ASF Header Extension Object payload is too small: ${payloadSize} bytes`);
|
|
116
116
|
}
|
|
117
|
-
const extHeader = await this.
|
|
117
|
+
const extHeader = await this.readToken(extensionHeaderToken);
|
|
118
118
|
const expectedExtensionSize = payloadSize - extensionHeaderToken.len;
|
|
119
119
|
if (extHeader.extensionDataSize !== expectedExtensionSize) {
|
|
120
120
|
throw new AsfContentParseError(`ASF extension data size ${extHeader.extensionDataSize} does not match enclosing payload size ${expectedExtensionSize}`);
|
|
@@ -124,12 +124,12 @@ export class AsfParser extends BasicParser {
|
|
|
124
124
|
}
|
|
125
125
|
case AsfObject.ContentDescriptionObjectState.guid.str: // 3.10
|
|
126
126
|
this.validateAllocationSize(payloadSize, 'Content Description Object');
|
|
127
|
-
tags = await this.
|
|
127
|
+
tags = await this.readToken(new AsfObject.ContentDescriptionObjectState(header));
|
|
128
128
|
await this.addTags(tags);
|
|
129
129
|
break;
|
|
130
130
|
case AsfObject.ExtendedContentDescriptionObjectState.guid.str: // 3.11
|
|
131
131
|
this.validateAllocationSize(payloadSize, 'Extended Content Description Object');
|
|
132
|
-
tags = await this.
|
|
132
|
+
tags = await this.readToken(new AsfObject.ExtendedContentDescriptionObjectState(header));
|
|
133
133
|
await this.addTags(tags);
|
|
134
134
|
break;
|
|
135
135
|
case AsfGuid.CodecListObject.str: {
|
|
@@ -154,13 +154,13 @@ export class AsfParser extends BasicParser {
|
|
|
154
154
|
if (payloadSize < 2) {
|
|
155
155
|
throw new AsfContentParseError('Truncated Stream Bitrate Properties Object');
|
|
156
156
|
}
|
|
157
|
-
const count = await this.
|
|
157
|
+
const count = await this.readToken(Token.UINT16_LE);
|
|
158
158
|
if (count * 6 > payloadSize - 2) {
|
|
159
159
|
throw new AsfContentParseError('ASF stream bitrate records exceed object payload');
|
|
160
160
|
}
|
|
161
161
|
for (let index = 0; index < count; ++index) {
|
|
162
|
-
const streamNumber = (await this.
|
|
163
|
-
this.streamBitrates.set(streamNumber, await this.
|
|
162
|
+
const streamNumber = (await this.readToken(Token.UINT16_LE)) & 0x7f;
|
|
163
|
+
this.streamBitrates.set(streamNumber, await this.readToken(Token.UINT32_LE));
|
|
164
164
|
}
|
|
165
165
|
await this.ignorePayload(payloadSize - 2 - count * 6, 'Stream Bitrate Properties Object');
|
|
166
166
|
break;
|
|
@@ -188,7 +188,7 @@ export class AsfParser extends BasicParser {
|
|
|
188
188
|
if (remainingSize < AsfObject.HeaderObjectToken.len) {
|
|
189
189
|
throw new AsfContentParseError(`Insufficient ASF ${containerType} data for an object header: ${remainingSize} bytes`);
|
|
190
190
|
}
|
|
191
|
-
const header = await this.
|
|
191
|
+
const header = await this.readToken(AsfObject.HeaderObjectToken);
|
|
192
192
|
if (header.objectSize > remainingSize) {
|
|
193
193
|
throw new AsfContentParseError(`ASF ${containerType} object size ${header.objectSize} exceeds remaining ${containerType} payload size ${remainingSize}`);
|
|
194
194
|
}
|
|
@@ -209,9 +209,7 @@ export class AsfParser extends BasicParser {
|
|
|
209
209
|
return payloadSize;
|
|
210
210
|
}
|
|
211
211
|
validateAllocationSize(payloadSize, objectType) {
|
|
212
|
-
|
|
213
|
-
throw new AsfContentParseError(`ASF ${objectType} payload size ${payloadSize} exceeds allocation limit ${maxAsfMetadataObjectSize}`);
|
|
214
|
-
}
|
|
212
|
+
validateAllocation(payloadSize, this.options, message => new AsfContentParseError(`ASF ${objectType}: ${message}`));
|
|
215
213
|
}
|
|
216
214
|
async ignorePayload(payloadSize, objectType) {
|
|
217
215
|
let ignored;
|
|
@@ -236,7 +234,7 @@ export class AsfParser extends BasicParser {
|
|
|
236
234
|
case AsfObject.ExtendedStreamPropertiesObjectState.guid.str: {
|
|
237
235
|
// 4.1
|
|
238
236
|
this.validateAllocationSize(payloadSize, 'Extended Stream Properties Object');
|
|
239
|
-
const extended = await this.
|
|
237
|
+
const extended = await this.readToken(new AsfObject.ExtendedStreamPropertiesObjectState(header));
|
|
240
238
|
if (extended.streamPropertiesObject) {
|
|
241
239
|
const stream = extended.streamPropertiesObject;
|
|
242
240
|
this.streams.set(stream.streamNumber, stream);
|
|
@@ -247,14 +245,14 @@ export class AsfParser extends BasicParser {
|
|
|
247
245
|
case AsfObject.MetadataObjectState.guid.str: {
|
|
248
246
|
// 4.7
|
|
249
247
|
this.validateAllocationSize(payloadSize, 'Metadata Object');
|
|
250
|
-
const moTags = await this.
|
|
248
|
+
const moTags = await this.readToken(new AsfObject.MetadataObjectState(header));
|
|
251
249
|
await this.addTags(moTags);
|
|
252
250
|
break;
|
|
253
251
|
}
|
|
254
252
|
case AsfObject.MetadataLibraryObjectState.guid.str: {
|
|
255
253
|
// 4.8
|
|
256
254
|
this.validateAllocationSize(payloadSize, 'Metadata Library Object');
|
|
257
|
-
const mlTags = await this.
|
|
255
|
+
const mlTags = await this.readToken(new AsfObject.MetadataLibraryObjectState(header));
|
|
258
256
|
await this.addTags(mlTags);
|
|
259
257
|
break;
|
|
260
258
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IGetToken } from '@tokenizer/token';
|
|
2
|
+
import { type IFileInfo, type ITokenizer, type ITokenizerOptions } from 'strtok3';
|
|
3
|
+
import type { IOptions } from '../type.js';
|
|
4
|
+
type ErrorFactory = (message: string) => Error;
|
|
5
|
+
export declare function getMaxMemoryAllocation(options: IOptions): number;
|
|
6
|
+
/** Configure the tokenizer safety net once, using the same limit as parser-owned buffers. */
|
|
7
|
+
export declare function getTokenizerOptions(options: IOptions, fileInfo?: IFileInfo): ITokenizerOptions;
|
|
8
|
+
/** Check untrusted lengths before allocating, including accumulated stream payloads. */
|
|
9
|
+
export declare function validateAllocation(length: number, options: IOptions, makeError?: ErrorFactory): void;
|
|
10
|
+
/** Read only bounded chunks from streams; an advertised stream size does not prove bytes exist. */
|
|
11
|
+
export declare function readBuffer(tokenizer: ITokenizer, length: number, options: IOptions, position?: number, makeError?: ErrorFactory): Promise<Uint8Array>;
|
|
12
|
+
/** Bound custom tokenizer reads and buffer large stream payloads incrementally. */
|
|
13
|
+
export declare function readToken<T>(tokenizer: ITokenizer, token: IGetToken<T>, options: IOptions, position?: number, makeError?: ErrorFactory): Promise<T>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EndOfStreamError } from 'strtok3';
|
|
2
|
+
import { UnexpectedFileContentError } from '../ParseError.js';
|
|
3
|
+
const defaultMaxMemoryAllocation = 64 * 1024 * 1024;
|
|
4
|
+
const readChunkSize = 64 * 1024;
|
|
5
|
+
const allocationError = message => new UnexpectedFileContentError('metadata', message);
|
|
6
|
+
export function getMaxMemoryAllocation(options) {
|
|
7
|
+
const limit = options.maxMemoryAllocation ?? defaultMaxMemoryAllocation;
|
|
8
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
9
|
+
throw new RangeError('maxMemoryAllocation must be a positive safe integer');
|
|
10
|
+
}
|
|
11
|
+
return limit;
|
|
12
|
+
}
|
|
13
|
+
/** Configure the tokenizer safety net once, using the same limit as parser-owned buffers. */
|
|
14
|
+
export function getTokenizerOptions(options, fileInfo) {
|
|
15
|
+
return { maxBufferSize: getMaxMemoryAllocation(options), fileInfo };
|
|
16
|
+
}
|
|
17
|
+
/** Check untrusted lengths before allocating, including accumulated stream payloads. */
|
|
18
|
+
export function validateAllocation(length, options, makeError = allocationError) {
|
|
19
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
20
|
+
throw makeError(`Invalid allocation size: ${length}`);
|
|
21
|
+
}
|
|
22
|
+
const limit = getMaxMemoryAllocation(options);
|
|
23
|
+
if (length > limit) {
|
|
24
|
+
throw makeError(`Metadata size ${length} exceeds allocation limit ${limit} (buffering limit)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function validateAvailableBytes(tokenizer, length, position) {
|
|
28
|
+
if (tokenizer.fileInfo.size !== undefined && length > tokenizer.fileInfo.size - position) {
|
|
29
|
+
throw new EndOfStreamError();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Read only bounded chunks from streams; an advertised stream size does not prove bytes exist. */
|
|
33
|
+
export async function readBuffer(tokenizer, length, options, position = tokenizer.position, makeError = allocationError) {
|
|
34
|
+
validateAllocation(length, options, makeError);
|
|
35
|
+
validateAvailableBytes(tokenizer, length, position);
|
|
36
|
+
if ((!tokenizer.supportsRandomAccess() || tokenizer.fileInfo.size === undefined) && length > readChunkSize) {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
for (let remaining = length; remaining > 0;) {
|
|
39
|
+
const chunk = new Uint8Array(Math.min(remaining, readChunkSize));
|
|
40
|
+
const bytesRead = await tokenizer.readBuffer(chunk, { position });
|
|
41
|
+
if (bytesRead !== chunk.length) {
|
|
42
|
+
throw new EndOfStreamError();
|
|
43
|
+
}
|
|
44
|
+
chunks.push(chunk);
|
|
45
|
+
remaining -= chunk.length;
|
|
46
|
+
position += chunk.length;
|
|
47
|
+
}
|
|
48
|
+
const buffer = new Uint8Array(length);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
buffer.set(chunk, offset);
|
|
52
|
+
offset += chunk.length;
|
|
53
|
+
}
|
|
54
|
+
return buffer;
|
|
55
|
+
}
|
|
56
|
+
const buffer = new Uint8Array(length);
|
|
57
|
+
if ((await tokenizer.readBuffer(buffer, { position })) !== length) {
|
|
58
|
+
throw new EndOfStreamError();
|
|
59
|
+
}
|
|
60
|
+
return buffer;
|
|
61
|
+
}
|
|
62
|
+
/** Bound custom tokenizer reads and buffer large stream payloads incrementally. */
|
|
63
|
+
export async function readToken(tokenizer, token, options, position = tokenizer.position, makeError = allocationError) {
|
|
64
|
+
validateAllocation(token.len, options, makeError);
|
|
65
|
+
validateAvailableBytes(tokenizer, token.len, position);
|
|
66
|
+
if (token.len <= readChunkSize) {
|
|
67
|
+
return tokenizer.readToken(token, position);
|
|
68
|
+
}
|
|
69
|
+
return token.get(await readBuffer(tokenizer, token.len, options, position, makeError), 0);
|
|
70
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IGetToken } from '@tokenizer/token';
|
|
1
2
|
import type { ITokenizer } from 'strtok3';
|
|
2
3
|
import type { ITokenParser } from '../ParserFactory.js';
|
|
3
4
|
import type { IOptions } from '../type.js';
|
|
@@ -13,5 +14,6 @@ export declare abstract class BasicParser implements ITokenParser {
|
|
|
13
14
|
* @param {IOptions} options Parsing options
|
|
14
15
|
*/
|
|
15
16
|
constructor(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions);
|
|
17
|
+
protected readToken<T>(token: IGetToken<T>, position?: number): Promise<T>;
|
|
16
18
|
abstract parse(): Promise<void>;
|
|
17
19
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMaxMemoryAllocation, readToken } from './Allocation.js';
|
|
1
2
|
export class BasicParser {
|
|
2
3
|
metadata;
|
|
3
4
|
tokenizer;
|
|
@@ -9,8 +10,12 @@ export class BasicParser {
|
|
|
9
10
|
* @param {IOptions} options Parsing options
|
|
10
11
|
*/
|
|
11
12
|
constructor(metadata, tokenizer, options) {
|
|
13
|
+
getMaxMemoryAllocation(options);
|
|
12
14
|
this.metadata = metadata;
|
|
13
15
|
this.tokenizer = tokenizer;
|
|
14
16
|
this.options = options;
|
|
15
17
|
}
|
|
18
|
+
readToken(token, position) {
|
|
19
|
+
return readToken(this.tokenizer, token, this.options, position);
|
|
20
|
+
}
|
|
16
21
|
}
|
package/lib/core.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { fromBlob, fromBuffer, fromWebStream } from 'strtok3';
|
|
5
5
|
import { APEv2Parser } from './apev2/APEv2Parser.js';
|
|
6
|
+
import { getTokenizerOptions } from './common/Allocation.js';
|
|
6
7
|
import { hasID3v1Header } from './id3v1/ID3v1Parser.js';
|
|
7
8
|
import { getLyricsHeaderLength } from './lyrics3/Lyrics3.js';
|
|
8
9
|
import { ParserFactory } from './ParserFactory.js';
|
|
@@ -17,7 +18,7 @@ export { LyricsContentType, TimestampFormat, TrackType } from './type.js';
|
|
|
17
18
|
* @returns Metadata
|
|
18
19
|
*/
|
|
19
20
|
export async function parseBlob(blob, options = {}) {
|
|
20
|
-
const tokenizer = fromBlob(blob);
|
|
21
|
+
const tokenizer = fromBlob(blob, getTokenizerOptions(options));
|
|
21
22
|
try {
|
|
22
23
|
return await parseFromTokenizer(tokenizer, options);
|
|
23
24
|
}
|
|
@@ -33,9 +34,7 @@ export async function parseBlob(blob, options = {}) {
|
|
|
33
34
|
* @returns Metadata
|
|
34
35
|
*/
|
|
35
36
|
export async function parseWebStream(webStream, fileInfo, options = {}) {
|
|
36
|
-
const tokenizer = fromWebStream(webStream, {
|
|
37
|
-
fileInfo: typeof fileInfo === 'string' ? { mimeType: fileInfo } : fileInfo
|
|
38
|
-
});
|
|
37
|
+
const tokenizer = fromWebStream(webStream, getTokenizerOptions(options, typeof fileInfo === 'string' ? { mimeType: fileInfo } : fileInfo));
|
|
39
38
|
try {
|
|
40
39
|
return await parseFromTokenizer(tokenizer, options);
|
|
41
40
|
}
|
|
@@ -52,9 +51,7 @@ export async function parseWebStream(webStream, fileInfo, options = {}) {
|
|
|
52
51
|
* Ref: https://github.com/Borewit/strtok3/blob/e6938c81ff685074d5eb3064a11c0b03ca934c1d/src/index.ts#L15
|
|
53
52
|
*/
|
|
54
53
|
export async function parseBuffer(uint8Array, fileInfo, options = {}) {
|
|
55
|
-
const tokenizer = fromBuffer(uint8Array, {
|
|
56
|
-
fileInfo: typeof fileInfo === 'string' ? { mimeType: fileInfo } : fileInfo
|
|
57
|
-
});
|
|
54
|
+
const tokenizer = fromBuffer(uint8Array, getTokenizerOptions(options, typeof fileInfo === 'string' ? { mimeType: fileInfo } : fileInfo));
|
|
58
55
|
return parseFromTokenizer(tokenizer, options);
|
|
59
56
|
}
|
|
60
57
|
/**
|
|
@@ -22,6 +22,9 @@ export declare class DsdiffContentParseError extends DsdiffContentParseError_bas
|
|
|
22
22
|
*/
|
|
23
23
|
export declare class DsdiffParser extends BasicParser {
|
|
24
24
|
parse(): Promise<void>;
|
|
25
|
+
private validateChunkSize;
|
|
26
|
+
private requireChunkSize;
|
|
27
|
+
private ignoreBytes;
|
|
25
28
|
private readFmt8Chunks;
|
|
26
29
|
private readData;
|
|
27
30
|
private handleSoundPropertyChunks;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import * as strtok3 from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { getTokenizerOptions } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { FourCcToken } from '../common/FourCC.js';
|
|
6
7
|
import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
|
|
7
8
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
8
9
|
import { ChunkHeader64 } from './DsdiffToken.js';
|
|
9
|
-
const debug = initDebug('music-metadata:parser:
|
|
10
|
+
const debug = initDebug('music-metadata:parser:dsdiff');
|
|
10
11
|
export class DsdiffContentParseError extends makeUnexpectedFileContentError('DSDIFF') {
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
@@ -17,12 +18,14 @@ export class DsdiffContentParseError extends makeUnexpectedFileContentError('DSD
|
|
|
17
18
|
*/
|
|
18
19
|
export class DsdiffParser extends BasicParser {
|
|
19
20
|
async parse() {
|
|
20
|
-
const header = await this.
|
|
21
|
+
const header = await this.readToken(ChunkHeader64);
|
|
21
22
|
if (header.chunkID !== 'FRM8') {
|
|
22
23
|
throw new DsdiffContentParseError('Unexpected chunk-ID');
|
|
23
24
|
}
|
|
25
|
+
this.validateChunkSize(header);
|
|
26
|
+
this.requireChunkSize(header, FourCcToken.len);
|
|
24
27
|
this.metadata.setAudioOnly();
|
|
25
|
-
const type = (await this.
|
|
28
|
+
const type = (await this.readToken(FourCcToken)).trim();
|
|
26
29
|
switch (type) {
|
|
27
30
|
case 'DSD':
|
|
28
31
|
this.metadata.setFormat('container', `DSDIFF/${type}`);
|
|
@@ -32,13 +35,40 @@ export class DsdiffParser extends BasicParser {
|
|
|
32
35
|
throw new DsdiffContentParseError(`Unsupported DSDIFF type: ${type}`);
|
|
33
36
|
}
|
|
34
37
|
}
|
|
38
|
+
validateChunkSize(header, remainingSize) {
|
|
39
|
+
const size = header.chunkSize;
|
|
40
|
+
if (size < 0n || size > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
41
|
+
throw new DsdiffContentParseError(`Invalid chunk size: ${size}`);
|
|
42
|
+
}
|
|
43
|
+
const paddedSize = size + (size & 1n);
|
|
44
|
+
if (remainingSize !== undefined && paddedSize > remainingSize) {
|
|
45
|
+
throw new DsdiffContentParseError(`Chunk ${header.chunkID} size ${size} exceeds its enclosing chunk`);
|
|
46
|
+
}
|
|
47
|
+
const fileSize = this.tokenizer.fileInfo.size;
|
|
48
|
+
if (fileSize !== undefined && paddedSize > BigInt(fileSize - this.tokenizer.position)) {
|
|
49
|
+
throw new DsdiffContentParseError(`Chunk ${header.chunkID} size ${size} exceeds remaining file size`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
requireChunkSize(header, minimum) {
|
|
53
|
+
if (header.chunkSize < BigInt(minimum)) {
|
|
54
|
+
throw new DsdiffContentParseError(`Chunk ${header.chunkID} is too small: ${header.chunkSize}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async ignoreBytes(length) {
|
|
58
|
+
if ((await this.tokenizer.ignore(Number(length))) !== Number(length)) {
|
|
59
|
+
throw new DsdiffContentParseError('Unexpected end of chunk');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
35
62
|
async readFmt8Chunks(remainingSize) {
|
|
36
63
|
while (remainingSize >= ChunkHeader64.len) {
|
|
37
|
-
const chunkHeader = await this.
|
|
64
|
+
const chunkHeader = await this.readToken(ChunkHeader64);
|
|
65
|
+
this.validateChunkSize(chunkHeader, remainingSize - BigInt(ChunkHeader64.len));
|
|
38
66
|
// If the data is an odd number of bytes in length, a pad byte must be added at the end
|
|
39
67
|
debug(`Chunk id=${chunkHeader.chunkID}`);
|
|
40
68
|
await this.readData(chunkHeader);
|
|
41
|
-
|
|
69
|
+
const padding = chunkHeader.chunkSize & 1n;
|
|
70
|
+
await this.ignoreBytes(padding);
|
|
71
|
+
remainingSize -= BigInt(ChunkHeader64.len) + chunkHeader.chunkSize + padding;
|
|
42
72
|
}
|
|
43
73
|
}
|
|
44
74
|
async readData(header) {
|
|
@@ -46,14 +76,16 @@ export class DsdiffParser extends BasicParser {
|
|
|
46
76
|
const p0 = this.tokenizer.position;
|
|
47
77
|
switch (header.chunkID.trim()) {
|
|
48
78
|
case 'FVER': {
|
|
79
|
+
this.requireChunkSize(header, Token.UINT32_LE.len);
|
|
49
80
|
// 3.1 FORMAT VERSION CHUNK
|
|
50
|
-
const version = await this.
|
|
81
|
+
const version = await this.readToken(Token.UINT32_LE);
|
|
51
82
|
debug(`DSDIFF version=${version}`);
|
|
52
83
|
break;
|
|
53
84
|
}
|
|
54
85
|
case 'PROP': {
|
|
86
|
+
this.requireChunkSize(header, FourCcToken.len);
|
|
55
87
|
// 3.2 PROPERTY CHUNK
|
|
56
|
-
const propType = await this.
|
|
88
|
+
const propType = await this.readToken(FourCcToken);
|
|
57
89
|
if (propType !== 'SND ') {
|
|
58
90
|
throw new DsdiffContentParseError('Unexpected PROP-chunk ID');
|
|
59
91
|
}
|
|
@@ -62,8 +94,8 @@ export class DsdiffParser extends BasicParser {
|
|
|
62
94
|
}
|
|
63
95
|
case 'ID3': {
|
|
64
96
|
// Unofficial ID3 tag support
|
|
65
|
-
const id3_data = await this.
|
|
66
|
-
const rst = strtok3.fromBuffer(id3_data);
|
|
97
|
+
const id3_data = await this.readToken(new Token.Uint8ArrayType(Number(header.chunkSize)));
|
|
98
|
+
const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
|
|
67
99
|
await new ID3v2Parser().parse(this.metadata, rst, this.options);
|
|
68
100
|
break;
|
|
69
101
|
}
|
|
@@ -82,34 +114,39 @@ export class DsdiffParser extends BasicParser {
|
|
|
82
114
|
const remaining = header.chunkSize - BigInt(this.tokenizer.position - p0);
|
|
83
115
|
if (remaining > 0) {
|
|
84
116
|
debug(`After Parsing chunk, remaining ${remaining} bytes`);
|
|
85
|
-
await this.
|
|
117
|
+
await this.ignoreBytes(remaining);
|
|
86
118
|
}
|
|
87
119
|
}
|
|
88
120
|
async handleSoundPropertyChunks(remainingSize) {
|
|
89
121
|
debug(`Parsing sound-property-chunks, remainingSize=${remainingSize}`);
|
|
90
|
-
while (remainingSize
|
|
91
|
-
const sndPropHeader = await this.
|
|
122
|
+
while (remainingSize >= BigInt(ChunkHeader64.len)) {
|
|
123
|
+
const sndPropHeader = await this.readToken(ChunkHeader64);
|
|
124
|
+
this.validateChunkSize(sndPropHeader, remainingSize - BigInt(ChunkHeader64.len));
|
|
92
125
|
debug(`Sound-property-chunk[ID=${sndPropHeader.chunkID}, size=${sndPropHeader.chunkSize}]`);
|
|
93
126
|
const p0 = this.tokenizer.position;
|
|
94
127
|
switch (sndPropHeader.chunkID.trim()) {
|
|
95
128
|
case 'FS': {
|
|
129
|
+
this.requireChunkSize(sndPropHeader, Token.UINT32_BE.len);
|
|
96
130
|
// 3.2.1 Sample Rate Chunk
|
|
97
|
-
const sampleRate = await this.
|
|
131
|
+
const sampleRate = await this.readToken(Token.UINT32_BE);
|
|
98
132
|
this.metadata.setFormat('sampleRate', sampleRate);
|
|
99
133
|
break;
|
|
100
134
|
}
|
|
101
135
|
case 'CHNL': {
|
|
136
|
+
this.requireChunkSize(sndPropHeader, Token.UINT16_BE.len);
|
|
102
137
|
// 3.2.2 Channels Chunk
|
|
103
|
-
const numChannels = await this.
|
|
138
|
+
const numChannels = await this.readToken(Token.UINT16_BE);
|
|
104
139
|
this.metadata.setFormat('numberOfChannels', numChannels);
|
|
105
140
|
await this.handleChannelChunks(sndPropHeader.chunkSize - BigInt(Token.UINT16_BE.len));
|
|
106
141
|
break;
|
|
107
142
|
}
|
|
108
143
|
case 'CMPR': {
|
|
144
|
+
this.requireChunkSize(sndPropHeader, FourCcToken.len + Token.UINT8.len);
|
|
109
145
|
// 3.2.3 Compression Type Chunk
|
|
110
|
-
const compressionIdCode = (await this.
|
|
111
|
-
const count = await this.
|
|
112
|
-
|
|
146
|
+
const compressionIdCode = (await this.readToken(FourCcToken)).trim();
|
|
147
|
+
const count = await this.readToken(Token.UINT8);
|
|
148
|
+
this.requireChunkSize(sndPropHeader, FourCcToken.len + Token.UINT8.len + count);
|
|
149
|
+
const compressionName = await this.readToken(new Token.StringType(count, 'ascii'));
|
|
113
150
|
if (compressionIdCode === 'DSD') {
|
|
114
151
|
this.metadata.setFormat('lossless', true);
|
|
115
152
|
this.metadata.setFormat('bitsPerSample', 1);
|
|
@@ -118,32 +155,39 @@ export class DsdiffParser extends BasicParser {
|
|
|
118
155
|
break;
|
|
119
156
|
}
|
|
120
157
|
case 'ABSS': {
|
|
158
|
+
this.requireChunkSize(sndPropHeader, 8);
|
|
121
159
|
// 3.2.4 Absolute Start Time Chunk
|
|
122
|
-
const hours = await this.
|
|
123
|
-
const minutes = await this.
|
|
124
|
-
const seconds = await this.
|
|
125
|
-
const samples = await this.
|
|
160
|
+
const hours = await this.readToken(Token.UINT16_BE);
|
|
161
|
+
const minutes = await this.readToken(Token.UINT8);
|
|
162
|
+
const seconds = await this.readToken(Token.UINT8);
|
|
163
|
+
const samples = await this.readToken(Token.UINT32_BE);
|
|
126
164
|
debug(`ABSS ${hours}:${minutes}:${seconds}.${samples}`);
|
|
127
165
|
break;
|
|
128
166
|
}
|
|
129
167
|
case 'LSCO': {
|
|
168
|
+
this.requireChunkSize(sndPropHeader, Token.UINT16_BE.len);
|
|
130
169
|
// 3.2.5 Loudspeaker Configuration Chunk
|
|
131
|
-
const lsConfig = await this.
|
|
170
|
+
const lsConfig = await this.readToken(Token.UINT16_BE);
|
|
132
171
|
debug(`LSCO lsConfig=${lsConfig}`);
|
|
133
172
|
break;
|
|
134
173
|
}
|
|
135
174
|
default:
|
|
136
175
|
debug(`Unknown sound-property-chunk[ID=${sndPropHeader.chunkID}, size=${sndPropHeader.chunkSize}]`);
|
|
137
|
-
await this.
|
|
176
|
+
await this.ignoreBytes(sndPropHeader.chunkSize);
|
|
138
177
|
}
|
|
139
178
|
const remaining = sndPropHeader.chunkSize - BigInt(this.tokenizer.position - p0);
|
|
140
179
|
if (remaining > 0) {
|
|
141
180
|
debug(`After Parsing sound-property-chunk ${sndPropHeader.chunkSize}, remaining ${remaining} bytes`);
|
|
142
|
-
await this.
|
|
181
|
+
await this.ignoreBytes(remaining);
|
|
143
182
|
}
|
|
144
|
-
|
|
183
|
+
const padding = sndPropHeader.chunkSize & 1n;
|
|
184
|
+
await this.ignoreBytes(padding);
|
|
185
|
+
remainingSize -= BigInt(ChunkHeader64.len) + sndPropHeader.chunkSize + padding;
|
|
145
186
|
debug(`Parsing sound-property-chunks, remainingSize=${remainingSize}`);
|
|
146
187
|
}
|
|
188
|
+
if (remainingSize !== 0n) {
|
|
189
|
+
throw new DsdiffContentParseError('Incomplete sound-property chunk header');
|
|
190
|
+
}
|
|
147
191
|
if (this.metadata.format.lossless &&
|
|
148
192
|
this.metadata.format.sampleRate &&
|
|
149
193
|
this.metadata.format.numberOfChannels &&
|
|
@@ -154,14 +198,10 @@ export class DsdiffParser extends BasicParser {
|
|
|
154
198
|
}
|
|
155
199
|
async handleChannelChunks(remainingSize) {
|
|
156
200
|
debug(`Parsing channel-chunks, remainingSize=${remainingSize}`);
|
|
157
|
-
const channels = [];
|
|
158
201
|
while (remainingSize >= FourCcToken.len) {
|
|
159
|
-
const channelId = await this.
|
|
202
|
+
const channelId = await this.readToken(FourCcToken);
|
|
160
203
|
debug(`Channel[ID=${channelId}]`);
|
|
161
|
-
channels.push(channelId);
|
|
162
204
|
remainingSize -= BigInt(FourCcToken.len);
|
|
163
205
|
}
|
|
164
|
-
debug(`Channels: ${channels.join(', ')}`);
|
|
165
|
-
return channels;
|
|
166
206
|
}
|
|
167
207
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ITokenizer } from 'strtok3';
|
|
2
|
+
import type { IOptions } from '../type.js';
|
|
2
3
|
import { type IElementType, type ITree, type ValueType } from './types.js';
|
|
3
4
|
declare const EbmlContentError_base: {
|
|
4
5
|
new (message: string): {
|
|
@@ -45,6 +46,7 @@ export type IElementListener = {
|
|
|
45
46
|
* WEBM VP8 AUDIO FILE
|
|
46
47
|
*/
|
|
47
48
|
export declare class EbmlIterator {
|
|
49
|
+
private readonly options;
|
|
48
50
|
private parserMap;
|
|
49
51
|
private ebmlMaxIDLength;
|
|
50
52
|
private ebmlMaxSizeLength;
|
|
@@ -53,9 +55,10 @@ export declare class EbmlIterator {
|
|
|
53
55
|
* @param {ITokenizer} tokenizer Input
|
|
54
56
|
* @param tokenizer
|
|
55
57
|
*/
|
|
56
|
-
constructor(tokenizer: ITokenizer);
|
|
58
|
+
constructor(tokenizer: ITokenizer, options?: IOptions);
|
|
57
59
|
iterate(dtdElement: IElementType, posDone: number, listener: IElementListener): Promise<ITree>;
|
|
58
60
|
private parseContainer;
|
|
61
|
+
private ignore;
|
|
59
62
|
private readVintData;
|
|
60
63
|
private readElement;
|
|
61
64
|
private readFloat;
|
package/lib/ebml/EbmlIterator.js
CHANGED
|
@@ -2,6 +2,7 @@ import initDebug from 'debug';
|
|
|
2
2
|
import { EndOfStreamError } from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
4
|
import { Float32_BE, Float64_BE, StringType, UINT8 } from 'token-types';
|
|
5
|
+
import { readBuffer } from '../common/Allocation.js';
|
|
5
6
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
6
7
|
import { DataType } from './types.js';
|
|
7
8
|
const debug = initDebug('music-metadata:parser:ebml');
|
|
@@ -22,6 +23,7 @@ export const ParseAction = {
|
|
|
22
23
|
* WEBM VP8 AUDIO FILE
|
|
23
24
|
*/
|
|
24
25
|
export class EbmlIterator {
|
|
26
|
+
options;
|
|
25
27
|
parserMap = new Map();
|
|
26
28
|
ebmlMaxIDLength = 4;
|
|
27
29
|
ebmlMaxSizeLength = 8;
|
|
@@ -30,7 +32,8 @@ export class EbmlIterator {
|
|
|
30
32
|
* @param {ITokenizer} tokenizer Input
|
|
31
33
|
* @param tokenizer
|
|
32
34
|
*/
|
|
33
|
-
constructor(tokenizer) {
|
|
35
|
+
constructor(tokenizer, options = {}) {
|
|
36
|
+
this.options = options;
|
|
34
37
|
this.tokenizer = tokenizer;
|
|
35
38
|
this.parserMap.set(DataType.uint, e => this.readUint(e));
|
|
36
39
|
this.parserMap.set(DataType.string, e => this.readString(e));
|
|
@@ -104,11 +107,11 @@ export class EbmlIterator {
|
|
|
104
107
|
break;
|
|
105
108
|
case ParseAction.IgnoreElement:
|
|
106
109
|
debug(`Ignore element: name=${getElementPath(child)}, element.id=0x${element.id}, container=${!!child.container} at position=${elementPosition}`);
|
|
107
|
-
await this.
|
|
110
|
+
await this.ignore(element.len, posDone);
|
|
108
111
|
break;
|
|
109
112
|
case ParseAction.SkipSiblings:
|
|
110
113
|
debug(`Ignore remaining container, at: name=${getElementPath(child)}, element.id=0x${element.id}, container=${!!child.container} at position=${elementPosition}`);
|
|
111
|
-
await this.
|
|
114
|
+
await this.ignore(posDone - this.tokenizer.position, posDone);
|
|
112
115
|
break;
|
|
113
116
|
case ParseAction.TerminateParsing:
|
|
114
117
|
debug(`Terminate parsing at element: name=${getElementPath(child)}, element.id=0x${element.id}, container=${!!child.container} at position=${elementPosition}`);
|
|
@@ -118,16 +121,26 @@ export class EbmlIterator {
|
|
|
118
121
|
else {
|
|
119
122
|
switch (element.id) {
|
|
120
123
|
case 0xec: // void
|
|
121
|
-
await this.
|
|
124
|
+
await this.ignore(element.len, posDone);
|
|
122
125
|
break;
|
|
123
126
|
default:
|
|
124
127
|
debug(`parseEbml: parent=${getElementPath(dtdElement)}, unknown child: id=${element.id.toString(16)} at position=${elementPosition}`);
|
|
125
|
-
await this.
|
|
128
|
+
await this.ignore(element.len, posDone);
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
return tree;
|
|
130
133
|
}
|
|
134
|
+
async ignore(length, posDone) {
|
|
135
|
+
// Unknown-size EBML containers may advertise lengths above the safe integer range.
|
|
136
|
+
// Bound skips by the enclosing container and known input before passing them to the tokenizer.
|
|
137
|
+
const end = Math.min(posDone, this.tokenizer.fileInfo.size ?? Number.POSITIVE_INFINITY);
|
|
138
|
+
const boundedLength = Math.min(length, end - this.tokenizer.position);
|
|
139
|
+
if (!Number.isSafeInteger(boundedLength) || boundedLength < 0) {
|
|
140
|
+
throw new EbmlContentError(`Invalid element length: ${length}`);
|
|
141
|
+
}
|
|
142
|
+
await this.tokenizer.ignore(boundedLength);
|
|
143
|
+
}
|
|
131
144
|
async readVintData(maxLength) {
|
|
132
145
|
const msb = await this.tokenizer.peekNumber(UINT8);
|
|
133
146
|
let mask = 0x80;
|
|
@@ -180,28 +193,7 @@ export class EbmlIterator {
|
|
|
180
193
|
return rawString.replace(/\x00.*$/g, '');
|
|
181
194
|
}
|
|
182
195
|
async readBuffer(e) {
|
|
183
|
-
|
|
184
|
-
if ((!this.tokenizer.supportsRandomAccess() || this.tokenizer.fileInfo.size === undefined) && e.len > chunkSize) {
|
|
185
|
-
// Neither a declared container size nor an advertised stream size proves these bytes exist.
|
|
186
|
-
// Allocate incrementally, and only assemble the leaf after reading its payload.
|
|
187
|
-
const chunks = [];
|
|
188
|
-
for (let remaining = e.len; remaining > 0;) {
|
|
189
|
-
const chunk = new Uint8Array(Math.min(remaining, chunkSize));
|
|
190
|
-
await this.tokenizer.readBuffer(chunk);
|
|
191
|
-
chunks.push(chunk);
|
|
192
|
-
remaining -= chunk.length;
|
|
193
|
-
}
|
|
194
|
-
const buf = new Uint8Array(e.len);
|
|
195
|
-
let offset = 0;
|
|
196
|
-
for (const chunk of chunks) {
|
|
197
|
-
buf.set(chunk, offset);
|
|
198
|
-
offset += chunk.length;
|
|
199
|
-
}
|
|
200
|
-
return buf;
|
|
201
|
-
}
|
|
202
|
-
const buf = new Uint8Array(e.len);
|
|
203
|
-
await this.tokenizer.readBuffer(buf);
|
|
204
|
-
return buf;
|
|
196
|
+
return readBuffer(this.tokenizer, e.len, this.options, undefined, message => new EbmlContentError(message));
|
|
205
197
|
}
|
|
206
198
|
}
|
|
207
199
|
function readUIntBE(buf, len) {
|
package/lib/flac/FlacParser.js
CHANGED
|
@@ -13,14 +13,14 @@ class FlacContentError extends makeUnexpectedFileContentError('FLAC') {
|
|
|
13
13
|
export class FlacParser extends AbstractID3Parser {
|
|
14
14
|
vorbisParser = new VorbisStream(this.metadata, this.options);
|
|
15
15
|
async postId3v2Parse() {
|
|
16
|
-
const fourCC = await this.
|
|
16
|
+
const fourCC = await this.readToken(FourCcToken);
|
|
17
17
|
if (fourCC.toString() !== 'fLaC') {
|
|
18
18
|
throw new FlacContentError('Invalid FLAC preamble');
|
|
19
19
|
}
|
|
20
20
|
let blockHeader;
|
|
21
21
|
do {
|
|
22
22
|
// Read block header
|
|
23
|
-
blockHeader = await this.
|
|
23
|
+
blockHeader = await this.readToken(Flac.BlockHeader);
|
|
24
24
|
// Parse block data
|
|
25
25
|
await this.parseDataBlock(blockHeader);
|
|
26
26
|
} while (!blockHeader.lastBlock);
|
|
@@ -60,7 +60,7 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
60
60
|
if (dataLen !== Flac.BlockStreamInfo.len) {
|
|
61
61
|
throw new FlacContentError('Unexpected block-stream-info length');
|
|
62
62
|
}
|
|
63
|
-
const streamInfo = await this.
|
|
63
|
+
const streamInfo = await this.readToken(Flac.BlockStreamInfo);
|
|
64
64
|
this.metadata.setFormat('container', 'FLAC');
|
|
65
65
|
this.processsStreamInfo(streamInfo);
|
|
66
66
|
}
|
|
@@ -83,7 +83,7 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
83
83
|
* Ref: https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-640004.2.3
|
|
84
84
|
*/
|
|
85
85
|
async readComment(dataLen) {
|
|
86
|
-
const data = await this.
|
|
86
|
+
const data = await this.readToken(new Uint8ArrayType(dataLen));
|
|
87
87
|
return this.parseComment(data);
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
@@ -97,9 +97,12 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
97
97
|
this.metadata.setFormat('tool', vendor);
|
|
98
98
|
}
|
|
99
99
|
const commentListLength = decoder.readInt32();
|
|
100
|
-
|
|
100
|
+
if (commentListLength > Math.floor(decoder.remaining / 4)) {
|
|
101
|
+
throw new FlacContentError(`Vorbis comment count ${commentListLength} exceeds remaining data`);
|
|
102
|
+
}
|
|
103
|
+
const tags = [];
|
|
101
104
|
for (let i = 0; i < commentListLength; i++) {
|
|
102
|
-
tags
|
|
105
|
+
tags.push(decoder.parseUserComment());
|
|
103
106
|
}
|
|
104
107
|
await Promise.all(tags.map(tag => {
|
|
105
108
|
if (tag.key === 'ENCODER') {
|
|
@@ -112,7 +115,7 @@ export class FlacParser extends AbstractID3Parser {
|
|
|
112
115
|
if (this.options.skipCovers) {
|
|
113
116
|
return this.tokenizer.ignore(dataLen);
|
|
114
117
|
}
|
|
115
|
-
return this.addPictureTag(await this.
|
|
118
|
+
return this.addPictureTag(await this.readToken(new VorbisPictureToken(dataLen)));
|
|
116
119
|
}
|
|
117
120
|
addPictureTag(picture) {
|
|
118
121
|
return this.addTag('METADATA_BLOCK_PICTURE', picture);
|
package/lib/id3v2/ID3v2Parser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Token from 'token-types';
|
|
2
|
+
import { readToken } from '../common/Allocation.js';
|
|
2
3
|
import { getFrameHeaderLength, readFrameHeader } from './FrameHeader.js';
|
|
3
4
|
import { FrameParser, Id3v2ContentError } from './FrameParser.js';
|
|
4
5
|
import { ExtendedHeader, ID3v2Header } from './ID3v2Token.js';
|
|
@@ -84,7 +85,7 @@ export class ID3v2Parser {
|
|
|
84
85
|
return this.parseId3Data(this.id3Header.size - extendedHeaderSize);
|
|
85
86
|
}
|
|
86
87
|
async parseId3Data(dataLen) {
|
|
87
|
-
const uint8Array = await this.tokenizer
|
|
88
|
+
const uint8Array = await readToken(this.tokenizer, new Token.Uint8ArrayType(dataLen), this.options, undefined, message => new Id3v2ContentError(message));
|
|
88
89
|
for (const tag of this.parseMetadata(uint8Array)) {
|
|
89
90
|
switch (tag.id) {
|
|
90
91
|
case 'TXXX':
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import initDebug from 'debug';
|
|
5
5
|
import { fromFile, fromStream } from 'strtok3';
|
|
6
|
+
import { getTokenizerOptions } from './common/Allocation.js';
|
|
6
7
|
import { CouldNotDetermineFileTypeError, parseFromTokenizer, UnsupportedFileTypeError } from './core.js';
|
|
7
8
|
import { ParserFactory } from './ParserFactory.js';
|
|
8
9
|
export * from './core.js';
|
|
@@ -15,9 +16,7 @@ const debug = initDebug('music-metadata:parser');
|
|
|
15
16
|
* @returns Metadata
|
|
16
17
|
*/
|
|
17
18
|
export async function parseStream(stream, fileInfo, options = {}) {
|
|
18
|
-
const tokenizer = await fromStream(stream, {
|
|
19
|
-
fileInfo: typeof fileInfo === 'string' ? { mimeType: fileInfo } : fileInfo
|
|
20
|
-
});
|
|
19
|
+
const tokenizer = await fromStream(stream, getTokenizerOptions(options, typeof fileInfo === 'string' ? { mimeType: fileInfo } : fileInfo));
|
|
21
20
|
try {
|
|
22
21
|
return await parseFromTokenizer(tokenizer, options);
|
|
23
22
|
}
|
|
@@ -33,7 +32,7 @@ export async function parseStream(stream, fileInfo, options = {}) {
|
|
|
33
32
|
*/
|
|
34
33
|
export async function parseFile(filePath, options = {}) {
|
|
35
34
|
debug(`parseFile: ${filePath}`);
|
|
36
|
-
const fileTokenizer = await fromFile(filePath);
|
|
35
|
+
const fileTokenizer = await fromFile(filePath, getTokenizerOptions(options));
|
|
37
36
|
const parserFactory = new ParserFactory();
|
|
38
37
|
try {
|
|
39
38
|
const parserLoader = parserFactory.findLoaderForExtension(filePath);
|
|
@@ -21,7 +21,7 @@ export class MatroskaParser extends BasicParser {
|
|
|
21
21
|
flagUseIndexToSkipClusters = this.options.mkvUseIndex ?? false;
|
|
22
22
|
async parse() {
|
|
23
23
|
const containerSize = this.tokenizer.fileInfo.size ?? Number.MAX_SAFE_INTEGER;
|
|
24
|
-
const matroskaIterator = new EbmlIterator(this.tokenizer);
|
|
24
|
+
const matroskaIterator = new EbmlIterator(this.tokenizer, this.options);
|
|
25
25
|
debug('Initializing DTD end MatroskaIterator');
|
|
26
26
|
await matroskaIterator.iterate(matroskaDtd, containerSize, {
|
|
27
27
|
startNext: element => {
|
package/lib/mp4/MP4Parser.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { IGetToken } from '@tokenizer/token';
|
|
1
2
|
import { BasicParser } from '../common/BasicParser.js';
|
|
2
3
|
import { Atom } from './Atom.js';
|
|
3
4
|
export declare class MP4Parser extends BasicParser {
|
|
4
5
|
private static read_BE_Integer;
|
|
5
6
|
private validatePayloadLength;
|
|
6
|
-
|
|
7
|
+
protected readToken<T>(token: IGetToken<T>, position?: number): Promise<T>;
|
|
7
8
|
private tracks;
|
|
8
9
|
private mediaDataRanges;
|
|
9
10
|
private streamedChapters;
|
package/lib/mp4/MP4Parser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { textDecode } from '@borewit/text-codec';
|
|
|
2
2
|
import initDebug from 'debug';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
4
|
import { uint8ArrayToHex } from 'uint8array-extras';
|
|
5
|
+
import { readToken, validateAllocation } from '../common/Allocation.js';
|
|
5
6
|
import { BasicParser } from '../common/BasicParser.js';
|
|
6
7
|
import { Genres } from '../id3v1/ID3v1Parser.js';
|
|
7
8
|
import { TrackType } from '../type.js';
|
|
@@ -123,13 +124,10 @@ export class MP4Parser extends BasicParser {
|
|
|
123
124
|
// Metadata and sample tables are buffered; media data is skipped separately.
|
|
124
125
|
// A bound is also necessary for streams whose total size is unknown.
|
|
125
126
|
validatePayloadLength(length) {
|
|
126
|
-
|
|
127
|
-
throw new Mp4ContentError(`Atom payload exceeds the 64 MiB buffering limit: ${length}`);
|
|
128
|
-
}
|
|
127
|
+
validateAllocation(length, this.options, message => new Mp4ContentError(message));
|
|
129
128
|
}
|
|
130
129
|
async readToken(token, position) {
|
|
131
|
-
this.
|
|
132
|
-
return this.tokenizer.readToken(token, position);
|
|
130
|
+
return readToken(this.tokenizer, token, this.options, position, message => new Mp4ContentError(message));
|
|
133
131
|
}
|
|
134
132
|
tracks = new Map();
|
|
135
133
|
mediaDataRanges = [];
|
package/lib/ogg/OggParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import { EndOfStreamError } from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { readToken } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { createAudioTrackInfo } from '../common/TrackInfo.js';
|
|
6
7
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
@@ -34,7 +35,7 @@ class OggStream {
|
|
|
34
35
|
debug('serial=%s page#=%s, Ogg.id=%s', header.streamSerialNumber, header.pageSequenceNo, header.capturePattern);
|
|
35
36
|
const segmentTable = await tokenizer.readToken(new SegmentTable(header));
|
|
36
37
|
debug('totalPageSize=%s', segmentTable.totalPageSize);
|
|
37
|
-
const pageData = await
|
|
38
|
+
const pageData = await readToken(tokenizer, new Token.Uint8ArrayType(segmentTable.totalPageSize), this.options);
|
|
38
39
|
this.payloadBytes += pageData.length;
|
|
39
40
|
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
|
|
40
41
|
if (header.headerType.firstPage) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { textDecode } from '@borewit/text-codec';
|
|
2
2
|
import * as Token from 'token-types';
|
|
3
|
+
import { makeUnexpectedFileContentError } from '../../ParseError.js';
|
|
4
|
+
class VorbisCommentError extends makeUnexpectedFileContentError('Vorbis') {
|
|
5
|
+
}
|
|
3
6
|
export class VorbisDecoder {
|
|
4
7
|
data;
|
|
5
8
|
offset;
|
|
@@ -7,13 +10,22 @@ export class VorbisDecoder {
|
|
|
7
10
|
this.data = data;
|
|
8
11
|
this.offset = offset;
|
|
9
12
|
}
|
|
13
|
+
get remaining() {
|
|
14
|
+
return this.data.length - this.offset;
|
|
15
|
+
}
|
|
10
16
|
readInt32() {
|
|
17
|
+
if (this.remaining < 4) {
|
|
18
|
+
throw new VorbisCommentError('Truncated Vorbis comment length');
|
|
19
|
+
}
|
|
11
20
|
const value = Token.UINT32_LE.get(this.data, this.offset);
|
|
12
21
|
this.offset += 4;
|
|
13
22
|
return value;
|
|
14
23
|
}
|
|
15
24
|
readStringUtf8() {
|
|
16
25
|
const len = this.readInt32();
|
|
26
|
+
if (len > this.remaining) {
|
|
27
|
+
throw new VorbisCommentError(`Vorbis comment length ${len} exceeds remaining data ${this.remaining}`);
|
|
28
|
+
}
|
|
17
29
|
const value = textDecode(this.data.subarray(this.offset, this.offset + len), 'utf-8');
|
|
18
30
|
this.offset += len;
|
|
19
31
|
return value;
|
|
@@ -23,6 +23,7 @@ export declare class VorbisContentError extends VorbisContentError_base {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare class VorbisStream implements IPageConsumer {
|
|
25
25
|
private pageSegments;
|
|
26
|
+
private bufferedPageLength;
|
|
26
27
|
protected metadata: INativeMetadataCollector;
|
|
27
28
|
protected options: IOptions;
|
|
28
29
|
protected lastPageHeader?: IPageHeader;
|
|
@@ -35,7 +36,7 @@ export declare class VorbisStream implements IPageConsumer {
|
|
|
35
36
|
* @param pageData Page data
|
|
36
37
|
*/
|
|
37
38
|
parsePage(header: IPageHeader, pageData: Uint8Array): Promise<void>;
|
|
38
|
-
private
|
|
39
|
+
private mergeUint8Arrays;
|
|
39
40
|
flush(): Promise<void>;
|
|
40
41
|
parseUserComment(pageData: Uint8Array, offset: number): Promise<number>;
|
|
41
42
|
addTag(id: string, value: string | IVorbisPicture): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import debugInit from 'debug';
|
|
2
2
|
import * as Token from 'token-types';
|
|
3
|
+
import { validateAllocation } from '../../common/Allocation.js';
|
|
3
4
|
import { makeUnexpectedFileContentError } from '../../ParseError.js';
|
|
4
5
|
import { CommonHeader, IdentificationHeader, VorbisPictureToken } from './Vorbis.js';
|
|
5
6
|
import { VorbisDecoder } from './VorbisDecoder.js';
|
|
@@ -12,6 +13,7 @@ export class VorbisContentError extends makeUnexpectedFileContentError('Vorbis')
|
|
|
12
13
|
*/
|
|
13
14
|
export class VorbisStream {
|
|
14
15
|
pageSegments = [];
|
|
16
|
+
bufferedPageLength = 0;
|
|
15
17
|
metadata;
|
|
16
18
|
options;
|
|
17
19
|
lastPageHeader;
|
|
@@ -36,30 +38,36 @@ export class VorbisStream {
|
|
|
36
38
|
if (this.pageSegments.length === 0) {
|
|
37
39
|
throw new VorbisContentError('Cannot continue on previous page');
|
|
38
40
|
}
|
|
41
|
+
validateAllocation(this.bufferedPageLength + pageData.length, this.options);
|
|
39
42
|
this.pageSegments.push(pageData);
|
|
43
|
+
this.bufferedPageLength += pageData.length;
|
|
40
44
|
}
|
|
41
45
|
if (header.headerType.lastPage || !header.headerType.continued) {
|
|
42
46
|
// Flush page segments
|
|
43
47
|
if (this.pageSegments.length > 0) {
|
|
44
|
-
const fullPage =
|
|
48
|
+
const fullPage = this.mergeUint8Arrays(this.pageSegments);
|
|
45
49
|
await this.parseFullPage(fullPage);
|
|
46
50
|
}
|
|
47
51
|
// Reset page segments
|
|
52
|
+
this.bufferedPageLength = header.headerType.lastPage ? 0 : pageData.length;
|
|
53
|
+
validateAllocation(this.bufferedPageLength, this.options);
|
|
48
54
|
this.pageSegments = header.headerType.lastPage ? [] : [pageData];
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
|
-
|
|
58
|
+
mergeUint8Arrays(arrays) {
|
|
53
59
|
const totalSize = arrays.reduce((acc, e) => acc + e.length, 0);
|
|
60
|
+
validateAllocation(totalSize, this.options);
|
|
54
61
|
const merged = new Uint8Array(totalSize);
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const array of arrays) {
|
|
57
64
|
merged.set(array, offset);
|
|
58
|
-
|
|
65
|
+
offset += array.length;
|
|
66
|
+
}
|
|
59
67
|
return merged;
|
|
60
68
|
}
|
|
61
69
|
async flush() {
|
|
62
|
-
await this.parseFullPage(
|
|
70
|
+
await this.parseFullPage(this.mergeUint8Arrays(this.pageSegments));
|
|
63
71
|
}
|
|
64
72
|
async parseUserComment(pageData, offset) {
|
|
65
73
|
const decoder = new VorbisDecoder(pageData, offset);
|
package/lib/type.d.ts
CHANGED
|
@@ -596,6 +596,12 @@ export interface IAudioMetadata extends INativeAudioMetadata {
|
|
|
596
596
|
*/
|
|
597
597
|
export type ParserType = 'mpeg' | 'apev2' | 'mp4' | 'asf' | 'flac' | 'ogg' | 'aiff' | 'wavpack' | 'riff' | 'musepack' | 'dsf' | 'dsdiff' | 'adts' | 'matroska';
|
|
598
598
|
export interface IOptions {
|
|
599
|
+
/**
|
|
600
|
+
* Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
|
|
601
|
+
* Defaults to 64 MiB (67108864 bytes). Must be a positive safe integer.
|
|
602
|
+
* This is not a limit on total process memory or the size of skipped audio/video data.
|
|
603
|
+
*/
|
|
604
|
+
maxMemoryAllocation?: number;
|
|
599
605
|
/**
|
|
600
606
|
* default: `false`, if set to `true`, it will parse the whole media file if required to determine the duration.
|
|
601
607
|
*/
|
package/lib/wav/WaveParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import * as strtok3 from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { getTokenizerOptions } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { FourCcToken } from '../common/FourCC.js';
|
|
6
7
|
import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
|
|
@@ -29,7 +30,7 @@ export class WaveParser extends BasicParser {
|
|
|
29
30
|
avgBytesPerSec = 0;
|
|
30
31
|
header;
|
|
31
32
|
async parse() {
|
|
32
|
-
const riffHeader = await this.
|
|
33
|
+
const riffHeader = await this.readToken(riff.Header);
|
|
33
34
|
debug(`pos=${this.tokenizer.position}, parse: chunkID=${riffHeader.chunkID}`);
|
|
34
35
|
if (riffHeader.chunkID !== 'RIFF') {
|
|
35
36
|
return; // Not RIFF format
|
|
@@ -43,7 +44,7 @@ export class WaveParser extends BasicParser {
|
|
|
43
44
|
await this.flushInfoTags();
|
|
44
45
|
}
|
|
45
46
|
async parseRiffChunk(chunkSize) {
|
|
46
|
-
const type = await this.
|
|
47
|
+
const type = await this.readToken(FourCcToken);
|
|
47
48
|
this.metadata.setFormat('container', type);
|
|
48
49
|
switch (type) {
|
|
49
50
|
case 'WAVE':
|
|
@@ -54,7 +55,7 @@ export class WaveParser extends BasicParser {
|
|
|
54
55
|
}
|
|
55
56
|
async readWaveChunk(remaining) {
|
|
56
57
|
while (remaining >= riff.Header.len) {
|
|
57
|
-
const header = await this.
|
|
58
|
+
const header = await this.readToken(riff.Header);
|
|
58
59
|
remaining -= riff.Header.len + header.chunkSize;
|
|
59
60
|
if (header.chunkSize > remaining) {
|
|
60
61
|
this.metadata.addWarning('Data chunk size exceeds file size');
|
|
@@ -66,7 +67,7 @@ export class WaveParser extends BasicParser {
|
|
|
66
67
|
if (header.chunkSize < 8) {
|
|
67
68
|
throw new WaveContentError('CSET chunk must contain at least 8 bytes');
|
|
68
69
|
}
|
|
69
|
-
const cset = await this.
|
|
70
|
+
const cset = await this.readToken(new Token.Uint8ArrayType(header.chunkSize));
|
|
70
71
|
this.codePage = Token.UINT16_LE.get(cset, 0);
|
|
71
72
|
if (!this.getInfoEncoding()) {
|
|
72
73
|
this.metadata.addWarning(`Unsupported RIFF CSET code page: ${this.codePage}; LIST/INFO tags will be omitted`);
|
|
@@ -79,11 +80,11 @@ export class WaveParser extends BasicParser {
|
|
|
79
80
|
break;
|
|
80
81
|
case 'fact': // extended Format chunk,
|
|
81
82
|
this.metadata.setFormat('lossless', false);
|
|
82
|
-
this.fact = await this.
|
|
83
|
+
this.fact = await this.readToken(new WaveChunk.FactChunk(header));
|
|
83
84
|
break;
|
|
84
85
|
case 'fmt ': {
|
|
85
86
|
// The Util Chunk, non-PCM Formats
|
|
86
|
-
const fmt = await this.
|
|
87
|
+
const fmt = await this.readToken(new WaveChunk.Format(header));
|
|
87
88
|
let subFormat = WaveChunk.WaveFormatNameMap[fmt.wFormatTag];
|
|
88
89
|
if (!subFormat) {
|
|
89
90
|
debug(`WAVE/non-PCM format=${fmt.wFormatTag}`);
|
|
@@ -100,8 +101,8 @@ export class WaveParser extends BasicParser {
|
|
|
100
101
|
case 'id3 ': // The way Picard, FooBar currently stores, ID3 meta-data
|
|
101
102
|
case 'ID3 ': {
|
|
102
103
|
// The way Mp3Tags stores ID3 meta-data
|
|
103
|
-
const id3_data = await this.
|
|
104
|
-
const rst = strtok3.fromBuffer(id3_data);
|
|
104
|
+
const id3_data = await this.readToken(new Token.Uint8ArrayType(header.chunkSize));
|
|
105
|
+
const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
|
|
105
106
|
await new ID3v2Parser().parse(this.metadata, rst, this.options);
|
|
106
107
|
break;
|
|
107
108
|
}
|
|
@@ -140,7 +141,7 @@ export class WaveParser extends BasicParser {
|
|
|
140
141
|
}
|
|
141
142
|
case 'bext': {
|
|
142
143
|
// Broadcast Audio Extension chunk https://tech.ebu.ch/docs/tech/tech3285.pdf
|
|
143
|
-
const bext = await this.
|
|
144
|
+
const bext = await this.readToken(BroadcastAudioExtensionChunk);
|
|
144
145
|
for (const key of Object.keys(bext)) {
|
|
145
146
|
await this.metadata.addTag('exif', `bext.${key}`, bext[key]);
|
|
146
147
|
}
|
|
@@ -165,7 +166,7 @@ export class WaveParser extends BasicParser {
|
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
async parseListTag(listHeader) {
|
|
168
|
-
const listType = await this.
|
|
169
|
+
const listType = await this.readToken(new Token.StringType(4, 'latin1'));
|
|
169
170
|
debug('pos=%s, parseListTag: chunkID=RIFF/WAVE/LIST/%s', this.tokenizer.position, listType);
|
|
170
171
|
switch (listType) {
|
|
171
172
|
case 'INFO':
|
|
@@ -178,9 +179,9 @@ export class WaveParser extends BasicParser {
|
|
|
178
179
|
}
|
|
179
180
|
async parseRiffInfoTags(chunkSize) {
|
|
180
181
|
while (chunkSize >= 8) {
|
|
181
|
-
const header = await this.
|
|
182
|
+
const header = await this.readToken(riff.Header);
|
|
182
183
|
const valueToken = new riff.ListInfoTagValue(header);
|
|
183
|
-
const bytes = await this.
|
|
184
|
+
const bytes = await this.readToken(new Token.Uint8ArrayType(valueToken.len));
|
|
184
185
|
if (this.codePage === undefined) {
|
|
185
186
|
this.metadata.registerTagType('exif');
|
|
186
187
|
this.pendingInfoTags.push({ header, bytes });
|
|
@@ -70,8 +70,11 @@ export class WavPackParser extends BasicParser {
|
|
|
70
70
|
while (remaining > MetadataIdToken.len) {
|
|
71
71
|
const id = await this.tokenizer.readToken(MetadataIdToken);
|
|
72
72
|
const dataSizeInWords = await this.tokenizer.readNumber(id.largeBlock ? Token.UINT24_LE : Token.UINT8);
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const headerSize = MetadataIdToken.len + (id.largeBlock ? Token.UINT24_LE.len : Token.UINT8.len);
|
|
74
|
+
if (dataSizeInWords * 2 > remaining - headerSize) {
|
|
75
|
+
throw new WavPackContentError('Metadata sub-block exceeds its enclosing block');
|
|
76
|
+
}
|
|
77
|
+
const data = await this.readToken(new Token.Uint8ArrayType(dataSizeInWords * 2 - (id.isOddSize ? 1 : 0)));
|
|
75
78
|
debug(`Metadata Sub-Blocks functionId=0x${id.functionId.toString(16)}, id.largeBlock=${id.largeBlock},data-size=${data.length}`);
|
|
76
79
|
switch (id.functionId) {
|
|
77
80
|
case 0x0: // ID_DUMMY: could be used to pad WavPack blocks
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "music-metadata",
|
|
3
3
|
"description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
|
|
4
|
-
"version": "12.1.
|
|
4
|
+
"version": "12.1.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Borewit",
|
|
7
7
|
"url": "https://github.com/Borewit"
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"debug": "^4.4.3",
|
|
125
125
|
"file-type": "^22.1.1",
|
|
126
126
|
"media-typer": "^2.0.0",
|
|
127
|
-
"strtok3": "^10.
|
|
127
|
+
"strtok3": "^10.4.0",
|
|
128
128
|
"token-types": "^6.1.2",
|
|
129
129
|
"uint8array-extras": "^1.6.0",
|
|
130
130
|
"win-guid": "^0.2.1"
|