• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benrr101 / node-taglib-sharp / 54280018

25 Jun 2026 06:08PM UTC coverage: 92.799% (+0.2%) from 92.616%
54280018

Pull #139

appveyor

benrr101
Last round of :robot: comments
Pull Request #139: [ID3v2] Split Frame Factory

3280 of 4139 branches covered (79.25%)

Branch coverage included in aggregate %.

1997 of 2019 new or added lines in 49 files covered. (98.91%)

13 existing lines in 5 files now uncovered.

27533 of 29065 relevant lines covered (94.73%)

467.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.74
/src/id3v2/frames/frameHeader.ts
1
import SyncData from "../syncData";
1✔
2
import {ByteVector, StringType} from "../../byteVector";
1✔
3
import {CorruptFileError, NotImplementedError} from "../../errors";
1✔
4
import {FrameIdentifier, FrameIdentifiers} from "../frameIdentifiers";
1✔
5
import {Guards, NumberUtils} from "../../utils";
1✔
6

7
/**
8
 * Indicates the flags applied to a {@link Id3v2FrameHeader} object.
9
 */
10
export enum Id3v2FrameFlags {
1✔
11
    /**
12
     * Header contains no flags.
13
     */
14
    None = 0,
1✔
15

16
    /**
17
     * Frame is to be deleted if the tag is altered.
18
     */
19
    TagAlterPreservation = 0x4000,
1✔
20

21
    /**
22
     * Frame is to be deleted if the file is altered.
23
     */
24
    FileAlterPreservation = 0x2000,
1✔
25

26
    /**
27
     * Frame is read-only and should not be altered.
28
     */
29
    ReadOnly = 0x1000,
1✔
30

31
    /**
32
     * Frame has a grouping identity.
33
     */
34
    GroupingIdentity = 0x0040,
1✔
35

36
    /**
37
     * Frame data is compressed.
38
     */
39
    Compression = 0x0008,
1✔
40

41
    /**
42
     * Frame data is encrypted.
43
     */
44
    Encryption = 0x0004,
1✔
45

46
    /**
47
     * Frame data has been unsynchronized using the ID3v2 unsynchronization scheme.
48
     */
49
    Unsynchronized = 0x0002,
1✔
50

51
    /**
52
     * Frame has a data length indicator.
53
     */
54
    DataLengthIndicator = 0x0001
1✔
55
}
56

57
/**
58
 * This class provides a representation of an ID3v2 frame header which can be read from and
59
 * written to disk.
60
 * @remarks
61
 *     ID3v2.3 and ID3v2.4 support optional fields for grouping, encryption, and compression. This
62
 *     class only represents the basic header that all frames will contain. For this library, these
63
 *     optional fields are considered part of the frame's body. However, care must be taken that
64
 *     when reading the frame, these optional fields are processed as well. Use {@link flags} to
65
 *     determine if the frame contains these optional fields.
66
 */
