Completed
Push — master ( a6c784...b034ab )
by Jan
16s queued 14s
created

ID3Helpers.js ➔ decompressFrame   B

Complexity

Conditions 6

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 31
rs 8.6166
c 0
b 0
f 0
cc 6
1
const zlib = require('zlib')
2
const ID3Definitions = require("./ID3Definitions")
3
const ID3Frames = require('./ID3Frames')
4
const ID3Util = require('./ID3Util')
5
6
/**
7
 * Returns array of buffers created by tags specified in the tags argument
8
 * @param tags - Object containing tags to be written
9
 * @returns {Array}
10
 */
11
function createBuffersFromTags(tags) {
12
    const frames = []
13
    if(!tags) {
14
        return frames
15
    }
16
    const rawObject = Object.keys(tags).reduce((acc, val) => {
17
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
18
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
19
        } else if(ID3Definitions.FRAME_IDENTIFIERS.v4[val] !== undefined) {
20
            /**
21
             * Currently, node-id3 always writes ID3 version 3.
22
             * However, version 3 and 4 are very similar, and node-id3 can also read version 4 frames.
23
             * Until version 4 is fully supported, as a workaround, allow writing version 4 frames into a version 3 tag.
24
             * If a reader does not support a v4 frame, it's (per spec) supposed to skip it, so it should not be a problem.
25
             */
26
            acc[ID3Definitions.FRAME_IDENTIFIERS.v4[val]] = tags[val]
27
        } else {
28
            acc[val] = tags[val]
29
        }
30
        return acc
31
    }, {})
32
33
    Object.keys(rawObject).forEach((frameIdentifier) => {
34
        let frame
35
        // Check if invalid frameIdentifier
36
        if(frameIdentifier.length !== 4) {
37
            return
38
        }
39
        if(ID3Frames[frameIdentifier] !== undefined) {
40
            frame = ID3Frames[frameIdentifier].create(rawObject[frameIdentifier], 3)
41
        } else if(frameIdentifier.startsWith('T')) {
42
            frame = ID3Frames.GENERIC_TEXT.create(frameIdentifier, rawObject[frameIdentifier], 3)
43
        } else if(frameIdentifier.startsWith('W')) {
44
            if(ID3Util.getSpecOptions(frameIdentifier, 3).multiple && rawObject[frameIdentifier] instanceof Array && rawObject[frameIdentifier].length > 0) {
45
                frame = Buffer.alloc(0)
46
                // deduplicate array
47
                for(const url of [...new Set(rawObject[frameIdentifier])]) {
48
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(frameIdentifier, url, 3)])
49
                }
50
            } else {
51
                frame = ID3Frames.GENERIC_URL.create(frameIdentifier, rawObject[frameIdentifier], 3)
52
            }
53
        }
54
55
        if (frame && frame instanceof Buffer) {
56
            frames.push(frame)
57
        }
58
    })
59
60
    return frames
61
}
62
63
/**
64
 * Return a buffer with the frames for the specified tags
65
 * @param tags - Object containing tags to be written
66
 * @returns {Buffer}
67
 */
