Passed
Pull Request — filestream (#173)
by
unknown
05:02
created

id3-tag.ts ➔ getId3Tag   A

Complexity

Conditions 2

Size

Total Lines 15
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 15
rs 9.7
c 0
b 0
f 0
cc 2
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): Buffer {
89
    // TODO support multiple-frames, improve tests
90
    const tag = getId3Tag(data)
91
    return tag ? Buffer.concat([tag.before, tag.after]) : data
92
}
93
94
export function getTagsFromId3Tag(buffer: Buffer, options: Options) {
95
    const tagBody = getId3TagBody(buffer)
96
    return getTags(tagBody, options)
97
}
98
99
export function getId3TagSize(buffer: Buffer): number {
100
    const encodedSize = subarray(buffer, Header.offset.size, 4)
101
    return Header.size + decodeSize(encodedSize)
102
}
103
104
function getId3TagBody(buffer: Buffer) {
105
    const tagPosition = findId3TagPosition(buffer)
106
    if (tagPosition === -1) {
107
        return undefined
108
    }
109
    const tagBuffer = buffer.subarray(tagPosition)
110
    const tagHeader = decodeId3TagHeader(tagBuffer)
111
    const totalHeaderSize =
112
        Header.size + getExtendedHeaderSize(tagHeader, tagBuffer)
113
    const bodySize = tagHeader.tagSize - totalHeaderSize
114
115
    // Copy for now, it might not be necessary, but we are not really sure for
116
    // now, will be re-assessed if we can avoid the copy.
117
    const body = Buffer.from(subarray(tagBuffer, totalHeaderSize, bodySize))
118
119
    return {
120
        version: tagHeader.version.major,
121
        buffer: body
122
    }
123
}
124
125
/**
126
 * @param tagBuffer A buffer starting with a valid id3 tag header.
127
 * @returns The size of the extended header.
128
 */
129
function getExtendedHeaderSize(header: TagHeader, tagBuffer: Buffer) {
130
    if (header.flags.extendedHeader) {
131
        if (header.version.major === 3) {
132
            return 4 + tagBuffer.readUInt32BE(Header.size)
133
        }
134
        if (header.version.major === 4) {
135
            return decodeSize(subarray(tagBuffer, Header.size, 4))
136
        }
137
    }
138
    return 0
139
}
140
141
/**
142
 * @param tagBuffer A buffer starting with a valid id3 tag header.
143
 * @returns The decoded header.
144
 */
145
function decodeId3TagHeader(tagBuffer: Buffer): TagHeader {
146
    return {
147
        tagSize: decodeId3TagSize(tagBuffer),
148
        version: {
149
            major: tagBuffer[Header.offset.version],
150
            revision: tagBuffer[Header.offset.revision]
151
        },
152
        flags: parseTagHeaderFlags(tagBuffer)
153
    }
154
}
155
156
/**
157
 * @param tagBuffer A buffer starting with a valid id3 tag header.
158
 * @returns The size of tag including the header.
159
 */
160
function decodeId3TagSize(tagBuffer: Buffer) {
161
    const encodedSize = subarray(tagBuffer, Header.offset.size, 4)
162
    return Header.size + decodeSize(encodedSize)
163
}
164
165
function parseTagHeaderFlags(header: Buffer): TagHeaderFlags {
166
    const version = header[Header.offset.version]
167
    const flagsByte = header[Header.offset.flags]
168
    if (version === 3) {
169
        return {
170
            unsynchronisation: !!(flagsByte & 128),
171
            extendedHeader: !!(flagsByte & 64),
172
            experimentalIndicator: !!(flagsByte & 32)
173
        }
174
    }
175
    if (version === 4) {
176
        return {
177
            unsynchronisation: !!(flagsByte & 128),
178
            extendedHeader: !!(flagsByte & 64),
179
            experimentalIndicator: !!(flagsByte & 32),
180
            footerPresent: !!(flagsByte & 16)
181
        }
182
    }
183
    return {
184
        unsynchronisation: false,
185
        extendedHeader: false,
186
        experimentalIndicator: false
187
    }
188
}
189
190
export function getId3Tag(data: Buffer) {
191
    const position = findId3TagPosition(data)
192
    if (position === -1) {
193
        return null
194
    }
195
    const from = data.subarray(position)
196
    const size = getId3TagSize(from)
197
    return {
198
        size,
199
        from,
200
        before: data.subarray(0, position),
201
        data: from.subarray(0, size),
202
        after: from.subarray(size),
203
        missingBytes: Math.max(0, size - from.length)
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