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

benrr101 / node-taglib-sharp / 54030989

12 May 2026 07:33PM UTC coverage: 92.455% (-0.09%) from 92.547%
54030989

push

appveyor

web-flow
Remove `.fromRawData()` static constructors from ID3v2 frames (#133)

* Remove AttachmentFrame.fromRawData()

* Remove CommentsFrame.fromRawData()

* :robot: Remove MusicCdIdentifierFrame.fromRawData()

* :robot: Remove PlayCountFrame.fromRawData()

* :robot: Remove PopularimeterFrame.fromRawData()

* :robot: Remove TermsOfUseFrame.fromRawData()

* :robot: Remove UniqueFileIdentifierFrame.fromRawData()

* :robot: Remove PrivateFrame.fromRawData()

* :robot: Remove UnknownFrame.fromRawData()

* :robot: Remove EventTimeCodeFrame.fromRawData()

* :robot: Remove UnsynchronizedLyricsFrame.fromRawData()

* :robot: Remove SynchronizedLyricsFrame.fromRawData()

* :robot: Remove RelativeVolumeFrame.fromRawData()

* :robot: Remove TextInformationFrame.fromRawData()

* :robot: Remove UserTextInformationFrame.fromRawData()

* :robot: Remove UrlLinkFrame.fromRawData()

* :robot: Remove UserUrlLinkFrame.fromRawData()

* :robot: Padding *before* header in fromOffsetRawData tests

* Remove stubbed fromRawData and the corresponding tests in FrameConstructorTests

* Fix wrong header in TOS frame test

3270 of 4161 branches covered (78.59%)

Branch coverage included in aggregate %.

107 of 107 new or added lines in 18 files covered. (100.0%)

5 existing lines in 3 files now uncovered.

26569 of 28113 relevant lines covered (94.51%)

475.01 hits per line

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

87.56
/src/id3v2/frames/attachmentFrame.ts
1
import Id3v2Settings from "../id3v2Settings";
1✔
2
import {ByteVector, StringType} from "../../byteVector";
1✔
3
import {CorruptFileError} from "../../errors";
1✔
4
import {IFileAbstraction} from "../../fileAbstraction";
5
import {Frame, FrameClassType} from "./frame";
1✔
6
import {Id3v2FrameHeader} from "./frameHeader";
1✔
7
import {FrameIdentifiers} from "../frameIdentifiers";
1✔
8
import {IPicture, Picture, PictureLazy, PictureType} from "../../picture";
1✔
9
import {Guards} from "../../utils";
1✔
10

11
export default class AttachmentFrame extends Frame implements IPicture {
1✔
12
    // NOTE: It probably doesn't look necessary to implement IPicture, but it makes converting a
13
    //     frame to a picture so much easier, which we need in the Id3v2Tag class.
14

15
    private _data: ByteVector;
16
    private _description: string;
17
    private _encoding: StringType = Id3v2Settings.defaultEncoding;
45✔
18
    private _filename: string;
19
    private _mimeType: string;
20
    private _rawData: ByteVector;
21
    private _rawPicture: IPicture;
22
    private _rawVersion: number;
23
    private _type: PictureType;
24

25
    // #region Constructors
26

27
    private constructor(frameHeader: Id3v2FrameHeader) {
28
        super(frameHeader);
45✔
29
    }
30

31
    /**
32
     * Constructs and initializes a new attachment frame by populating it with the contents of a
33
     * section of a file. This constructor is only meant to be used internally. All loading is done
34
     * lazily.
35
     * @param file File to load frame data from
36
     * @param header ID3v2 frame header that defines the frame
37
     * @param frameStart Index into the file where the frame starts
38
     * @param size Length of the frame data
39
     * @param version ID3v2 version the frame was originally encoded with
40
     * @internal
41
     */
42
    // @TODO: Make lazy loading optional
43
    public static fromFile(
44
        file: IFileAbstraction,
45
        header: Id3v2FrameHeader,
46
        frameStart: number,
47
        size: number,
48
        version: number
49
    ): AttachmentFrame {
50
        Guards.truthy(file, "file");
2✔
51
        Guards.truthy(header, "header");
2✔
52
        Guards.safeUint(frameStart, "frameStart");
2✔
53
        Guards.safeUint(size, "size");
2✔
54

55
        const frame = new AttachmentFrame(header);
2✔
56
        frame._rawPicture = PictureLazy.fromFile(file, frameStart, size);
2✔
57
        frame._rawVersion = version;
2✔
58
        return frame;
2✔
59
    }
60

61
    /**
62
     * Constructs and initializes a new attachment frame by reading its raw data in a specified
63
     * ID3v2 version.
64
     * @param data ByteVector containing the raw representation of the new frame
65
     * @param offset Index into `data` where the frame actually begins
66
     * @param header Header of the frame found at `offset` in the data
67
     * @param version ID3v2 version the frame was originally encoded with
68
     */
69
    public static fromOffsetRawData(
70
        data: ByteVector,
71
        offset: number,
72
        header: Id3v2FrameHeader,
73
        version: number
74
    ): AttachmentFrame {
75
        Guards.truthy(data, "data");
18✔
76
        Guards.uint(offset, "offset");
16✔
77
        Guards.truthy(header, "header");
11✔
78
        Guards.byte(version, "version");
9✔
79

80
        const frame = new AttachmentFrame(header);
9✔
81
        frame.setData(data, offset, false, version);
9✔
82
        return frame;
9✔
83
    }
84

85
    /**
86
     * Constructs and initializes a new attachment frame by populating it with the contents of
87
     * another {@link IPicture} object.
88
     * @remarks
89
     *     When a frame is created, it is not automatically added to the tag.
90
     *     Additionally, see {@link Tag.pictures} provides a generic way of getting and setting
91
     *     attachments which is preferable to format specific code.
92
     * @param picture Value to use in the new instance.
93
     */
94
    public static fromPicture(picture: IPicture): AttachmentFrame {
95
        Guards.truthy(picture, "picture");
33✔
96

97
        // NOTE: We assume the frame is an APIC frame of size 1 until we parse it and find out
98
        //     otherwise.
99
        const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameIdentifiers.APIC, undefined, 1));
31✔
100
        frame._rawPicture = picture;
31✔
101
        return frame;
31✔
102
    }
