892986136d
- Prevent parser rewind by disallowing negative skip lengths in tokenizer - Validate ASF object sizes to ensure minimum header length - Add sanity limit (10k) for numberOfHeaderObjects to avoid excessive iteration
157 lines
6.4 KiB
TypeScript
157 lines
6.4 KiB
TypeScript
import initDebug from 'debug';
|
|
|
|
import { type ITag, TrackType } from '../type.js';
|
|
import AsfGuid from './AsfGuid.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';
|
|
|
|
/**
|
|
* Windows Media Metadata Usage Guidelines
|
|
* - Ref: https://msdn.microsoft.com/en-us/library/ms867702.aspx
|
|
*
|
|
* Ref:
|
|
* - https://tools.ietf.org/html/draft-fleischman-asf-01
|
|
* - https://hwiegman.home.xs4all.nl/fileformats/asf/ASF_Specification.pdf
|
|
* - http://drang.s4.xrea.com/program/tips/id3tag/wmp/index.html
|
|
* - https://msdn.microsoft.com/en-us/library/windows/desktop/ee663575(v=vs.85).aspx
|
|
*/
|
|
export class AsfParser extends BasicParser {
|
|
|
|
public async parse() {
|
|
const header = await this.tokenizer.readToken<AsfObject.IAsfTopLevelObjectHeader>(AsfObject.TopLevelHeaderObjectToken);
|
|
if (header.numberOfHeaderObjects > 10000) {
|
|
throw new AsfContentParseError(
|
|
`Unrealistic number of ASF header objects: ${header.numberOfHeaderObjects}`
|
|
);
|
|
}
|
|
await this.parseObjectHeader(header.numberOfHeaderObjects);
|
|
}
|
|
|
|
private async parseObjectHeader(numberOfObjectHeaders: number): Promise<void> {
|
|
let tags: ITag[];
|
|
do {
|
|
// Parse common header of the ASF Object (3.1)
|
|
const header = await this.tokenizer.readToken<AsfObject.IAsfObjectHeader>(AsfObject.HeaderObjectToken);
|
|
// Parse data part of the ASF Object
|
|
debug('header GUID=%s', header.objectId.str);
|
|
switch (header.objectId.str) {
|
|
|
|
case AsfObject.FilePropertiesObject.guid.str: { // 3.2
|
|
const fpo = await this.tokenizer.readToken<AsfObject.IFilePropertiesObject>(new AsfObject.FilePropertiesObject(header));
|
|
this.metadata.setFormat('duration', Number(fpo.playDuration / BigInt(1000)) / 10000 - Number(fpo.preroll) / 1000);
|
|
this.metadata.setFormat('bitrate', fpo.maximumBitrate);
|
|
break;
|
|
}
|
|
|
|
case AsfObject.StreamPropertiesObject.guid.str: { // 3.3
|
|
const spo = await this.tokenizer.readToken<AsfObject.IStreamPropertiesObject>(new AsfObject.StreamPropertiesObject(header));
|
|
this.metadata.setFormat('container', `ASF/${spo.streamType}`);
|
|
break;
|
|
}
|
|
|
|
case AsfObject.HeaderExtensionObject.guid.str: { // 3.4
|
|
const extHeader = await this.tokenizer.readToken<AsfObject.IHeaderExtensionObject>(new AsfObject.HeaderExtensionObject());
|
|
await this.parseExtensionObject(extHeader.extensionDataSize);
|
|
break;
|
|
}
|
|
|
|
case AsfObject.ContentDescriptionObjectState.guid.str: // 3.10
|
|
tags = await this.tokenizer.readToken<ITag[]>(new AsfObject.ContentDescriptionObjectState(header));
|
|
await this.addTags(tags);
|
|
break;
|
|
|
|
case AsfObject.ExtendedContentDescriptionObjectState.guid.str: // 3.11
|
|
tags = await this.tokenizer.readToken<ITag[]>(new AsfObject.ExtendedContentDescriptionObjectState(header));
|
|
await this.addTags(tags);
|
|
break;
|
|
|
|
case AsfGuid.CodecListObject.str: {
|
|
const codecs = await AsfObject.readCodecEntries(this.tokenizer);
|
|
codecs.forEach(codec => {
|
|
this.metadata.addStreamInfo({
|
|
type: codec.type.videoCodec ? TrackType.video : TrackType.audio,
|
|
codecName: codec.codecName
|
|
});
|
|
});
|
|
const audioCodecs = codecs.filter(codec => codec.type.audioCodec).map(codec => codec.codecName).join('/');
|
|
this.metadata.setFormat('codec', audioCodecs);
|
|
break;
|
|
}
|
|
|
|
case AsfGuid.StreamBitratePropertiesObject.str:
|
|
// ToDo?
|
|
await this.tokenizer.ignore(header.objectSize - AsfObject.HeaderObjectToken.len);
|
|
break;
|
|
|
|
case AsfGuid.PaddingObject.str:
|
|
// ToDo: register bytes pad
|
|
debug('Padding: %s bytes', header.objectSize - AsfObject.HeaderObjectToken.len);
|
|
await this.tokenizer.ignore(header.objectSize - AsfObject.HeaderObjectToken.len);
|
|
break;
|
|
|
|
default:
|
|
this.metadata.addWarning(`Ignore ASF-Object-GUID: ${header.objectId.str}`);
|
|
debug('Ignore ASF-Object-GUID: %s', header.objectId.str);
|
|
await this.tokenizer.readToken<void>(new AsfObject.IgnoreObjectState(header));
|
|
}
|
|
} while (--numberOfObjectHeaders);
|
|
// done
|
|
}
|
|
|
|
private async addTags(tags: ITag[]): Promise<void> {
|
|
await Promise.all(tags.map(({ id, value }) => this.metadata.addTag(headerType, id, value)));
|
|
}
|
|
|
|
private async parseExtensionObject(extensionSize: number): Promise<void> {
|
|
do {
|
|
// Parse common header of the ASF Object (3.1)
|
|
const header = await this.tokenizer.readToken<AsfObject.IAsfObjectHeader>(AsfObject.HeaderObjectToken);
|
|
const remaining = header.objectSize - AsfObject.HeaderObjectToken.len;
|
|
// Parse data part of the ASF Object
|
|
switch (header.objectId.str) {
|
|
|
|
case AsfObject.ExtendedStreamPropertiesObjectState.guid.str: // 4.1
|
|
// ToDo: extended stream header properties are ignored
|
|
await this.tokenizer.readToken<AsfObject.IExtendedStreamPropertiesObject>(new AsfObject.ExtendedStreamPropertiesObjectState(header));
|
|
break;
|
|
|
|
case AsfObject.MetadataObjectState.guid.str: { // 4.7
|
|
const moTags = await this.tokenizer.readToken<ITag[]>(new AsfObject.MetadataObjectState(header));
|
|
await this.addTags(moTags);
|
|
break;
|
|
}
|
|
|
|
case AsfObject.MetadataLibraryObjectState.guid.str: { // 4.8
|
|
const mlTags = await this.tokenizer.readToken<ITag[]>(new AsfObject.MetadataLibraryObjectState(header));
|
|
await this.addTags(mlTags);
|
|
break;
|
|
}
|
|
|
|
case AsfGuid.PaddingObject.str:
|
|
// ToDo: register bytes pad
|
|
await this.tokenizer.ignore(remaining);
|
|
break;
|
|
|
|
case AsfGuid.CompatibilityObject.str:
|
|
await this.tokenizer.ignore(remaining);
|
|
break;
|
|
|
|
case AsfGuid.ASF_Index_Placeholder_Object.str:
|
|
await this.tokenizer.ignore(remaining);
|
|
break;
|
|
|
|
default:
|
|
this.metadata.addWarning(`Ignore ASF-Object-GUID: ${header.objectId.str}`);
|
|
// console.log("Ignore ASF-Object-GUID: %s", header.objectId.str);
|
|
await this.tokenizer.readToken<void>(new AsfObject.IgnoreObjectState(header));
|
|
break;
|
|
}
|
|
extensionSize -= header.objectSize;
|
|
} while (extensionSize > 0);
|
|
}
|
|
}
|