Passed
Push — filestream ( a348a7...184865 )
by Jan
01:53
created

id3-tag.ts ➔ decodeId3TagHeader   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 1
1
import { buildFramesBuffer } from "./frames-builder"
2
import { getTags } from "./frames-reader"
3
import { Options } from "./types/Options"
4
import { WriteTags } from "./types/Tags"
5
import { decodeSize, encodeSize } from "./util-size"
6
7
export const Header = {
8
    identifier: "ID3",
9
    size: 10,
10
    offset: {
11
        identifier: 0,  // 3 bytes
12
        version: 3,     // major version: 1 byte
13
        revision: 4,    // 1 byte
14
        flags: 5,       // 1 byte
15
        size: 6         // 4 bytes
16
    }
17
} as const
18
19
/**
20
 * ID3v2 Header
21
 */
22
type TagHeader = {
23
    /**
24
     * The sum of the header size and the tag data size.
25
     */
26
    tagSize: number
27
    /**
28
     * Version format is v2.major.revision
29
     */
30
    version: {
31
        /**
32
         * Major versions are not backwards compatible.
33
         */
34
        major: number
35
        /**
36
         * Revisions are backwards compatible.
37
         */
38
        revision: number
39
    }
40
    flags: TagHeaderFlags
41
}
42
43
type TagHeaderFlags = {
44
    /**
45
     * Indicates whether or not unsynchronisation is applied on all frames
46
     * (see section 6.1 for details); true indicates usage.
47
     */
48
    unsynchronisation: boolean
49
    /**
50
     * Indicates whether or not the header is followed by an extended
51
     * header. The extended header is described in section 3.2.
52
     * True indicates the presence of an extended header.
53
     */
54
    extendedHeader: boolean
55
    /**
56
     * This flag SHALL always be set when the tag is in an experimental stage.
57
     */
58
    experimentalIndicator: boolean
59
    /**
60
     * A footer (section 3.4) is present at the very end of the tag.
61
     * True indicates the presence of a footer.
62
     */
63
    footerPresent?: boolean
64
}
65
66
const subarray = (buffer: Buffer, offset: number, size: number) =>
67
    buffer.subarray(offset, offset + size)
68
69
export function createId3Tag(tags: WriteTags) {
70
    const frames = buildFramesBuffer(tags)
71
    return embedFramesInId3Tag(frames)
72
}
73
74
export function embedFramesInId3Tag(frames: Buffer) {
75
    const header = Buffer.alloc(Header.size)
76
    header.fill(0)
77
    header.write(Header.identifier, Header.offset.identifier)
78
    header.writeUInt16BE(0x0300, Header.offset.version)
79
    header.writeUInt16BE(0x0000, Header.offset.flags)
80
    encodeSize(frames.length).copy(header, Header.offset.size)
81
82
    return Buffer.concat([header, frames])
83
}
84
85
/**
86
 * Remove already written ID3-Frames from a buffer
87
 */
88
export function removeId3Tag(data: Buffer) {
89
    const tagPosition = findId3TagPosition(data)
90
    if (tagPosition === -1) {
91
        return data
92
    }
93
    const encodedSize = subarray(data, tagPosition + Header.offset.size, 4)
94
95
    if (!isValidEncodedSize(encodedSize)) {
96
        return false
97
    }
98
99
    if (data.length >= tagPosition + Header.size) {
100
        const size = decodeSize(encodedSize)
101
        return Buffer.concat([
102
            data.subarray(0, tagPosition),
103
            data.subarray(tagPosition + size + Header.size)
104
        ])
105
    }
106
107
    return data
108
}
109
110
export function getTagsFromId3Tag(buffer: Buffer, options: Options) {
111
    const tagBody = getId3TagBody(buffer)
112
    return getTags(tagBody, options)
113
}
114
115
export function getId3TagSize(buffer: Buffer): number {
116
    const encodedSize = subarray(buffer, Header.offset.size, 4)
117
    return Header.size + decodeSize(encodedSize)
118
}
119
120
function getId3TagBody(buffer: Buffer) {
121
    const tagPosition = findId3TagPosition(buffer)
122
    if (tagPosition === -1) {
123
        return undefined
124
    }
125
    const tagBuffer = buffer.subarray(tagPosition)
126
    const tagHeader = decodeId3TagHeader(tagBuffer)
127
    const totalHeaderSize =
128
        Header.size + getExtendedHeaderSize(tagHeader, tagBuffer)
129
    const bodySize = tagHeader.tagSize - totalHeaderSize
130
131
    // Copy for now, it might not be necessary, but we are not really sure for
132
    // now, will be re-assessed if we can avoid the copy.
133
    const body = Buffer.alloc(bodySize)
134
    tagBuffer.copy(body, 0, totalHeaderSize)
135
136
    return {
137
        version: tagHeader.version.major,
138
        buffer: body
139
    }
140
}
141
142
/**
143
 * @param tagBuffer A buffer starting with a valid id3 tag header.
144
 * @returns The size of the extended header.
145
 */
