Passed
Pull Request — 0.2 (#185)
by
unknown
04:36
created

src/ID3Util.js   A

Complexity

Total Complexity 42
Complexity/F 2.47

Size

Lines of Code 235
Function Count 17

Duplication

Duplicated Lines 19
Ratio 8.09 %

Importance

Changes 0
Metric Value
wmc 42
eloc 140
mnd 25
bc 25
fnc 17
dl 19
loc 235
rs 9.0399
bpm 1.4705
cpm 2.4705
noi 2
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
A ID3Util.js ➔ constructor 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like src/ID3Util.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const iconv = require('iconv-lite')
2
const ID3Definitions = require('./ID3Definitions')
3
4
const ENCODINGS = [
5
    'ISO-8859-1', 'UTF-16', 'UTF-16BE', 'UTF-8'
6
]
7
8
module.exports.SplitBuffer = class SplitBuffer {
9
    constructor(value = null, remainder = null) {
10
        this.value = value
11
        this.remainder = remainder
12
    }
13
}
14
15
/**
16
 * Expects a buffer containing a string at the beginning that is terminated by a \0 character.
17
 * Returns a split buffer containing the bytes before and after null termination.
18
 */
19
module.exports.splitNullTerminatedBuffer = function(buffer, encodingByte = 0x00) {
20
    // UTF-16/BE always uses two bytes per character.
21
    // \0 is therefore encoded as [0x00, 0x00] instead of just [0x00].
22
    // We'll do a sliding window search, window size depends on encoding.
23
    const charSize = [0x01, 0x02].includes(encodingByte) ? 2 : 1
24
    for(let pos = 0; pos + charSize - 1 < buffer.length; pos += charSize) {
25
        if(buffer.readUIntBE(pos, charSize) === 0) {
26
            return new this.SplitBuffer(
27
                buffer.subarray(0, pos),
28
                buffer.subarray(pos + charSize)
29
            )
30
        }
31
    }
32
33
    return new this.SplitBuffer(null, buffer.subarray(0))
34
}
35
36
module.exports.terminationBuffer = function(encodingByte = 0x00) {
37
    if(encodingByte === 0x01 || encodingByte === 0x02) {
38
        return Buffer.alloc(2, 0x00)
39
    }
40
    return Buffer.alloc(1, 0x00)
41
}
42
43
module.exports.encodingFromStringOrByte = function(encoding) {
44
    if(ENCODINGS.indexOf(encoding) !== -1) {
45
        return encoding
46
    } else if(encoding > -1 && encoding < ENCODINGS.length) {
47
        encoding = ENCODINGS[encoding]
48
    } else {
49
        encoding = ENCODINGS[0]
50
    }
51
    return encoding
52
}
53
54
module.exports.stringToEncodedBuffer = function(str, encodingByte) {
55
    return iconv.encode(str, this.encodingFromStringOrByte(encodingByte))
56
}
57
58
module.exports.bufferToDecodedString = function(buffer, encodingByte) {
59
    return iconv.decode(buffer, this.encodingFromStringOrByte(encodingByte)).replace(/\0/g, '')
60
}
61
62
module.exports.getSpecOptions = function(frameIdentifier) {
63
    if(ID3Definitions.ID3_FRAME_OPTIONS[frameIdentifier]) {
64
        return ID3Definitions.ID3_FRAME_OPTIONS[frameIdentifier]
65
    }
66
67
    return {}
68
}
69
70
module.exports.isValidID3Header = function(buffer) {
71
    if(buffer.length < 10) {
72
        return false
73
    }
74
    if(buffer.readUIntBE(0, 3) !== 0x494433) {
75
        return false
76
    }
77
    if([0x02, 0x03, 0x04].indexOf(buffer[3]) === -1 || buffer[4] !== 0x00) {
78
        return false
79
    }
80
    return this.isValidEncodedSize(buffer.slice(6, 10))
81
}
82
83
module.exports.getFramePosition = function(buffer) {
84
    /* ID3v2 tags are ALWAYS at the beginning of the file (position 0)
85
     * This prevents false positives from "ID3" patterns in audio data
86
     * which can cause severe file corruption when removed
87
     */
88
    let framePosition = -1
89
    let frameHeaderValid = false
90
    
91
    // Only check position 0
92
    if (buffer.length >= 3 && buffer.slice(0, 3).toString() === "ID3") {
93
        framePosition = 0
94
        frameHeaderValid = this.isValidID3Header(buffer.slice(0, 10))
95
    }
96
97
    if(!frameHeaderValid) {
98
        return -1
99
    }
100
    return framePosition
101
}
102
103
/**
104
 * @param {Buffer} encodedSize
105
 * @return {boolean} Return if the header contains a valid encoded size
106
 */
107
module.exports.isValidEncodedSize = function(encodedSize) {
108
    // The size must not have the bit 7 set
109
    return ((
110
        encodedSize[0] |
111
        encodedSize[1] |
112
        encodedSize[2] |
113
        encodedSize[3]
114
    ) & 128) === 0
115
}
116
117
/**
118
 * ID3 header size uses only 7 bits of a byte, bit shift is needed
119
 * @param {number} size
120
 * @return {Buffer} Return a Buffer of 4 bytes with the encoded size
121
 */
122
module.exports.encodeSize = function(size) {
123
    const byte_3 = size & 0x7F
124
    const byte_2 = (size >> 7) & 0x7F
125
    const byte_1 = (size >> 14) & 0x7F
126
    const byte_0 = (size >> 21) & 0x7F
127
    return Buffer.from([byte_0, byte_1, byte_2, byte_3])
128
}
129
130
/**
131
 * Decode the size encoded in the ID3 header
132
 * @param {Buffer} encodedSize
133
 * @return {number} Return the decoded size
134
 */
135
module.exports.decodeSize = function(encodedSize) {
136
    return (
137
        (encodedSize[0] << 21) +
138
        (encodedSize[1] << 14) +
139
        (encodedSize[2] << 7) +
140
        encodedSize[3]
141
    )
142
}
143
144
module.exports.getFrameSize = function(buffer, decode, ID3Version) {
145
    let decodeBytes
146
    if(ID3Version > 2) {
147
        decodeBytes = [buffer[4], buffer[5], buffer[6], buffer[7]]
148
    } else {
149
        decodeBytes = [buffer[3], buffer[4], buffer[5]]
150
    }
151
    if(decode) {
152
        return this.decodeSize(Buffer.from(decodeBytes))
153
    } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
154
        return Buffer.from(decodeBytes).readUIntBE(0, decodeBytes.length)
155
    }
156
}
157
158
module.exports.parseTagHeaderFlags = function(header) {
159
    if(!(header instanceof Buffer && header.length >= 10)) {
160
        return {}
161
    }
162
    const version = header[3]
163
    const flagsByte = header[5]
164
    if(version === 3) {
165
        return {
166
            unsynchronisation: !!(flagsByte & 128),
167
            extendedHeader: !!(flagsByte & 64),
168
            experimentalIndicator: !!(flagsByte & 32)
169
        }
170
    }
171 View Code Duplication
    if(version === 4) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
172
        return {
173
            unsynchronisation: !!(flagsByte & 128),
174
            extendedHeader: !!(flagsByte & 64),
175
            experimentalIndicator: !!(flagsByte & 32),
176
            footerPresent: !!(flagsByte & 16)
177
        }
178
    }