103

104
    // #endregion
105

106
    // #region Properties
107

108
    /** @inheritDoc */
109
    public get frameClassType(): FrameClassType { return FrameClassType.AttachmentFrame; }
17✔
110

111
    /**
112
     * Gets the image data stored in the current instance.
113
     */
114
    public get data(): ByteVector {
115
        this.parseFromRaw();
18✔
116
        return this._data ? this._data : ByteVector.empty();
18✔
117
    }
118
    /**
119
     * Sets the image data stored in the current instance.
120
     */
121
    public set data(value: ByteVector) {
122
        this.parseFromRaw();
3✔
123
        this._data = value;
3✔
124
    }
125

126
    /**
127
     * Gets the description stored in the current instance.
128
     */
129
    public get description(): string {
130
        this.parseFromRaw();
26✔
131
        return this._description ? this._description : "";
26✔
132
    }
133
    /**
134
     * Sets the description stored in the current instance.
135
     * There should only be one frame with a matching description and type per tag.
136
     */
137
    public set description(value: string) {
138
        this.parseFromRaw();
11✔
139
        this._description = value;
11✔
140
    }
141

142
    /**
143
     * Gets a filename of the picture stored in the current instance.
144
     */
145
    public get filename(): string {
146
        this.parseFromRaw();
17✔
147
        return this._filename;
17✔
148
    }
149
    /**
150
     * Sets a filename of the picture stored in the current instance.
151
     */
152
    public set filename(value: string) {
153
        this.parseFromRaw();
3✔
154
        this._filename = value;
3✔
155
    }
156

157
    /**
158
     * Gets the MimeType of the picture stored in the current instance.
159
     */
160
    public get mimeType(): string {
161
        this.parseFromRaw();
20✔
162

163
        return this._mimeType ? this._mimeType : "";
20✔
164
    }
165
    /**
166
     * Sets the MimeType of the picture stored in the current instance.
167
     * @param value MimeType of the picture stored in the current instance.
168
     */
169
    public set mimeType(value: string) {
170
        this.parseFromRaw();
3✔
171
        this._mimeType = value;
3✔
172
    }