146
function getExtendedHeaderSize(header: TagHeader, tagBuffer: Buffer) {
147
    if (header.flags.extendedHeader) {
148
        if (header.version.major === 3) {
149
            return 4 + tagBuffer.readUInt32BE(Header.size)
150
        }
151
        if (header.version.major === 4) {
152
            return decodeSize(subarray(tagBuffer, Header.size, 4))
153
        }
154
    }
155
    return 0
156
}
157
158
/**
159
 * @param tagBuffer A buffer starting with a valid id3 tag header.
160
 * @returns The decoded header.
161
 */
162
function decodeId3TagHeader(tagBuffer: Buffer): TagHeader {
163
    return {
164
        tagSize: decodeId3TagSize(tagBuffer),
165
        version: {
166
            major: tagBuffer[Header.offset.version],
167
            revision: tagBuffer[Header.offset.revision]
168
        },
169
        flags: parseTagHeaderFlags(tagBuffer)
170
    }
171
}
172
173
/**
174
 * @param tagBuffer A buffer starting with a valid id3 tag header.
175
 * @returns The size of tag including the header.
176
 */
177
function decodeId3TagSize(tagBuffer: Buffer) {
178
    const encodedSize = subarray(tagBuffer, Header.offset.size, 4)
179
    return Header.size + decodeSize(encodedSize)
180
}
181
182
function parseTagHeaderFlags(header: Buffer): TagHeaderFlags {
183
    const version = header[Header.offset.version]
184
    const flagsByte = header[Header.offset.flags]
185
    if (version === 3) {
186
        return {
187
            unsynchronisation: !!(flagsByte & 128),
188
            extendedHeader: !!(flagsByte & 64),
189
            experimentalIndicator: !!(flagsByte & 32)
190
        }
191
    }
192
    if (version === 4) {
193
        return {
194
            unsynchronisation: !!(flagsByte & 128),
195
            extendedHeader: !!(flagsByte & 64),
196
            experimentalIndicator: !!(flagsByte & 32),
197
            footerPresent: !!(flagsByte & 16)
198
        }
199
    }
200
    return {
201
        unsynchronisation: false,
202
        extendedHeader: false,
203
        experimentalIndicator: false
204
    }
205
}
206
207
/**
208
 * Returns the position of the first valid tag found or -1 if no tag was found.
209
 */
210
export function findId3TagPosition(buffer: Buffer) {
211
    // Search Buffer for valid ID3 frame
212
    let position = -1
213
    do {
214
        position = buffer.indexOf(Header.identifier, position + 1)
215
        if (position !== -1) {
216
            // It's possible that there is a "ID3" sequence without being an
217
            // ID3 Frame, so we need to check for validity of the next 10 bytes.
218
            if (isValidId3Header(buffer.subarray(position))) {
219
                return position
220
            }
221
        }
222
    } while (position !== -1)
223
    return -1
224
}
225
226
function isValidId3Header(buffer: Buffer) {
227
    // From id3.org:
228
    // An ID3v2 tag can be detected with the following pattern:
229
    // $49 44 33 yy yy xx zz zz zz zz
230
    // Where yy is less than $FF, xx is the 'flags' byte and zz is less than
231
    // $80.
232
    if (buffer.length < Header.size) {
233
        return false
234
    }
235
    const identifier = buffer.readUIntBE(Header.offset.identifier, 3)
236
    if (identifier !== 0x494433) {
237
        return false
238
    }
239
    const majorVersion = buffer[Header.offset.version]
240
    const revision = buffer[Header.offset.revision]
241
    if (majorVersion === 0xFF || revision === 0xFF) {
242
        return false
243
    }
244
    // This library currently only handle these versions.
245
    if ([0x02, 0x03, 0x04].indexOf(majorVersion) === -1) {
246
        return false
247
    }
248
    return isValidEncodedSize(subarray(buffer, Header.offset.size, 4))
249
}
250
251
function isValidEncodedSize(encodedSize: Buffer) {
252
    // The size must not have the bit 7 set
253
    return ((
254
        encodedSize[0] |
255
        encodedSize[1] |
256
        encodedSize[2] |
257
        encodedSize[3]
258
    ) & 128) === 0
259
}
260