179
    return {}
180
}
181
182
module.exports.parseFrameHeaderFlags = function(header, ID3Version) {
183
    if(!(header instanceof Buffer && header.length === 10)) {
184
        return {}
185
    }
186
    const flagsFirstByte = header[8]
187
    const flagsSecondByte = header[9]
188 View Code Duplication
    if(ID3Version === 3) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
189
        return {
190
            tagAlterPreservation: !!(flagsFirstByte & 128),
191
            fileAlterPreservation: !!(flagsFirstByte & 64),
192
            readOnly: !!(flagsFirstByte & 32),
193
            compression: !!(flagsSecondByte & 128),
194
            encryption: !!(flagsSecondByte & 64),
195
            groupingIdentity: !!(flagsSecondByte & 32),
196
            dataLengthIndicator: !!(flagsSecondByte & 128)
197
        }
198
    }
199
    if(ID3Version === 4) {
200
        return {
201
            tagAlterPreservation: !!(flagsFirstByte & 64),
202
            fileAlterPreservation: !!(flagsFirstByte & 32),
203
            readOnly: !!(flagsFirstByte & 16),
204
            groupingIdentity: !!(flagsSecondByte & 64),
205
            compression: !!(flagsSecondByte & 8),
206
            encryption: !!(flagsSecondByte & 4),
207
            unsynchronisation: !!(flagsSecondByte & 2),
208
            dataLengthIndicator: !!(flagsSecondByte & 1)
209
        }
210
    }
211
    return {}
212
}
213
214
module.exports.processUnsynchronisedBuffer = function(buffer) {
215
    const newDataArr = []
216
    if(buffer.length > 0) {
217
        newDataArr.push(buffer[0])
218
    }
219
    for(let i = 1; i < buffer.length; i++) {
220
        if(buffer[i - 1] === 0xFF && buffer[i] === 0x00)
221
            continue
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
222
        newDataArr.push(buffer[i])
223
    }
224
    return Buffer.from(newDataArr)
225
}
226
227
module.exports.getPictureMimeTypeFromBuffer = function(pictureBuffer) {
228
    if (pictureBuffer.length > 3 && pictureBuffer.compare(Buffer.from([0xff, 0xd8, 0xff]), 0, 3, 0, 3) === 0) {
229
        return "image/jpeg"
230
    } else if (pictureBuffer > 8 && pictureBuffer.compare(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), 0, 8, 0, 8) === 0) {
231
        return "image/png"
232
    } else {
233
        return null
234
    }
235
}
236