68
module.exports.createBufferFromTags = function(tags) {
69
    return Buffer.concat(createBuffersFromTags(tags))
70
}
71
72
module.exports.getTagsFromBuffer = function(filebuffer, options) {
73
    const framePosition = ID3Util.getFramePosition(filebuffer)
74
    if(framePosition === -1) {
75
        return getTagsFromFrames([], 3, options)
76
    }
77
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
78
    const ID3Frame = Buffer.alloc(frameSize + 1)
79
    filebuffer.copy(ID3Frame, 0, framePosition)
80
    //ID3 version e.g. 3 if ID3v2.3.0
81
    const ID3Version = ID3Frame[3]
82
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
83
    let extendedHeaderOffset = 0
84
    if(tagFlags.extendedHeader) {
85
        if(ID3Version === 3) {
86
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
87
        } else if(ID3Version === 4) {
88
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
89
        }
90
    }
91
    const ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
92
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
93
94
    const frames = getFramesFromID3Body(ID3FrameBody, ID3Version, options)
95
96
    return getTagsFromFrames(frames, ID3Version, options)
97
}
98
99
function isFrameDiscardedByOptions(frameIdentifier, options) {
100
    if(options.exclude instanceof Array && options.exclude.includes(frameIdentifier)) {
101
        return true
102
    }
103
104
    return options.include instanceof Array && !options.include.includes(frameIdentifier)
105
}
106
107
function getFramesFromID3Body(ID3TagBody, ID3Version, options = {}) {
108
    let currentPosition = 0
109
    const frames = []
110
    if(!ID3TagBody || !(ID3TagBody instanceof Buffer)) {
111
        return frames
112
    }
113
114
    const frameIdentifierSize = (ID3Version === 2) ? 3 : 4
115
    const frameHeaderSize = (ID3Version === 2) ? 6 : 10
116
117
    while(currentPosition < ID3TagBody.length && ID3TagBody[currentPosition] !== 0x00) {
118
        const frameHeader = ID3TagBody.subarray(currentPosition, currentPosition + frameHeaderSize)
119
120
        const frameIdentifier = frameHeader.toString('utf8', 0, frameIdentifierSize)
121
        const decodeSize = ID3Version === 4
122
        const frameBodySize = ID3Util.getFrameSize(frameHeader, decodeSize, ID3Version)
123
        // It's possible to discard frames via options.exclude/options.include
124
        // If that is the case, skip this frame and continue with the next
125
        if(isFrameDiscardedByOptions(frameIdentifier, options)) {
126
            currentPosition += frameBodySize + frameHeaderSize
127
            continue
128
        }
129
        // Prevent errors when the current frame's size exceeds the remaining tags size (e.g. due to broken size bytes).
130
        if(frameBodySize + frameHeaderSize > (ID3TagBody.length - currentPosition)) {
131
            break
132
        }
133
134
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(frameHeader, ID3Version)
135
        // Frames may have a 32-bit data length indicator appended after their header,
136
        // if that is the case, the real body starts after those 4 bytes.
137
        const frameBodyOffset = frameHeaderFlags.dataLengthIndicator ? 4 : 0
138
        const frameBodyStart = currentPosition + frameHeaderSize + frameBodyOffset
139
        const frameBody = ID3TagBody.subarray(frameBodyStart, frameBodyStart + frameBodySize - frameBodyOffset)
140
141
        const frame = {
142
            name: frameIdentifier,
143
            flags: frameHeaderFlags,
144
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(frameBody) : frameBody
145
        }
146
        if(frameHeaderFlags.dataLengthIndicator) {
147
            frame.dataLengthIndicator = ID3TagBody.readInt32BE(currentPosition + frameHeaderSize)
148
        }
149
        frames.push(frame)
150
151
        //  Size of frame body + its header
152
        currentPosition += frameBodySize + frameHeaderSize
153
    }
154
155
    return frames
156
}
157
158
function decompressFrame(frame) {
159
    if(frame.body.length < 5 || frame.dataLengthIndicator === undefined) {
160
        return null
161
    }
162
163
    /*
164
    * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
165
    * ZLIB has a 2-byte header.
166
    * 1. try if header + body decompression
167
    * 2. else try if header is not stored (assume that all content is deflated "body")
168
    * 3. else try if inflation works if the header is omitted (implementation dependent)
169
    * */
170
    let decompressedBody
171
    try {
172
        decompressedBody = zlib.inflateSync(frame.body)
173
    } catch (e) {
174
        try {
175
            decompressedBody = zlib.inflateRawSync(frame.body)
176
        } catch (e) {
177
            try {
178
                decompressedBody = zlib.inflateRawSync(frame.body.slice(2))
179
            } catch (e) {
180
                return null
181
            }
182
        }
183
    }
184
    if(decompressedBody.length !== frame.dataLengthIndicator) {
185
        return null
186
    }
187
    return decompressedBody
188
}
189
190
function getTagsFromFrames(frames, ID3Version, options = {}) {
191
    const tags = { }
192
    const raw = { }
193
194
    frames.forEach((frame) => {
195
        let frameIdentifier
196
        let identifier
197
        if(ID3Version === 2) {
198
            frameIdentifier = ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]]
199
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]
200
        } else if(ID3Version === 3 || ID3Version === 4) {
201
            /**
202
             * Due to their similarity, it's possible to mix v3 and v4 frames even if they don't exist in their corrosponding spec.
203
             * Programs like Mp3tag allow you to do so, so we should allow reading e.g. v4 frames from a v3 ID3 Tag
204
             */
205
            frameIdentifier = frame.name
206
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name] || ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v4[frame.name]
207
        }
208
209
        if(!frameIdentifier || !identifier || frame.flags.encryption) {
210
            return
211
        }
212
213
        if(frame.flags.compression) {
214
            const decompressedBody = decompressFrame(frame)
215
            if(!decompressedBody) {
216
                return
217
            }
218
            frame.body = decompressedBody
219
        }
220
221
        let decoded
222
        if(ID3Frames[frameIdentifier]) {
223
            decoded = ID3Frames[frameIdentifier].read(frame.body, ID3Version)
224
        } else if(frameIdentifier.startsWith('T')) {
225
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
226
        } else if(frameIdentifier.startsWith('W')) {
227
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
228
        }
229
230
        if(!decoded) {
231
            return
232
        }
233
234
        if(ID3Util.getSpecOptions(frameIdentifier, ID3Version).multiple) {
235
            if(!options.onlyRaw) {
236
                if(!tags[identifier]) {
237
                    tags[identifier] = []
238
                }
239
                tags[identifier].push(decoded)
240
            }
241
            if(!options.noRaw) {
242
                if(!raw[frameIdentifier]) {
243
                    raw[frameIdentifier] = []
244
                }
245
                raw[frameIdentifier].push(decoded)
246
            }
247
        } else {
248
            if(!options.onlyRaw) {
249
                tags[identifier] = decoded
250
            }
251
            if(!options.noRaw) {
252
                raw[frameIdentifier] = decoded
253
            }
254
        }
255
    })
256
257
    if(options.onlyRaw) {
258
        return raw
259
    }
260
    if(options.noRaw) {
261
        return tags
262
    }
263
264
    tags.raw = raw
265
    return tags
266
}
267
268
module.exports.getTagsFromID3Body = function(body) {
269
    return getTagsFromFrames(getFramesFromID3Body(body, 3), 3)
270
}
271