Passed
Pull Request — master (#165)
by
unknown
01:54
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
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
function getId3TagBody(buffer: Buffer) {
116
    const tagPosition = findId3TagPosition(buffer)
117
    if (tagPosition === -1) {
118
        return undefined
119
    }
120
    const tagBuffer = buffer.subarray(tagPosition)
121
    const tagHeader = decodeId3TagHeader(tagBuffer)
122
    const totalHeaderSize =
123
        Header.size + getExtendedHeaderSize(tagHeader, tagBuffer)
124
    const bodySize = tagHeader.tagSize - totalHeaderSize
125
126
    // Copy for now, it might not be necessary, but we are not really sure for
127
    // now, will be re-assessed if we can avoid the copy.
128
    const body = Buffer.alloc(bodySize)
129
    tagBuffer.copy(body, 0, totalHeaderSize)
130
131
    return {
132
        version: tagHeader.version.major,
133
        buffer: body
134
    }
135
}
136
137
/**
138
 * @param tagBuffer A buffer starting with a valid id3 tag header.
139
 * @returns The size of the extended header.
140
 */
141
function getExtendedHeaderSize(header: TagHeader, tagBuffer: Buffer) {
142
    if (header.flags.extendedHeader) {
143
        if (header.version.major === 3) {
144
            return 4 + tagBuffer.readUInt32BE(Header.size)
145
        }
146
        if (header.version.major === 4) {
147
            return decodeSize(subarray(tagBuffer, Header.size, 4))
148
        }
149
    }
150
    return 0
151
}
152
153
/**
154
 * @param tagBuffer A buffer starting with a valid id3 tag header.
155
 * @returns The decoded header.
156
 */
157
function decodeId3TagHeader(tagBuffer: Buffer): TagHeader {
158
    return {
159
        tagSize: decodeId3TagSize(tagBuffer),
160
        version: {
161
            major: tagBuffer[Header.offset.version],
162
            revision: tagBuffer[Header.offset.revision]
163
        },
164
        flags: parseTagHeaderFlags(tagBuffer)
165
    }
166
}
167
168
/**
169
 * @param tagBuffer A buffer starting with a valid id3 tag header.
170
 * @returns The size of tag including the header.
171
 */
172
function decodeId3TagSize(tagBuffer: Buffer) {
173
    const encodedSize = subarray(tagBuffer, Header.offset.size, 4)
174
    return Header.size + decodeSize(encodedSize)
175
}
176
177
function parseTagHeaderFlags(header: Buffer): TagHeaderFlags {
178
    const version = header[Header.offset.version]
179
    const flagsByte = header[Header.offset.flags]
180
    if (version === 3) {
181
        return {
182
            unsynchronisation: !!(flagsByte & 128),
183
            extendedHeader: !!(flagsByte & 64),
184
            experimentalIndicator: !!(flagsByte & 32)
185
        }
186
    }
187
    if (version === 4) {
188
        return {
189
            unsynchronisation: !!(flagsByte & 128),
190
            extendedHeader: !!(flagsByte & 64),
191
            experimentalIndicator: !!(flagsByte & 32),
192
            footerPresent: !!(flagsByte & 16)
193
        }
194
    }
195
    return {
196
        unsynchronisation: false,
197
        extendedHeader: false,
198
        experimentalIndicator: false
199
    }
200
}
201
202
/**
203
 * Returns the position of the first valid tag found or -1 if no tag was found.
204
 */
205
function findId3TagPosition(buffer: Buffer) {
206
    // Search Buffer for valid ID3 frame
207
    let position = -1
208
    do {
209
        position = buffer.indexOf(Header.identifier, position + 1)
210
        if (position !== -1) {
211
            // It's possible that there is a "ID3" sequence without being an
212
            // ID3 Frame, so we need to check for validity of the next 10 bytes.
213
            if (isValidId3Header(buffer.subarray(position))) {
214
                return position
215
            }
216
        }
217
    } while (position !== -1)
218
    return -1
219
}
220
221
function isValidId3Header(buffer: Buffer) {
222
    // From id3.org:
223
    // An ID3v2 tag can be detected with the following pattern:
224
    // $49 44 33 yy yy xx zz zz zz zz
225
    // Where yy is less than $FF, xx is the 'flags' byte and zz is less than
226
    // $80.
227
    if (buffer.length < Header.size) {
228
        return false
229
    }
230
    const identifier = buffer.readUIntBE(Header.offset.identifier, 3)
231
    if (identifier !== 0x494433) {
232
        return false
233
    }
234
    const majorVersion = buffer[Header.offset.version]
235
    const revision = buffer[Header.offset.revision]
236
    if (majorVersion === 0xFF || revision === 0xFF) {
237
        return false
238
    }
239
    // This library currently only handle these versions.
240
    if ([0x02, 0x03, 0x04].indexOf(majorVersion) === -1) {
241
        return false
242
    }
243
    return isValidEncodedSize(subarray(buffer, Header.offset.size, 4))
244
}
245
246
function isValidEncodedSize(encodedSize: Buffer) {
247
    // The size must not have the bit 7 set
248
    return ((
249
        encodedSize[0] |
250
        encodedSize[1] |
251
        encodedSize[2] |
252
        encodedSize[3]
253
    ) & 128) === 0
254
}
255