1
0
mirror of synced 2026-05-22 14:43:19 +00:00

Move header parsing to FrameHeader.ts file, to allow re-using

Replace passing offset, and utilize `Uint8Array.subarray` instead.
This commit is contained in:
Borewit
2026-01-05 16:23:37 +01:00
committed by Borewit
parent 8caef3641a
commit 65ec1c8fbd
4 changed files with 121 additions and 75 deletions
+2 -1
View File
@@ -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');
+107
View File
@@ -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}`);
}
+8 -9
View File
@@ -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'){
+4 -65
View File
@@ -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) {