67
export class Id3v2FrameHeader {
1✔
68
    private readonly _frameId: FrameIdentifier;
69

70
    private _dataLength: number;
71
    private _encryptionId: number;
72
    private _flags: Id3v2FrameFlags;
73
    private _frameSize: number;
74
    private _groupId: number;
75

76
    /**
77
     * Constructs and initializes a new instance by processing the data for the frame header.
78
     * @param id Identifier of the frame
79
     * @param flags Flags to assign to the frame (if omitted, defaults to
80
     *     {@link Id3v2FrameFlags.None})
81
     * @param frameSize Size of the frame in bytes, excluding the size of the header (if omitted,
82
     *     defaults to 0)
83
     */
84
    // @TODO: This shouldn't be public?
85
    public constructor(id: FrameIdentifier, flags: Id3v2FrameFlags = Id3v2FrameFlags.None, frameSize: number = 0) {
1,770✔
86
        Guards.truthy(id, "id");
1,525✔
87
        Guards.uint(frameSize, "frameSize");
1,523✔
88

89
        this._frameId = id;
1,523✔
90
        this._frameSize = frameSize;
1,523✔
91

92
        if (NumberUtils.hasFlag(flags, (Id3v2FrameFlags.Compression | Id3v2FrameFlags.Encryption))) {
1,523!
NEW
93
            throw new NotImplementedError("Argument invalid: Encryption and compression are not supported");
×
94
        }
95

96
        this._flags = flags;
1,523✔
97
    }
98

99
    /**
100
     * Constructs and initializes a new instance of {@link Id3v2FrameHeader} by reading it from raw
101
     * header data of a specified version.
102
     * @param data Raw data to build the new instance from.
103
     *     If the data size is smaller than the size of a full header, the data is just treated as
104
     *     a frame identifier and the remaining values are zeroed. @TODO: Why?? Why needs that functionality?
105
     * @param version ID3v2 version with which the data in `data` was encoded.
106
     */
107
    public static fromData(data: ByteVector, version: number): Id3v2FrameHeader {
108
        Guards.truthy(data, "data");
135✔
109
        Guards.byte(version, "version");
133✔
110
        Guards.betweenInclusive(version, 2, 4, "version");
128✔
111

112
        let rawFrameId: string;
113
        let frameId: FrameIdentifier;
114
        let flags = 0;
126✔
115
        let frameSize = 0;
126✔
116
        switch (version) {
126✔
117
            case 2:
118
                if (data.length < 3) {
5✔
119
                    throw new CorruptFileError("Data must contain at least a 3 byte frame identifier");
1✔
120
                }
121

122
                // Set frame ID -- first 3 bytes
123
                rawFrameId = data.subarray(0, 3).toString(StringType.Latin1);
4✔
124
                frameId = FrameIdentifiers[rawFrameId] || new FrameIdentifier(undefined, undefined, rawFrameId);
4✔
125

126
                // If the full header information was not passed in, do not continue to the steps
127
                // to parse the frame size and flags.
128
                if (data.length < 6) {
4✔
129
                    break;
2✔
130
                }
131

132
                frameSize = data.subarray(3, 3).toUint();
2✔
133
                break;
2✔
134

135
            case 3:
136
                if (data.length < 4) {
27✔
137
                    throw new CorruptFileError("Data must contain at least a 4 byte frame identifier");
1✔
138
                }
139

140
                // Set the frame ID -- first 4 bytes
141
                rawFrameId = data.subarray(0, 4).toString(StringType.Latin1);
26✔
142
                frameId = FrameIdentifiers[rawFrameId] || new FrameIdentifier(undefined, rawFrameId, undefined);
26✔
143

144
                // If the full header information was not passed in, do not continue to the steps
145
                // to parse the frame size and flags.
146
                if (data.length < 10) {
26✔
147
                    break;
2✔
148
                }
149

150
                // Store the flags internally as version 2.4
151
                frameSize = data.subarray(4, 4).toUint();
24✔
152
                flags = NumberUtils.uintOr(
24✔
153
                    NumberUtils.uintAnd(NumberUtils.uintLShift(data.get(8), 7), 0x7000),
154
                    NumberUtils.uintAnd(NumberUtils.uintRShift(data.get(9), 4), 0x000C),
155
                    NumberUtils.uintAnd(NumberUtils.uintLShift(data.get(9), 1), 0x0040)
156
                );
157
                break;
24✔
158

159
            case 4:
160
                if (data.length < 4) {
94✔
161
                    throw new CorruptFileError("Data must contain at least 4 byte frame identifier");
1✔
162
                }
163

164
                // Set the frame ID -- the first 4 bytes
165
                rawFrameId = data.subarray(0, 4).toString(StringType.Latin1);
93✔
166
                frameId = FrameIdentifiers[rawFrameId] || new FrameIdentifier(rawFrameId, undefined, undefined);
93✔
167

168
                // If the full header information was not passed in, do not continue to the steps to
169
                // ... eh, you probably get it by now.
170
                if (data.length < 10) {
93✔
171
                    break;
2✔
172
                }
173

174
                frameSize = SyncData.toUint(data.subarray(4, 4));
91✔
175
                flags = data.subarray(8, 2).toUshort();
91✔
176
                break;
91✔
177
        }
178

179
        return new Id3v2FrameHeader(frameId, flags, frameSize);
123✔
180
    }
181

182
    /**
183
     * Constructs and initializes a new, blank frame header of size 0, with the
184
     * provided frame identifier.
185
     * @param id Identifier for the frame
186
     */
187
    public static fromFrameIdentifier(id: FrameIdentifier): Id3v2FrameHeader {
188
        return new Id3v2FrameHeader(id, Id3v2FrameFlags.None, 0);
36✔
189
    }
190

191
    // #region Properties
192

193
    /**
194
     * Gets the length of the fields in the frame. This is only updated during rendering.
195
     * @internal
196
     */
197
    public get dataLength(): number|undefined { return this._dataLength; }
25✔
198
    /**
199
     * Sets the length of the fields in the frame (the payload, without the extended header bytes).
200
     * This is only intended to be updated during rendering.
201
     * @internal
202
     */
203
    public set dataLength(value: number|undefined) {
204
        Guards.safeUintOptional(value, "value");
402✔
205
        this._dataLength = value;
398✔
206
    }
207

208
    /**
209
     * Gets the encryption ID applied to the current instance.
210
     * @returns
211
     *     Value containing the encryption identifier for the current instance or
212
     *     `undefined` if not set.
213
     */
214
    public get encryptionId(): number|undefined {
215
        return NumberUtils.hasFlag(this.flags, Id3v2FrameFlags.Encryption)
22✔
216
            ? this._encryptionId
217
            : undefined;
218
    }
219
    /**
220
     * Sets the encryption ID applied to the current instance.
221
     * @param value Value containing the encryption identifier for the current instance. Must be an
222
     *     8-bit unsigned integer. Setting to `undefined` will remove the encryption header and ID
223
     */
224
    public set encryptionId(value: number|undefined) {
225
        Guards.byteOptional(value, "value");
6✔
226
        if (value !== undefined) {
3✔
227
            throw new NotImplementedError("Encryption and compression are not supported");
2✔
228
        } else {
229
            this._encryptionId = value;
1✔
230
            this._flags &= ~Id3v2FrameFlags.Encryption;
1✔
231
        }
232
    }
233

234
    /**
235
     * Gets the flags applied to the current instance.
236
     */
237
    public get flags(): Id3v2FrameFlags { return this._flags; }
963✔
238
    /**
239
     * @TODO: It should not be necessary to update the flags manually like this.
240
     * @internal
241
     */
242
    public set flags(value: Id3v2FrameFlags) { this._flags = value; }
215✔
243

244
    /**
245
     * Gets the identifier of the frame described by the current instance.
246
     */
247
    public get frameId(): FrameIdentifier { return this._frameId; }
1,556✔
248

249
    /**
250
     * Gets the size of the frame described by the current instance, minus the header.
251
     */
252
    public get frameSize(): number { return this._frameSize; }
461✔
253
    /**
254
     * Sets the size of the frame described by the current instance, minus the header.
255
     * Must be a positive, safe integer.
256
     */
257
    public set frameSize(value: number) {
258
        Guards.safeUint(value, "value");
475✔
259
        this._frameSize = value;
470✔
260
    }
261

262
    /**
263
     * Gets the grouping ID applied to the current instance.
264
     * @returns
265
     *     Value containing the grouping identifier for the current instance, or
266
     *     `undefined` if not set.
267
     */
268
    public get groupId(): number | undefined {
269
        return NumberUtils.hasFlag(this.flags, Id3v2FrameFlags.GroupingIdentity)
29✔
270
            ? this._groupId
271
            : undefined;
272
    }
273
    /**
274
     * Sets the grouping ID applied to the current instance.
275
     * @param value Grouping identifier for the current instance. Must be an 8-bit unsigned integer.
276
     *     Setting to `undefined` will remove the grouping identity header and ID
277
     */
278
    public set groupId(value: number | undefined) {
279
        Guards.byteOptional(value, "value");
13✔
280
        this._groupId = value;
7✔
281
        if (value !== undefined) {
7✔
282
            this._flags |= Id3v2FrameFlags.GroupingIdentity;
5✔
283
        } else {
284
            this._flags &= ~Id3v2FrameFlags.GroupingIdentity;
2✔
285
        }
286
    }
287

288
    public get isUnsynchronizationApplied(): boolean {
289
        return NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Unsynchronized);
113✔
290
    }
