Move header parsing to FrameHeader.ts file, to allow re-using
Replace passing offset, and utilize `Uint8Array.subarray` instead.
This commit is contained in:
+2
-1
@@ -64,7 +64,8 @@ export function decodeString(uint8Array: Uint8Array, encoding: StringEncoding):
|
||||
// https://github.com/leetreveil/musicmetadata/issues/84
|
||||
if (uint8Array[0] === 0xFF && uint8Array[1] === 0xFE) { // little endian
|
||||
return decodeString(uint8Array.subarray(2), encoding);
|
||||
}if (encoding === 'utf-16le' && uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) {
|
||||
}
|
||||
if (encoding === 'utf-16le' && uint8Array[0] === 0xFE && uint8Array[1] === 0xFF) {
|
||||
// BOM, indicating big endian decoding
|
||||
if ((uint8Array.length & 1) !== 0)
|
||||
throw new FieldDecodingError('Expected even number of octets for 16-bit unicode string');
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// lib/id3v2/FrameHeader.ts
|
||||
import * as Token from 'token-types';
|
||||
import * as util from '../common/Util.js';
|
||||
import { UINT32SYNCSAFE, type ID3v2MajorVersion } from './ID3v2Token.js';
|
||||
import type { IWarningCollector } from '../common/MetadataCollector.js';
|
||||
import { textDecode } from '@borewit/text-codec';
|
||||
import { Id3v2ContentError } from './FrameParser.js';
|
||||
|
||||
export interface IFrameFlags {
|
||||
status: {
|
||||
tag_alter_preservation: boolean;
|
||||
file_alter_preservation: boolean;
|
||||
read_only: boolean;
|
||||
};
|
||||
format: {
|
||||
grouping_identity: boolean;
|
||||
compression: boolean;
|
||||
encryption: boolean;
|
||||
unsynchronisation: boolean;
|
||||
data_length_indicator: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IFrameHeader {
|
||||
id: string;
|
||||
length: number;
|
||||
flags?: IFrameFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame header length (bytes) depending on ID3v2 major version.
|
||||
*/
|
||||
export function getFrameHeaderLength(majorVer: number): 6 | 10 {
|
||||
switch (majorVer) {
|
||||
case 2: return 6;
|
||||
case 3:
|
||||
case 4: return 10;
|
||||
default: throw makeUnexpectedMajorVersionError(majorVer);
|
||||
}
|
||||
}
|
||||
|
||||
function readFrameFlags(b: Uint8Array): IFrameFlags {
|
||||
return {
|
||||
status: {
|
||||
tag_alter_preservation: util.getBit(b, 0, 6),
|
||||
file_alter_preservation: util.getBit(b, 0, 5),
|
||||
read_only: util.getBit(b, 0, 4)
|
||||
},
|
||||
format: {
|
||||
grouping_identity: util.getBit(b, 1, 7),
|
||||
compression: util.getBit(b, 1, 3),
|
||||
encryption: util.getBit(b, 1, 2),
|
||||
unsynchronisation: util.getBit(b, 1, 1),
|
||||
data_length_indicator: util.getBit(b, 1, 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: parse a frame header from its header bytes (6 for v2.2, 10 for v2.3/v2.4).
|
||||
*
|
||||
* Note: It only *parses* and does light validation. It does not read payload bytes.
|
||||
*/
|
||||
export function readFrameHeader(uint8Array: Uint8Array, majorVer: ID3v2MajorVersion, warningCollector: IWarningCollector): IFrameHeader {
|
||||
switch (majorVer) {
|
||||
case 2:
|
||||
return parseFrameHeaderV22(uint8Array, majorVer, warningCollector);
|
||||
|
||||
case 3:
|
||||
case 4:
|
||||
return parseFrameHeaderV23V24(uint8Array, majorVer, warningCollector);
|
||||
|
||||
default:
|
||||
throw makeUnexpectedMajorVersionError(majorVer);
|
||||
}
|
||||
}
|
||||
|
||||
function parseFrameHeaderV22(uint8Array: Uint8Array, majorVer: 2, warningCollector: IWarningCollector): IFrameHeader {
|
||||
const header: IFrameHeader = {
|
||||
id: textDecode(uint8Array.subarray(0, 3), 'ascii'),
|
||||
length: Token.UINT24_BE.get(uint8Array, 3)
|
||||
};
|
||||
|
||||
if (!header.id.match(/^[A-Z0-9]{3}$/)) {
|
||||
warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
function parseFrameHeaderV23V24(uint8Array: Uint8Array, majorVer: 3 | 4, warningCollector: IWarningCollector): IFrameHeader {
|
||||
const header: IFrameHeader = {
|
||||
id: textDecode(uint8Array.subarray(0, 4), 'ascii'),
|
||||
length: (majorVer === 4 ? UINT32SYNCSAFE : Token.UINT32_BE).get(uint8Array, 4),
|
||||
flags: readFrameFlags(uint8Array.subarray(8, 10))
|
||||
};
|
||||
|
||||
if (!header.id.match(/^[A-Z0-9]{4}$/)) {
|
||||
warningCollector.addWarning(`Invalid ID3v2.${majorVer} frame-header-ID: ${header.id}`);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
function makeUnexpectedMajorVersionError(majorVer: number): never {
|
||||
throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`);
|
||||
}
|
||||
@@ -182,7 +182,7 @@ export class FrameParser {
|
||||
}
|
||||
|
||||
case 'TXXX': {
|
||||
const idAndData = FrameParser.readIdentifierAndData(uint8Array, offset + 1, length, encoding);
|
||||
const idAndData = FrameParser.readIdentifierAndData(uint8Array.subarray(1), encoding);
|
||||
const textTag = {
|
||||
description: idAndData.id,
|
||||
text: this.splitValue(type, util.decodeString(idAndData.data, encoding).replace(/\x00+$/, ''))
|
||||
@@ -291,13 +291,13 @@ export class FrameParser {
|
||||
}
|
||||
|
||||
case 'UFID': {
|
||||
const ufid = FrameParser.readIdentifierAndData(uint8Array, offset, length, defaultEnc);
|
||||
const ufid = FrameParser.readIdentifierAndData(uint8Array.subarray(offset), defaultEnc);
|
||||
output = {owner_identifier: ufid.id, identifier: ufid.data} as IIdentifierTag;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'PRIV': { // private frame
|
||||
const priv = FrameParser.readIdentifierAndData(uint8Array, offset, length, defaultEnc);
|
||||
const priv = FrameParser.readIdentifierAndData(uint8Array.subarray(offset), defaultEnc);
|
||||
output = {owner_identifier: priv.id, data: priv.data} as ICustomDataTag;
|
||||
break;
|
||||
}
|
||||
@@ -446,19 +446,18 @@ export class FrameParser {
|
||||
return values.map(value => value.replace(/\x00+$/, '').trim());
|
||||
}
|
||||
|
||||
private static readIdentifierAndData(uint8Array: Uint8Array, offset: number, length: number, encoding: util.StringEncoding): { id: string, data: Uint8Array } {
|
||||
const fzero = util.findZero(uint8Array, offset, length, encoding);
|
||||
private static readIdentifierAndData(uint8Array: Uint8Array, encoding: util.StringEncoding): { id: string, data: Uint8Array } {
|
||||
const fzero = util.findZero(uint8Array, 0, uint8Array.length, encoding);
|
||||
|
||||
const id = util.decodeString(uint8Array.subarray(offset, fzero), encoding);
|
||||
offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
||||
const id = util.decodeString(uint8Array.subarray(0, fzero), encoding);
|
||||
const offset = fzero + FrameParser.getNullTerminatorLength(encoding);
|
||||
|
||||
return {id, data: uint8Array.subarray(offset, length)};
|
||||
return {id, data: uint8Array.subarray(offset)};
|
||||
}
|
||||
|
||||
private static getNullTerminatorLength(enc: util.StringEncoding): number {
|
||||
return enc === 'utf-16le' ? 2 : 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Id3v2ContentError extends makeUnexpectedFileContentError('id3v2'){
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { ITokenizer } from 'strtok3';
|
||||
import * as Token from 'token-types';
|
||||
|
||||
import * as util from '../common/Util.js';
|
||||
import type { TagType } from '../common/GenericTagTypes.js';
|
||||
import { FrameParser, Id3v2ContentError, type ITextTag } from './FrameParser.js';
|
||||
import { ExtendedHeader, ID3v2Header, type ID3v2MajorVersion, type IID3v2header, UINT32SYNCSAFE } from './ID3v2Token.js';
|
||||
import { ExtendedHeader, ID3v2Header, type ID3v2MajorVersion, type IID3v2header } from './ID3v2Token.js';
|
||||
|
||||
import type { ITag, IOptions, AnyTagValue } from '../type.js';
|
||||
import type { INativeMetadataCollector, IWarningCollector } from '../common/MetadataCollector.js';
|
||||
|
||||
import { textDecode } from '@borewit/text-codec';
|
||||
import { getFrameHeaderLength, readFrameHeader } from './FrameHeader.js';
|
||||
|
||||
interface IFrameFlags {
|
||||
status: {
|
||||
@@ -50,35 +49,6 @@ export class ID3v2Parser {
|
||||
return buffer.subarray(0, writeI);
|
||||
}
|
||||
|
||||
private static getFrameHeaderLength(majorVer: number): number {
|
||||
switch (majorVer) {
|
||||
case 2:
|
||||
return 6;
|
||||
case 3:
|
||||
case 4:
|
||||
return 10;
|
||||
default:
|
||||
throw makeUnexpectedMajorVersionError(majorVer);
|
||||
}
|
||||
}
|
||||
|
||||
private static readFrameFlags(b: Uint8Array): IFrameFlags {
|
||||
return {
|
||||
status: {
|
||||
tag_alter_preservation: util.getBit(b, 0, 6),
|
||||
file_alter_preservation: util.getBit(b, 0, 5),
|
||||
read_only: util.getBit(b, 0, 4)
|
||||
},
|
||||
format: {
|
||||
grouping_identity: util.getBit(b, 1, 7),
|
||||
compression: util.getBit(b, 1, 3),
|
||||
encryption: util.getBit(b, 1, 2),
|
||||
unsynchronisation: util.getBit(b, 1, 1),
|
||||
data_length_indicator: util.getBit(b, 1, 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static readFrameData(uint8Array: Uint8Array, frameHeader: IFrameHeader, majorVer: ID3v2MajorVersion, includeCovers: boolean, warningCollector: IWarningCollector) {
|
||||
const frameParser = new FrameParser(majorVer, warningCollector);
|
||||
switch (majorVer) {
|
||||
@@ -177,7 +147,7 @@ export class ID3v2Parser {
|
||||
while (true) {
|
||||
if (offset === data.length) break;
|
||||
|
||||
const frameHeaderLength = ID3v2Parser.getFrameHeaderLength(this.id3Header.version.major);
|
||||
const frameHeaderLength = getFrameHeaderLength(this.id3Header.version.major);
|
||||
|
||||
if (offset + frameHeaderLength > data.length) {
|
||||
this.metadata.addWarning('Illegal ID3v2 tag length');
|
||||
@@ -186,7 +156,7 @@ export class ID3v2Parser {
|
||||
|
||||
const frameHeaderBytes = data.subarray(offset, offset + frameHeaderLength);
|
||||
offset += frameHeaderLength;
|
||||
const frameHeader = this.readFrameHeader(frameHeaderBytes, this.id3Header.version.major);
|
||||
const frameHeader = readFrameHeader(frameHeaderBytes, this.id3Header.version.major, this.metadata);
|
||||
|
||||
const frameDataBytes = data.subarray(offset, offset + frameHeader.length);
|
||||
offset += frameHeader.length;
|
||||
@@ -198,37 +168,6 @@ export class ID3v2Parser {
|
||||
return tags;
|
||||
}
|
||||
|
||||
private readFrameHeader(uint8Array: Uint8Array, majorVer: number): IFrameHeader {
|
||||
let header: IFrameHeader;
|
||||
switch (majorVer) {
|
||||
|
||||
case 2:
|
||||
header = {
|
||||
id: textDecode(uint8Array.subarray(0, 3), 'ascii'),
|
||||
length: Token.UINT24_BE.get(uint8Array, 3)
|
||||
};
|
||||
if (!header.id.match(/[A-Z0-9]{3}/g)) {
|
||||
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
case 4:
|
||||
header = {
|
||||
id: textDecode(uint8Array.subarray(0, 4), 'ascii'),
|
||||
length: (majorVer === 4 ? UINT32SYNCSAFE : Token.UINT32_BE).get(uint8Array, 4),
|
||||
flags: ID3v2Parser.readFrameFlags(uint8Array.subarray(8, 10))
|
||||
};
|
||||
if (!header.id.match(/[A-Z0-9]{4}/g)) {
|
||||
this.metadata.addWarning(`Invalid ID3v2.${this.id3Header.version.major} frame-header-ID: ${header.id}`);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw makeUnexpectedMajorVersionError(majorVer);
|
||||
}
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
function makeUnexpectedMajorVersionError(majorVer: number) {
|
||||
|
||||
Reference in New Issue
Block a user