173

174
    /**
175
     * Gets the text encoding to use when storing the current instance.
176
     * @value Text encoding to use when storing the current instance.
177
     */
178
    public get textEncoding(): StringType {
179
        this.parseFromRaw();
22✔
180
        return this._encoding;
22✔
181
    }
182
    /**
183
     * Sets the text encoding to use when storing the current instance.
184
     * @param value Text encoding to use when storing the current instance.
185
     *     This encoding is overridden when rendering if
186
     *     {@link Id3v2Settings.forceDefaultEncoding} is `true` or the render version does not
187
     *     support it.
188
     */
189
    public set textEncoding(value: StringType) {
190
        this.parseFromRaw();
1✔
191
        this._encoding = value;
1✔
192
    }
193

194
    /**
195
     * Gets the object type stored in the current instance.
196
     */
197
    public get type(): PictureType {
198
        this.parseFromRaw();
22✔
199
        return this._type;
22✔
200
    }
201
    /**
202
     * Sets the object type stored in the current instance.
203
     * For General Object Frame, use {@link PictureType.NotAPicture}. Other types will make it a
204
     * Picture Frame.
205
     */
206
    public set type(value: PictureType) {
207
        this.parseFromRaw();
14✔
208

209
        // Change the frame type depending on whether this is a picture or a general object
210
        const frameId = value === PictureType.NotAPicture
14✔
211
            ? FrameIdentifiers.GEOB
212
            : FrameIdentifiers.APIC;
213
        if (this.header.frameId !== frameId) {
14✔
214
            this.header = new Id3v2FrameHeader(frameId);
3✔
215
        }
216

217
        this._type = value;
14✔
218
    }
219

220
    // #endregion
221

222
    // #region Public Methods
223

224
    /** @inheritDoc */
225
    public clone(): Frame {
226
        const frame = new AttachmentFrame(new Id3v2FrameHeader(this.frameId));
3✔
227
        frame._data = this._data?.toByteVector();
3✔
228
        frame._description = this._description;
3✔
229
        frame._encoding = this._encoding;
3✔
230
        frame._filename = this._filename;
3✔
231
        frame._mimeType = this._mimeType;
3✔
232
        frame._rawData = this._rawData;
3✔
233
        frame._rawPicture = this._rawPicture;
3✔
234
        frame._rawVersion = this._rawVersion;
3✔
235
        frame._type = this._type;
3✔
236

237
        return frame;
3✔
238
    }
239

240
    /**
241
     * Get a specified attachment frame from the specified tag, optionally creating it if it does
242
     * not exist.
243
     * @param frames List of attachment frames to search
244
     * @param description Description to match
245
     * @param type Picture type to match
246
     * @returns Matching frame or `undefined` if a match wasn't found and `create` is `false`
247
     */
248
    public static find(
249
        frames: AttachmentFrame[],
250
        description?: string,
251
        type: PictureType = PictureType.Other
5✔
252
    ): AttachmentFrame {
253
        Guards.truthy(frames, "frames");
8✔
254
        return frames.find((f) => {
6✔
255
                if (description && f.description !== description) { return false; }
9✔
256
                // noinspection RedundantIfStatementJS
257
                if (type !== PictureType.Other && f.type !== type) { return false; }
6✔
258
                return true;
3✔
259
            });
260
    }
261

262
    /**
263
     * Generates a string representation of the current instance.
264
     * @deprecated No need for this.
265
     */
266
    public toString(): string {
267
        this.parseFromRaw();
×
268

269
        let builder = "";
×
270
        if (this.description) {
×
271
            builder += this.description;
×
272
            builder += " ";
×
273
        }
274
        builder += `[${this.mimeType}] ${this.data.length} bytes`;
×
275

276
        return builder;
×
277
    }
278

279
    // #endregion
280

281
    /** @inheritDoc */
282
    protected parseFields(data: ByteVector, version: number): void {
283
        if (data.length < 5) {
9!
UNCOV
284
            throw new CorruptFileError("A picture frame must contain at least 5 bytes");
×
285
        }
286

287
        this._rawData = data;
9✔
288
        this._rawVersion = version;
9✔
289
    }
290

291
    /** @inheritDoc */
292
    protected renderFields(version: number): ByteVector {
293
        this.parseFromRaw();
7✔
294

295
        const encoding = AttachmentFrame.correctEncoding(this.textEncoding, version);
7✔
296
        let data;
297

298
        if (this.frameId === FrameIdentifiers.APIC) {
7✔
299
            // Render an ID3v2 attached picture
300
            let extensionData;
301
            if (version === 2) {
3✔
302
                let ext = Picture.getExtensionFromMimeType(this.mimeType);
2✔
303
                ext = ext && ext.length >= 3 ? ext.substring(ext.length - 3).toUpperCase() : "XXX";
2✔
304
                extensionData = ByteVector.fromString(ext, StringType.Latin1);
2✔
305
            } else {
306
                extensionData = ByteVector.concatenate(
1✔
307
                    ByteVector.fromString(this.mimeType, StringType.Latin1),
308
                    ByteVector.getTextDelimiter(StringType.Latin1)
309
                );
310
            }
311

312
            data = ByteVector.concatenate(
3✔
313
                encoding,
314
                extensionData,
315
                this._type,
316
                ByteVector.fromString(this.description, encoding),
317
                ByteVector.getTextDelimiter(encoding),
318
                this._data
319
            );
320
        } else if (this.frameId === FrameIdentifiers.GEOB) {
4!
321
            // Make an ID3v2 general encapsulated object
322
            data = ByteVector.concatenate(
4✔
323
                encoding,
324
                this._mimeType ? ByteVector.fromString(this._mimeType, StringType.Latin1) : undefined,
4✔
325
                ByteVector.getTextDelimiter(StringType.Latin1),
326
                this._filename ? ByteVector.fromString(this._filename, encoding) : undefined,
4✔
327
                ByteVector.getTextDelimiter(encoding),
328
                this._description ? ByteVector.fromString(this._description, encoding) : undefined,
4✔
329
                ByteVector.getTextDelimiter(encoding),
330
                this._data
331
            );
332
        } else {
333
            throw new Error("Invalid operation: Bad frame type");
×
334
        }
335

336
        return data;
7✔
337
    }
338

339
    private parseFromRaw(): void {
340
        if (this._rawData) {
167✔
341
            this.parseFromRawData(false);
8✔
342
        } else if (this._rawPicture) {
159✔
343
            if (this._rawVersion !== undefined) {
30!
344
                this._rawData = this._rawPicture.data.toByteVector();
×
345
                this._rawPicture = undefined;
×
346
                this.parseFromRawData(true);
×
347
            } else {
348
                this.parseFromRawPicture();
30✔
349
            }
350
        }
351
    }
352

353
    private parseFromRawData(shouldRunFieldData: boolean): void {
354
        // Indicate raw data has been processed
355
        const data = shouldRunFieldData
8!
356
            ? super.fieldData(this._rawData, 0, this._rawVersion, false)
357
            : this._rawData;
358
        this._rawData = undefined;
8✔
359

360
        // Determine encoding
361
        this._encoding = data.get(0);
8✔
362
        const delim = ByteVector.getTextDelimiter(this._encoding);
8✔
363

364
        let descriptionEndIndex;
365
        // @TODO: Maybe make two different classes?
366
        if (this.frameId === FrameIdentifiers.APIC) {
8✔
367
            // Retrieve an ID3v2 attached picture
368
            if (this._rawVersion > 2) {
6✔
369
                // Text encoding      $xx
370
                // MIME type          <text string> $00
371
                // Picture type       $xx
372
                // Description        <text string according to encoding> $00 (00)
373
                // Picture data       <binary data>
374
                const mimeTypeEndIndex = data.offsetFind(ByteVector.getTextDelimiter(StringType.Latin1), 1);
5✔
375
                if (mimeTypeEndIndex < 0) {
5!
376
                    return;
×
377
                }
378
                const mimeTypeLength = mimeTypeEndIndex - 1;
5✔
379
                this._mimeType = data.subarray(1, mimeTypeLength).toString(StringType.Latin1);
5✔
380

381
                this._type = data.get(mimeTypeEndIndex + 1);
5✔
382

383
                descriptionEndIndex = data.offsetFind(delim, mimeTypeEndIndex + 2, delim.length);
5✔
384
                if (descriptionEndIndex < 0) {
5!
385
                    return;
×
386
                }
387

388
                const descriptionLength = descriptionEndIndex - mimeTypeEndIndex - 2;
5✔
389
                this._description = data.subarray(mimeTypeEndIndex + 2, descriptionLength).toString(this._encoding);
5✔
390
            } else {
391
                // Text encoding      $xx
392
                // Image format       $xx xx xx
393
                // Picture type       $xx
394
                // Description        <text_string> $00 (00)
395
                // Picture data       <binary data>
396
                const imageFormat = data.subarray(1, 3).toString(StringType.Latin1);
1✔
397
                this._mimeType = Picture.getMimeTypeFromFilename(imageFormat);
1✔
398

399
                this._type = data.get(4);
1✔
400

401
                descriptionEndIndex = data.offsetFind(delim, 5, delim.length);
1✔
402
                if (descriptionEndIndex < 0) {
1!
403
                    return;
×
404
                }
405
                const descriptionLength = descriptionEndIndex - 5;
1✔
406
                this._description = data.subarray(5, descriptionLength).toString(this._encoding);
1✔
407
            }
408

409
            this._data = data.subarray(descriptionEndIndex + delim.length).toByteVector();
6✔
410
        } else if (this.frameId === FrameIdentifiers.GEOB) {
2!
411
            // Retrieve an ID3v2 generic encapsulated object
412
            // Text encoding          $xx
413
            // MIME type              <text string> $00
414
            // Filename               <text string according to encoding> $00 (00)
415
            // Content description    <text string according to encoding> $00 (00)
416
            // Encapsulated object    <binary data>
417
            const mimeTypeEndIndex = data.offsetFind(ByteVector.getTextDelimiter(StringType.Latin1), 1);
2✔
418
            if (mimeTypeEndIndex === -1) {
2!
419
                return;
×
420
            }
421
            const mimeTypeLength = mimeTypeEndIndex - 1;
2✔
422
            this._mimeType = data.subarray(1, mimeTypeLength)
2✔
423
                .toString(StringType.Latin1);
424

425
            const filenameEndIndex = data.offsetFind(delim, mimeTypeEndIndex + 1, delim.length);
2✔
426
            const filenameLength = filenameEndIndex - mimeTypeEndIndex - 1;
2✔
427
            this._filename = data.subarray(mimeTypeEndIndex + 1, filenameLength)
2✔
428
                .toString(this._encoding);
429

430
            descriptionEndIndex = data.offsetFind(delim, filenameEndIndex + delim.length, delim.length);
2✔
431
            const descriptionLength = descriptionEndIndex - filenameEndIndex - delim.length;
2✔
432
            this._description = data.subarray(filenameEndIndex + delim.length, descriptionLength)
2✔
433
                .toString(this._encoding);
434

435
            this._data = data.subarray(descriptionEndIndex + delim.length).toByteVector();
2✔
436
            this._type = PictureType.NotAPicture;
2✔
437
        } else {
438
            // Unsupported
UNCOV
439
            throw new Error("Unsupported: AttachmentFrame cannot be used for frame IDs other than GEOB or APIC");
×
440
        }
441
    }
442

443
    private parseFromRawPicture(): void {
444
        // Indicate raw picture has been processed
445
        const picture = this._rawPicture;
30✔
446
        this._rawPicture = undefined;
30✔
447

448
        // Bring over values from the picture
449
        this._data = picture.data.toByteVector();
30✔
450
        this._description = picture.description;
30✔
451
        this._filename = picture.filename;
30✔
452
        this._mimeType = picture.mimeType;
30✔
453
        this._type = picture.type;
30✔
454
        this.header.frameSize = this._data.length;
30✔
455

456
        this._encoding = Id3v2Settings.defaultEncoding;
30✔
457

458
        // Switch the frame ID if we discovered the attachment isn't an image
459
        if (this._type === PictureType.NotAPicture) {
30✔
460
            this.header.frameId = FrameIdentifiers.GEOB;
5✔
461
        }
462
    }
463
}
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