291

292
    // #endregion
293

294
    // #region Public Methods
295

296
    /**
297
     * Gets the size of a header for a specified ID3v2 version.
298
     * @param version Version of ID3v2 to get the size for. Must be a positive integer < 256
299
     */
300
    public static getBaseSize(version: number): number {
301
        Guards.byte(version, "version");
228✔
302
        return version < 3 ? 6 : 10;
228✔
303
    }
304

305
    /**
306
     * Reads any extended header fields from the frame's payload bytes. Fields to read are
307
     * determined by the flags initially read for the freame header.
308
     * @param payloadBytes Frame's payload from which the extended header bytes will be read. These
309
     *     bytes must be resynchronized if the frame was marked as unsynchronized.
310
     * @param version ID3v2 version. Must be a byte.
311
     * @returns number Number of bytes for the extended frame header fields.
312
     */
313
    public readExtendedHeaderFromPayloadBytes(payloadBytes: ByteVector, version: number): number {
314
        Guards.truthy(payloadBytes, "payloadBytes");
132✔
315
        Guards.byte(version, "version");
132✔
316

317
        let position = 0;
132✔
318
        switch (version) {
132!
319
            case 2:
320
                break;
2✔
321
            case 3:
322
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Compression)) {
31✔
323
                    this._dataLength = this.getFieldBytes(payloadBytes, position, 4).toUint();
5✔
324
                    position += 4;
5✔
325
                }
326
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Encryption)) {
31✔
327
                    this._encryptionId = this.getFieldBytes(payloadBytes, position, 1).get(0);
5✔
328
                    position++;
5✔
329
                }
330
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.GroupingIdentity)) {
31✔
331
                    this._groupId = this.getFieldBytes(payloadBytes, position, 1).get(0);
7✔
332
                    position++;
7✔
333
                }
