Passed
Pull Request — master (#148)
by Jan
03:08 queued 01:25
created

ID3Util.ts ➔ splitNullTerminatedBuffer   A

Complexity

Conditions 4

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 28
rs 9.6
c 0
b 0
f 0
cc 4
1
import iconv = require('iconv-lite')
2
import { FrameOptions, FRAME_OPTIONS } from './definitions/FrameOptions'
3
import { isKeyOf, isString } from './util'
4
import { TextEncoding } from './definitions/Encoding'
5
6
export class SplitBuffer {
7
    value: Buffer | null
8
    remainder: Buffer | null
9
    constructor(value: Buffer | null = null, remainder: Buffer | null = null) {
10
        this.value = value
11
        this.remainder = remainder
12
    }
13
}
14
15
/**
16
 * @param buffer A buffer starting with a null-terminated text string.
17
 * @param encoding The encoding type in which the text string is encoded.
18
 * @returns A split buffer containing the bytes before and after the null
19
 *          termination. If no null termination is found, considers that
20
 *          the buffer was not containing a text string and returns
21
 *          the given buffer as the remainder in the split buffer.
22
 */
23
export function splitNullTerminatedBuffer(
24
    buffer: Buffer,
25
    encoding: number = TextEncoding.ISO_8859_1
26
) {
27
    const charSize = ([
28
        TextEncoding.UTF_16_WITH_BOM,
29
        TextEncoding.UTF_16_BE
30
    ] as number[]).includes(encoding) ? 2 : 1
31
32
    for (let pos = 0; pos <= buffer.length - charSize; pos += charSize) {
33
        if (buffer.readUIntBE(pos, charSize) === 0) {
34
            return new SplitBuffer(
35
                buffer.subarray(0, pos),
36
                buffer.subarray(pos + charSize)
37
            )
38
        }
39
    }
40
41
    return new SplitBuffer(null, buffer.subarray(0))
42
}
43
44
export function encodingFromStringOrByte(encoding: string | number) {
45
    const ENCODINGS = [
46
        'ISO-8859-1', 'UTF-16', 'UTF-16BE', 'UTF-8'
47
    ]
48
49
    if (isString(encoding) && ENCODINGS.includes(encoding)) {
50
        return encoding
51
    }
52
    if (
53
        typeof encoding === "number" &&
54
        encoding >= 0 && encoding < ENCODINGS.length
55
    ) {
56
        return ENCODINGS[encoding]
57
    }
58
    return ENCODINGS[0]
59
}
60
61
export function stringToEncodedBuffer(
62
    value: string,
63
    encodingByte: string | number
64
) {
65
    return iconv.encode(
66
        value,
67
        encodingFromStringOrByte(encodingByte)
68
    )
69
}
70
71
export function bufferToDecodedString(
72
    buffer: Buffer,
73
    encodingByte: string | number
74
) {
75
    return iconv.decode(
76
        buffer,
77
        encodingFromStringOrByte(encodingByte)
78
    ).replace(/\0/g, '')
79
}
80
81
export function getSpecOptions(frameIdentifier: string): FrameOptions {
82
    if (isKeyOf(frameIdentifier, FRAME_OPTIONS)) {
83
        return FRAME_OPTIONS[frameIdentifier]
84
    }
85
    return {
86
        multiple: false
87
    }
88
}
89
90
export function isValidID3Header(buffer: Buffer) {
91
    if (buffer.length < 10) {
92
        return false
93
    }
94
    if (buffer.readUIntBE(0, 3) !== 0x494433) {
95
        return false
96
    }
97
    if ([0x02, 0x03, 0x04].indexOf(buffer[3]) === -1 || buffer[4] !== 0x00) {
98
        return false
99
    }
100
    return isValidEncodedSize(buffer.subarray(6, 10))
101
}
102
103
/**
104
 * Returns -1 if no tag was found.
105
 */
106
export function getTagPosition(buffer: Buffer) {
107
    // Search Buffer for valid ID3 frame
108
    const tagHeaderSize = 10
109
    let position = -1
110
    let headerValid = false
111
    do {
112
        position = buffer.indexOf("ID3", position + 1)
113
        if (position !== -1) {
114
            // It's possible that there is a "ID3" sequence without being an
115
            // ID3 Frame, so we need to check for validity of the next 10 bytes.
116
            headerValid = isValidID3Header(
117
                buffer.subarray(position, position + tagHeaderSize)
118
            )
119
        }
120
    } while (position !== -1 && !headerValid)
121
122
    if (!headerValid) {
123
        return -1
124
    }
125
    return position
126
}
127
128
 export function isValidEncodedSize(encodedSize: Buffer) {
129
    // The size must not have the bit 7 set
130
    return ((
131
        encodedSize[0] |
132
        encodedSize[1] |
133
        encodedSize[2] |
134
        encodedSize[3]
135
    ) & 128) === 0
136
}
137
138
/**
139
 * ID3 header size uses only 7 bits of a byte, bit shift is needed.
140
 * @returns Return a Buffer of 4 bytes with the encoded size
141
 */
142
 export function encodeSize(size: number) {
143
    const byte_3 = size & 0x7F
144
    const byte_2 = (size >> 7) & 0x7F
145
    const byte_1 = (size >> 14) & 0x7F
146
    const byte_0 = (size >> 21) & 0x7F
147
    return Buffer.from([byte_0, byte_1, byte_2, byte_3])
148
}
149
150
/**
151
 * Decode the encoded size from an ID3 header.
152
 */
153
 export function decodeSize(encodedSize: Buffer) {
154
    return (
155
        (encodedSize[0] << 21) +
156
        (encodedSize[1] << 14) +
157
        (encodedSize[2] << 7) +
158
        encodedSize[3]
159
    )
160
}
161
162
export function getFrameSize(buffer: Buffer, decode: boolean, version: number) {
163
    const decodeBytes = version > 2 ?
164
        [buffer[4], buffer[5], buffer[6], buffer[7]] :
165
        [buffer[3], buffer[4], buffer[5]]
166
    if (decode) {
167
        return decodeSize(Buffer.from(decodeBytes))
168
    }
169
    return Buffer.from(decodeBytes).readUIntBE(0, decodeBytes.length)
170
}
171
172
export function parseTagHeaderFlags(header: Buffer) {
173
    if (!(header instanceof Buffer && header.length >= 10)) {
174
        return {}
175
    }
176
    const version = header[3]
177
    const flagsByte = header[5]
178
    if (version === 3) {
179
        return {
180
            unsynchronisation: !!(flagsByte & 128),
181
            extendedHeader: !!(flagsByte & 64),
182
            experimentalIndicator: !!(flagsByte & 32)
183
        }
184
    }
185
    if (version === 4) {
186
        return {
187
            unsynchronisation: !!(flagsByte & 128),
188
            extendedHeader: !!(flagsByte & 64),
189
            experimentalIndicator: !!(flagsByte & 32),
190
            footerPresent: !!(flagsByte & 16)
191
        }
192
    }
193
    return {}
194
}
195
196
export function processUnsynchronisedBuffer(buffer: Buffer) {
197
    const newDataArr = []
198
    if (buffer.length > 0) {
199
        newDataArr.push(buffer[0])
200
    }
201
    for(let i = 1; i < buffer.length; i++) {
202
        if (buffer[i - 1] === 0xFF && buffer[i] === 0x00) {
203
            continue
204
        }
205
        newDataArr.push(buffer[i])
206
    }
207
    return Buffer.from(newDataArr)
208
}
209
210
export function getPictureMimeTypeFromBuffer(pictureBuffer: Buffer) {
211
    if (
212
        pictureBuffer.length > 3 &&
213
        pictureBuffer.compare(Buffer.from([0xff, 0xd8, 0xff]), 0, 3, 0, 3) === 0
214
    ) {
215
        return "image/jpeg"
216
    }
217
    if (
218
        pictureBuffer.length > 8 &&
219
        pictureBuffer.compare(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), 0, 8, 0, 8) === 0
220
    ) {
221
        return "image/png"
222
    }
223
    return null
224
}
225