Completed
Push — master ( 8caa6f...c37a57 )
by Jan
20s queued 13s
created

id3-tag.ts ➔ getId3TagBody   B

Complexity

Conditions 5

Size

Total Lines 33
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 33
rs 8.7653
c 0
b 0
f 0
cc 5
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
        id: 0,
12
        version: 3,
13
        flags: 5,
14
        size: 6
15
    }
16
} as const
17
18
const subarray = (buffer: Buffer, offset: number, size: number) =>
19
    buffer.subarray(offset, offset + size)
20
21
export function createId3Tag(tags: WriteTags) {
22
    const frames = buildFramesBuffer(tags)
23
    return embedFramesInId3Tag(frames)
24
}
25
26
export function embedFramesInId3Tag(frames: Buffer) {
27
    const header = Buffer.alloc(Header.size)
28
    header.fill(0)
29
    header.write(Header.identifier, Header.offset.id)
30
    header.writeUInt16BE(0x0300, Header.offset.version)
31
    header.writeUInt16BE(0x0000, Header.offset.flags)
32
    encodeSize(frames.length).copy(header, Header.offset.size)
33
34
    return Buffer.concat([header, frames])
35
}
36
37
/**
38
 * Remove already written ID3-Frames from a buffer
39
 */
40
export function removeId3Tag(data: Buffer) {
41
    const tagPosition = findId3TagPosition(data)
42
    if (tagPosition === -1) {
43
        return data
44
    }
45
    const encodedSize = subarray(data, tagPosition + Header.offset.size, 4)
46
47
    if (!isValidEncodedSize(encodedSize)) {
48
        return false
49
    }
50
51
    if (data.length >= tagPosition + Header.size) {
52
        const size = decodeSize(encodedSize)
53
        return Buffer.concat([
54
            data.subarray(0, tagPosition),
55
            data.subarray(tagPosition + size + Header.size)
56
        ])
57
    }
58
59
    return data
60
}
61
62
export function getTagsFromId3Tag(buffer: Buffer, options: Options) {
63
    const tagBody = getId3TagBody(buffer)
64
    return getTags(tagBody, options)
65
}
66
67
function getId3TagBody(buffer: Buffer) {
68
    const tagPosition = findId3TagPosition(buffer)
69
    if (tagPosition === -1) {
70
        return undefined
71
    }
72
    const encodedSize = subarray(buffer, tagPosition + Header.offset.size, 4)
73
    const tagSize = Header.size + decodeSize(encodedSize)
74
75
    const tagData = subarray(buffer, tagPosition, tagSize)
76
    const tagHeader = tagData.subarray(0, Header.size)
77
78
    // ID3 version e.g. 3 if ID3v2.3.0
79
    const version = tagHeader[Header.offset.version]
80
    const tagFlags = parseTagHeaderFlags(tagHeader)
81
    let extendedHeaderSize = 0
82
    if (tagFlags.extendedHeader) {
83
        if (version === 3) {
84
            extendedHeaderSize = 4 + tagData.readUInt32BE(Header.size)
85
        } else if(version === 4) {
86
            extendedHeaderSize = decodeSize(subarray(tagData, Header.size, 4))
87
        }
88
    }
89
    const totalHeaderSize = Header.size + extendedHeaderSize
90
    const bodySize = tagSize - totalHeaderSize
91
92
    // Copy for now, it might not be necessary, but we are not really sure for
93
    // now, will be re-assessed if we can avoid the copy.
94
    const body = Buffer.alloc(bodySize)
95
    tagData.copy(body, 0, totalHeaderSize)
96
97
    return {
98
        version, buffer: body
99
    }
100
}
101
102
function parseTagHeaderFlags(header: Buffer) {
103
    if (header.length < Header.size) {
104
        return {}
105
    }
106
    const version = header[3]
107
    const flagsByte = header[5]
108
    if (version === 3) {
109
        return {
110
            unsynchronisation: !!(flagsByte & 128),
111
            extendedHeader: !!(flagsByte & 64),
112
            experimentalIndicator: !!(flagsByte & 32)
113
        }
114
    }
115
    if (version === 4) {
116
        return {
117
            unsynchronisation: !!(flagsByte & 128),
118
            extendedHeader: !!(flagsByte & 64),
119
            experimentalIndicator: !!(flagsByte & 32),
120
            footerPresent: !!(flagsByte & 16)
121
        }
122
    }
123
    return {}
124
}
125
126
/**
127
 * Returns -1 if no tag was found.
128
 */
129
function findId3TagPosition(buffer: Buffer) {
130
    // Search Buffer for valid ID3 frame
131
    let position = -1
132
    let headerValid = false
133
    do {
134
        position = buffer.indexOf(Header.identifier, position + 1)
135
        if (position !== -1) {
136
            // It's possible that there is a "ID3" sequence without being an
137
            // ID3 Frame, so we need to check for validity of the next 10 bytes.
138
            headerValid = isValidId3Header(
139
                buffer.subarray(position, position + Header.size)
140
            )
141
        }
142
    } while (position !== -1 && !headerValid)
143
144
    if (!headerValid) {
145
        return -1
146
    }
147
    return position
148
}
149
150
function isValidId3Header(buffer: Buffer) {
151
    if (buffer.length < Header.size) {
152
        return false
153
    }
154
    if (buffer.readUIntBE(0, 3) !== 0x494433) {
155
        return false
156
    }
157
    if ([0x02, 0x03, 0x04].indexOf(buffer[3]) === -1 || buffer[4] !== 0x00) {
158
        return false
159
    }
160
    return isValidEncodedSize(subarray(buffer, Header.offset.size, 4))
161
}
162
163
function isValidEncodedSize(encodedSize: Buffer) {
164
    // The size must not have the bit 7 set
165
    return ((
166
        encodedSize[0] |
167
        encodedSize[1] |
168
        encodedSize[2] |
169
        encodedSize[3]
170
    ) & 128) === 0
171
}
172