334
                break;
31✔
335
            case 4:
336
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.GroupingIdentity)) {
99✔
337
                    this._groupId = this.getFieldBytes(payloadBytes, position, 1).get(0);
7✔
338
                    position++;
7✔
339
                }
340
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Encryption)) {
99✔
341
                    this._encryptionId = this.getFieldBytes(payloadBytes, position, 1).get(0);
5✔
342
                    position++;
5✔
343
                }
344
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.DataLengthIndicator)) {
99✔
345
                    this._dataLength = SyncData.toUint(this.getFieldBytes(payloadBytes, position, 4));
6✔
346
                    position += 4;
5✔
347
                }
348
                break;
98✔
349
            default:
NEW
350
                throw new Error("Argument error: version must be a valid ID3v2 version.");
×
351
        }
352

353
        return position;
131✔
354
    }
355

356
    public clone(identifier?: FrameIdentifier): Id3v2FrameHeader {
357
        const clone = new Id3v2FrameHeader(identifier ?? this.frameId);
14✔
358
        clone._dataLength = this._dataLength;
14✔
359
        clone._encryptionId = this._encryptionId;
14✔
360
        clone._frameSize = this._frameSize;
14✔
361
        clone._flags = this._flags;
14✔
362
        clone._groupId = this._groupId;
14✔
363

364
        return clone;
14✔
365
    }
366

367
    /**
368
     * Renders the current instance, encoded in a specified ID3v2 version.
369
     * @param version Version of ID3v2 to use when encoding the current instance.
370
     */
371
    public render(version: number): ByteVector {
372
        Guards.byte(version, "version");
574✔
373
        Guards.betweenInclusive(version, 2, 4, "version");
574✔
374

375
        // Start by rendering the frame identifier
376
        const byteVectors = [this._frameId.render(version)];
574✔
377

378
        switch (version) {
572✔
379
            case 2:
380
                byteVectors.push(ByteVector.fromUint(this._frameSize).subarray(1, 3));
96✔
381
                break;
96✔
382

383
            case 3:
384
                const newFlags = NumberUtils.uintOr(
134✔
385
                    NumberUtils.uintAnd(NumberUtils.uintLShift(this._flags, 1), 0xE000),
386
                    NumberUtils.uintAnd(NumberUtils.uintLShift(this._flags, 4), 0x00C0),
387
                    NumberUtils.uintAnd(NumberUtils.uintRShift(this._flags, 1), 0x0020)
388
                );
389

390
                byteVectors.push(ByteVector.fromUint(this._frameSize));
134✔
391
                byteVectors.push(ByteVector.fromUshort(newFlags));
134✔
392
                break;
134✔
393

394
            case 4:
395
                byteVectors.push(SyncData.fromUint(this._frameSize));
342✔
396
                byteVectors.push(ByteVector.fromUshort(this._flags));
342✔
397
                break;
342✔
398
        }
399

400
        return ByteVector.concatenate(... byteVectors);
572✔
401
    }
402

403
    public renderExtendedHeader(version: number): ByteVector {
404
        const fieldVectors = [];
394✔
405
        switch (version) {
394✔
406
            case 2:
407
                break;
48✔
408
            case 3:
409
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Compression)) {
78!
NEW
410
                    throw new NotImplementedError("Compression is not supported.");
×
411
                }
412
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Encryption)) {
78!
NEW
413
                    throw new NotImplementedError("Encryption is not supported");
×
414
                }
415
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.GroupingIdentity)) {
78!
NEW
416
                    fieldVectors.push(ByteVector.fromByte(this._groupId));
×
417
                }
418
                break;
78✔
419
            case 4:
420
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.GroupingIdentity)) {
268✔
421
                    fieldVectors.push(ByteVector.fromByte(this._groupId));
1✔
422
                }
423
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.Encryption)) {
268!
NEW
424
                    throw new NotImplementedError("Encryption is not supported");
×
425
                }
426
                if (NumberUtils.hasFlag(this._flags, Id3v2FrameFlags.DataLengthIndicator)) {
268✔
427
                    fieldVectors.push(SyncData.fromUint(this._dataLength));
2✔
428
                }
429
                break;
268✔
430
        }
431

432
        return ByteVector.concatenate(... fieldVectors);
394✔
433
    }
434

435
    // #endregion
436

437
    private getFieldBytes(payloadBytes: ByteVector, position: number, length: number): ByteVector {
438
        const fieldBytes = payloadBytes.subarray(position, length);
35✔
439
        if (fieldBytes.length < length) {
35✔
440
            throw new CorruptFileError(
1✔
441
                "ID3v2 frame extended header does not contain enough bytes for fields set by flags"
442
            );
443
        }
444

445
        return fieldBytes;
34✔
446
    }
447
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc