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

benrr101 / node-taglib-sharp / 46462135

pending completion
46462135

push

appveyor

Benjamin Russell
Merge branch 'release/v5.1.0'

3096 of 3788 branches covered (81.73%)

Branch coverage included in aggregate %.

2171 of 2171 new or added lines in 47 files covered. (100.0%)

25320 of 26463 relevant lines covered (95.68%)

437.0 hits per line

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

89.57
/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;
46✔
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);
46✔
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");
12✔
76
        Guards.uint(offset, "offset");
10✔
77
        Guards.truthy(header, "header");
5✔
78
        Guards.byte(version, "version");
3✔
79

80
        const frame = new AttachmentFrame(header);
3✔
81
        frame.setData(data, offset, false, version);
3✔
82
        return frame;
3✔
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
    /**
105
     * Constructs and initializes a new attachment frame by reading its raw data in a specified
106
     * Id3v2 version.
107
     * @param data ByteVector starting with the raw representation of the new frame
108
     * @param version ID3v2 version the raw frame is encoded with.
109
     */
110
    public static fromRawData(data: ByteVector, version: number): AttachmentFrame {
111
        Guards.truthy(data, "data");
12✔
112
        Guards.byte(version, "version");
10✔
113

114
        const frame = new AttachmentFrame(Id3v2FrameHeader.fromData(data, version));
7✔
115
        frame.setData(data, 0, true, version);
7✔
116
        return frame;
6✔
117
    }
118

119
    // #endregion
120

121
    // #region Properties
122

123
    /** @inheritDoc */
124
    public get frameClassType(): FrameClassType { return FrameClassType.AttachmentFrame; }
16✔
125

126
    /**
127
     * Gets the image data stored in the current instance.
128
     */
129
    public get data(): ByteVector {
130
        this.parseFromRaw();
17✔
131
        return this._data ? this._data : ByteVector.empty();
17✔
132
    }
133
    /**
134
     * Sets the image data stored in the current instance.
135
     */
136
    public set data(value: ByteVector) {
137
        this.parseFromRaw();
3✔
138
        this._data = value;
3✔
139
    }
140

141
    /**
142
     * Gets the description stored in the current instance.
143
     */
144
    public get description(): string {
145
        this.parseFromRaw();
25✔
146
        return this._description ? this._description : "";
25✔
147
    }
148
    /**
149
     * Sets the description stored in the current instance.
150
     * There should only be one frame with a matching description and type per tag.
151
     */
152
    public set description(value: string) {
153
        this.parseFromRaw();
11✔
154
        this._description = value;
11✔
155
    }
156

157
    /**
158
     * Gets a filename of the picture stored in the current instance.
159
     */
160
    public get filename(): string {
161
        this.parseFromRaw();
16✔
162
        return this._filename;
16✔
163
    }
164
    /**
165
     * Sets a filename of the picture stored in the current instance.
166
     */
167
    public set filename(value: string) {
168
        this.parseFromRaw();
3✔
169
        this._filename = value;
3✔
170
    }
171

172
    /**
173
     * Gets the MimeType of the picture stored in the current instance.
174
     */
175
    public get mimeType(): string {
176
        this.parseFromRaw();
19✔
177

178
        return this._mimeType ? this._mimeType : "";
19✔
179
    }
180
    /**
181
     * Sets the MimeType of the picture stored in the current instance.
182
     * @param value MimeType of the picture stored in the current instance.
183
     */
184
    public set mimeType(value: string) {
185
        this.parseFromRaw();
3✔
186
        this._mimeType = value;
3✔
187
    }
188

189
    /**
190
     * Gets the text encoding to use when storing the current instance.
191
     * @value Text encoding to use when storing the current instance.
192
     */
193
    public get textEncoding(): StringType {
194
        this.parseFromRaw();
21✔
195
        return this._encoding;
21✔
196
    }
197
    /**
198
     * Sets the text encoding to use when storing the current instance.
199
     * @param value Text encoding to use when storing the current instance.
200
     *     This encoding is overridden when rendering if
201
     *     {@link Id3v2Settings.forceDefaultEncoding} is `true` or the render version does not
202
     *     support it.
203
     */
204
    public set textEncoding(value: StringType) {
205
        this.parseFromRaw();
1✔
206
        this._encoding = value;
1✔
207
    }
208

209
    /**
210
     * Gets the object type stored in the current instance.
211
     */
212
    public get type(): PictureType {
213
        this.parseFromRaw();
22✔
214
        return this._type;
21✔
215
    }
216
    /**
217
     * Sets the object type stored in the current instance.
218
     * For General Object Frame, use {@link PictureType.NotAPicture}. Other types will make it a
219
     * Picture Frame.
220
     */
221
    public set type(value: PictureType) {
222
        this.parseFromRaw();
14✔
223

224
        // Change the frame type depending if this is a picture or a general object
225
        const frameId = value === PictureType.NotAPicture
14✔
226
            ? FrameIdentifiers.GEOB
14✔
227
            : FrameIdentifiers.APIC;
228
        if (this.header.frameId !== frameId) {
14✔
229
            this.header = new Id3v2FrameHeader(frameId);
3✔
230
        }
231

232
        this._type = value;
14✔
233
    }
234

235
    // #endregion
236

237
    // #region Public Methods
238

239
    /** @inheritDoc */
240
    public clone(): Frame {
241
        const frame = new AttachmentFrame(new Id3v2FrameHeader(this.frameId));
3✔
242
        frame._data = this._data?.toByteVector();
3✔
243
        frame._description = this._description;
3✔
244
        frame._encoding = this._encoding;
3✔
245
        frame._filename = this._filename;
3✔
246
        frame._mimeType = this._mimeType;
3✔
247
        frame._rawData = this._rawData;
3✔
248
        frame._rawPicture = this._rawPicture;
3✔
249
        frame._rawVersion = this._rawVersion;
3✔
250
        frame._type = this._type;
3✔
251

252
        return frame;
3✔
253
    }
254

255
    /**
256
     * Get a specified attachment frame from the specified tag, optionally creating it if it does
257
     * not exist.
258
     * @param frames List of attachment frames to search
259
     * @param description Description to match
260
     * @param type Picture type to match
261
     * @returns Matching frame or `undefined` if a match wasn't found and `create` is `false`
262
     */
263
    public static find(
264
        frames: AttachmentFrame[],
265
        description?: string,
266
        type: PictureType = PictureType.Other
5✔
267
    ): AttachmentFrame {
268
        Guards.truthy(frames, "frames");
8✔
269
        return frames.find((f) => {
6✔
270
                if (description && f.description !== description) { return false; }
9✔
271
                // noinspection RedundantIfStatementJS
272
                if (type !== PictureType.Other && f.type !== type) { return false; }
6✔
273
                return true;
3✔
274
            });
275
    }
276

277
    /**
278
     * Generates a string representation of the current instance.
279
     * @deprecated No need for this.
280
     */
281
    public toString(): string {
282
        this.parseFromRaw();
×
283

284
        let builder = "";
×
285
        if (this.description) {
×
286
            builder += this.description;
×
287
            builder += " ";
×
288
        }
289
        builder += `[${this.mimeType}] ${this.data.length} bytes`;
×
290

291
        return builder;
×
292
    }
293

294
    // #endregion
295

296
    /** @inheritDoc */
297
    protected parseFields(data: ByteVector, version: number): void {
298
        if (data.length < 5) {
10✔
299
            throw new CorruptFileError("A picture frame must contain at least 5 bytes");
1✔
300
        }
301

302
        this._rawData = data;
9✔
303
        this._rawVersion = version;
9✔
304
    }
305

306
    /** @inheritDoc */
307
    protected renderFields(version: number): ByteVector {
308
        this.parseFromRaw();
7✔
309

310
        const encoding = AttachmentFrame.correctEncoding(this.textEncoding, version);
7✔
311
        let data;
312

313
        if (this.frameId === FrameIdentifiers.APIC) {
7✔
314
            // Render an ID3v2 attached picture
315
            let extensionData;
316
            if (version === 2) {
3✔
317
                let ext = Picture.getExtensionFromMimeType(this.mimeType);
2✔
318
                ext = ext && ext.length >= 3 ? ext.substring(ext.length - 3).toUpperCase() : "XXX";
2✔
319
                extensionData = ByteVector.fromString(ext, StringType.Latin1);
2✔
320
            } else {
321
                extensionData = ByteVector.concatenate(
1✔
322
                    ByteVector.fromString(this.mimeType, StringType.Latin1),
323
                    ByteVector.getTextDelimiter(StringType.Latin1)
324
                );
325
            }
326

327
            data = ByteVector.concatenate(
3✔
328
                encoding,
329
                extensionData,
330
                this._type,
331
                ByteVector.fromString(this.description, encoding),
332
                ByteVector.getTextDelimiter(encoding),
333
                this._data
334
            );
335
        } else if (this.frameId === FrameIdentifiers.GEOB) {
4!
336
            // Make an ID3v2 general encapsulated object
337
            data = ByteVector.concatenate(
4✔
338
                encoding,
339
                this._mimeType ? ByteVector.fromString(this._mimeType, StringType.Latin1) : undefined,
4✔
340
                ByteVector.getTextDelimiter(StringType.Latin1),
341
                this._filename ? ByteVector.fromString(this._filename, encoding) : undefined,
4✔
342
                ByteVector.getTextDelimiter(encoding),
343
                this._description ? ByteVector.fromString(this._description, encoding) : undefined,
4✔
344
                ByteVector.getTextDelimiter(encoding),
345
                this._data
346
            );
347
        } else {
348
            throw new Error("Invalid operation: Bad frame type");
×
349
        }
350

351
        return data;
7✔
352
    }
353

354
    private parseFromRaw(): void {
355
        if (this._rawData) {
162✔
356
            this.parseFromRawData(false);
8✔
357
        } else if (this._rawPicture) {
154✔
358
            if (this._rawVersion !== undefined) {
30!
359
                this._rawData = this._rawPicture.data.toByteVector();
×
360
                this._rawPicture = undefined;
×
361
                this.parseFromRawData(true);
×
362
            } else {
363
                this.parseFromRawPicture();
30✔
364
            }
365
        }
366
    }
367

368
    private parseFromRawData(shouldRunFieldData: boolean): void {
369
        // Indicate raw data has been processed
370
        const data = shouldRunFieldData
8✔
371
            ? super.fieldData(this._rawData, 0, this._rawVersion, false)
8!
372
            : this._rawData;
373
        this._rawData = undefined;
8✔
374

375
        // Determine encoding
376
        this._encoding = data.get(0);
8✔
377
        const delim = ByteVector.getTextDelimiter(this._encoding);
8✔
378

379
        let descriptionEndIndex;
380
        // @TODO: Maybe make two different classes?
381
        if (this.frameId === FrameIdentifiers.APIC) {
8✔
382
            // Retrieve an ID3v2 attached picture
383
            if (this._rawVersion > 2) {
6✔
384
                // Text encoding      $xx
385
                // MIME type          <text string> $00
386
                // Picture type       $xx
387
                // Description        <text string according to encoding> $00 (00)
388
                // Picture data       <binary data>
389
                const mimeTypeEndIndex = data.offsetFind(ByteVector.getTextDelimiter(StringType.Latin1), 1);
5✔
390
                if (mimeTypeEndIndex < 0) {
5!
391
                    return;
×
392
                }
393
                const mimeTypeLength = mimeTypeEndIndex - 1;
5✔
394
                this._mimeType = data.subarray(1, mimeTypeLength).toString(StringType.Latin1);
5✔
395

396
                this._type = data.get(mimeTypeEndIndex + 1);
5✔
397

398
                descriptionEndIndex = data.offsetFind(delim, mimeTypeEndIndex + 2, delim.length);
5✔
399
                if (descriptionEndIndex < 0) {
5!
400
                    return;
×
401
                }
402

403
                const descriptionLength = descriptionEndIndex - mimeTypeEndIndex - 2;
5✔
404
                this._description = data.subarray(mimeTypeEndIndex + 2, descriptionLength).toString(this._encoding);
5✔
405
            } else {
406
                // Text encoding      $xx
407
                // Image format       $xx xx xx
408
                // Picture type       $xx
409
                // Description        <text_string> $00 (00)
410
                // Picture data       <binary data>
411
                const imageFormat = data.subarray(1, 3).toString(StringType.Latin1);
1✔
412
                this._mimeType = Picture.getMimeTypeFromFilename(imageFormat);
1✔
413

414
                this._type = data.get(4);
1✔
415

416
                descriptionEndIndex = data.offsetFind(delim, 5, delim.length);
1✔
417
                if (descriptionEndIndex < 0) {
1!
418
                    return;
×
419
                }
420
                const descriptionLength = descriptionEndIndex - 5;
1✔
421
                this._description = data.subarray(5, descriptionLength).toString(this._encoding);
1✔
422
            }
423

424
            this._data = data.subarray(descriptionEndIndex + delim.length).toByteVector();
6✔
425
        } else if (this.frameId === FrameIdentifiers.GEOB) {
2✔
426
            // Retrieve an ID3v2 generic encapsulated object
427
            // Text encoding          $xx
428
            // MIME type              <text string> $00
429
            // Filename               <text string according to encoding> $00 (00)
430
            // Content description    <text string according to encoding> $00 (00)
431
            // Encapsulated object    <binary data>
432
            const mimeTypeEndIndex = data.find(ByteVector.getTextDelimiter(StringType.Latin1));
1✔
433
            if (mimeTypeEndIndex === -1) {
1!
434
                return;
×
435
            }
436
            const mimeTypeLength = mimeTypeEndIndex - 1;
1✔
437
            this._mimeType = data.subarray(1, mimeTypeLength)
1✔
438
                .toString(StringType.Latin1);
439

440
            const filenameEndIndex = data.offsetFind(delim, mimeTypeEndIndex + 1, delim.length);
1✔
441
            const filenameLength = filenameEndIndex - mimeTypeEndIndex - 1;
1✔
442
            this._filename = data.subarray(mimeTypeEndIndex + 1, filenameLength)
1✔
443
                .toString(this._encoding);
444

445
            descriptionEndIndex = data.offsetFind(delim, filenameEndIndex + delim.length, delim.length);
1✔
446
            const descriptionLength = descriptionEndIndex - filenameEndIndex - delim.length;
1✔
447
            this._description = data.subarray(filenameEndIndex + delim.length, descriptionLength)
1✔
448
                .toString(this._encoding);
449

450
            this._data = data.subarray(descriptionEndIndex + delim.length).toByteVector();
1✔
451
            this._type = PictureType.NotAPicture;
1✔
452
        } else {
453
            // Unsupported
454
            throw new Error("Unsupported: AttachmentFrame cannot be used for frame IDs other than GEOB or APIC");
1✔
455
        }
456
    }
457

458
    private parseFromRawPicture(): void {
459
        // Indicate raw picture has been processed
460
        const picture = this._rawPicture;
30✔
461
        this._rawPicture = undefined;
30✔
462

463
        // Bring over values from the picture
464
        this._data = picture.data.toByteVector();
30✔
465
        this._description = picture.description;
30✔
466
        this._filename = picture.filename;
30✔
467
        this._mimeType = picture.mimeType;
30✔
468
        this._type = picture.type;
30✔
469
        this.header.frameSize = this._data.length;
30✔
470

471
        this._encoding = Id3v2Settings.defaultEncoding;
30✔
472

473
        // Switch the frame ID if we discovered the attachment isn't an image
474
        if (this._type === PictureType.NotAPicture) {
30✔
475
            this.header.frameId = FrameIdentifiers.GEOB;
5✔
476
        }
477
    }
478
}
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