Completed
Push — master ( ff6124...8caa6f )
by Jan
23s queued 12s
created

src/Frame.ts   A

Complexity

Total Complexity 35
Complexity/F 3.18

Size

Lines of Code 213
Function Count 11

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 35
eloc 156
mnd 24
bc 24
fnc 11
dl 0
loc 213
rs 9.6
bpm 2.1818
cpm 3.1818
noi 0
c 0
b 0
f 0

11 Functions

Rating   Name   Duplication   Size   Complexity  
A Frame.getValue 0 3 1
A Frame.ts ➔ makeFrameBuffer 0 21 4
A Frame.ts ➔ decompressBody 0 7 2
A Frame.ts ➔ makeValueArray 0 12 3
A Frame.ts ➔ handleMultipleAndMakeFrameBuffer 0 15 2
B Frame.ts ➔ decompressBuffer 0 32 6
A Frame.ts ➔ makeFrameValue 0 17 5
A Frame.ts ➔ getBody 0 12 3
A Frame.ts ➔ getDataLength 0 3 2
A Frame.ts ➔ getFrameDataFromFrameBuffer 0 28 4
A Frame.ts ➔ createFromBuffer 0 15 3
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as GenericFrames from './frames/generic'
8
import { Frames } from './frames/frames'
9
import * as ID3Util from './ID3Util'
10
import { deduplicate, isBuffer, isKeyOf } from "./util"
11
12
type HeaderInfo = {
13
    identifier: string
14
    headerSize: number
15
    bodySize: number
16
    flags: Flags
17
}
18
19
export class Frame {
20
    identifier: string
21
    private value: unknown
22
    flags: Flags
23
24
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
25
        this.identifier = identifier
26
        this.value = value
27
        this.flags = flags
28
    }
29
30
    static createFromBuffer = createFromBuffer
31
32
    getValue() {
33
        return this.value
34
    }
35
}
36
37
type FrameData = {
38
    header: HeaderInfo
39
    body: Buffer
40
}
41
42
function getFrameDataFromFrameBuffer(
43
    frameBuffer: Buffer,
44
    version: number
45
): FrameData | null {
46
    const headerSize = getHeaderSize(version)
47
    // Specification requirement
48
    if (frameBuffer.length < headerSize + 1) {
49
        return null
50
    }
51
    const headerBuffer = frameBuffer.subarray(0, headerSize)
52
    const header: HeaderInfo = {
53
        headerSize,
54
        ...FrameHeader.createFromBuffer(headerBuffer, version)
55
    }
56
    if (header.flags.encryption) {
57
        return null
58
    }
59
60
    const body = decompressBody(
61
        header.flags,
62
        getDataLength(header, frameBuffer),
63
        getBody(header, frameBuffer)
64
    )
65
    if (!body) {
66
        return null
67
    }
68
    return { header, body }
69
}
70
71
function createFromBuffer(
72
    frameBuffer: Buffer,
73
    version: number
74
): Frame | null {
75
    const frameData = getFrameDataFromFrameBuffer(frameBuffer, version)
76
    if (!frameData) {
77
        return null
78
    }
79
    const { header, body } = frameData
80
    const value = makeFrameValue(header.identifier, body, version)
81
    if (!value) {
82
        return null
83
    }
84
    return new Frame(header.identifier, value, header.flags)
85
}
86
87
export function makeFrameBuffer(identifier: string, value: unknown) {
88
    if (isKeyOf(identifier, Frames)) {
89
        return handleMultipleAndMakeFrameBuffer(
90
            identifier,
91
            value,
92
            Frames[identifier].create
93
        )
94
    }
95
    if (identifier.startsWith('T')) {
96
        return GenericFrames.GENERIC_TEXT.create(identifier, value as string)
97
    }
98
    if (identifier.startsWith('W')) {
99
        return handleMultipleAndMakeFrameBuffer(
100
            identifier,
101
            value,
102
            url => GenericFrames.GENERIC_URL.create(identifier, url),
103
            deduplicate
104
        )
105
    }
106
    return null
107
}
108
109
function handleMultipleAndMakeFrameBuffer<
110
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
    Create extends (value: any, index: number) => Buffer | null
112
>(
113
    identifier: string,
114
    data: unknown,
115
    create: Create,
116
    deduplicate = (values: ([Parameters<Create>])[]) => values
117
) {
118
    const values = makeValueArray(identifier, data)
119
    const frames = deduplicate(values)
120
        .map(create)
121
        .filter(isBuffer)
122
    return frames.length ? Buffer.concat(frames) : null
123
}
124
125
/**
126
 * Throws if an array is given but not expected, i.e. the contract is not
127
 * respected, otherwise always return an array.
128
 */
129
function makeValueArray(identifier: string, data: unknown) {
130
    const isMultiple = ID3Util.getSpecOptions(identifier).multiple
131
    const isArray = Array.isArray(data)
132
    if (!isMultiple && isArray) {
133
        throw new TypeError(`Unexpected array for frame ${identifier}`)
134
    }
135
    return isMultiple && isArray ? data : [data]
136
}
137
138
function makeFrameValue(identifier:string, body: Buffer, version: number) {
139
    try {
140
        if (isKeyOf(identifier, Frames)) {
141
            return Frames[identifier].read(body, version)
142
        }
143
        if (identifier.startsWith('T')) {
144
            return GenericFrames.GENERIC_TEXT.read(body)
145
        }
146
        if (identifier.startsWith('W')) {
147
            return GenericFrames.GENERIC_URL.read(body)
148
        }
149
    } catch(error) {
150
        // On read ignore frames with errors
151
        return null
152
    }
153
    return null
154
}
155
156
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
157
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
158
    const bodyStart = headerSize + bodyOffset
159
    const bodyEnd = bodyStart + bodySize - bodyOffset
160
    const body = buffer.subarray(bodyStart, bodyEnd)
161
    if (flags.unsynchronisation) {
162
        // This method should stay in ID3Util for now because it's also used
163
        // in the Tag's header which we don't have a class for.
164
        return ID3Util.processUnsynchronisedBuffer(body)
165
    }
166
    return body
167
}
168
169
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
170
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
171
}
172
173
function decompressBody(
174
    {compression}: Flags,
175
    dataLength: number,
176
    body: Buffer
177
) {
178
    return compression ? decompressBuffer(body, dataLength) : body
179
}
180
181
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
182
    if (buffer.length < 5) {
183
        return null
184
    }
185
186
    // ID3 spec defines that compression is stored in ZLIB format,
187
    // but doesn't specify if header is present or not.
188
    // ZLIB has a 2-byte header.
189
    // 1. try if header + body decompression
190
    // 2. else try if header is not stored (assume that all content is deflated "body")
191
    // 3. else try if inflation works if the header is omitted (implementation dependent)
192
    const tryDecompress = () => {
193
        try {
194
            return zlib.inflateSync(buffer)
195
        } catch (error) {
196
            try {
197
                return zlib.inflateRawSync(buffer)
198
            } catch (error) {
199
                try {
200
                    return zlib.inflateRawSync(buffer.subarray(2))
201
                } catch (error) {
202
                    return null
203
                }
204
            }
205
        }
206
    }
207
    const decompressed = tryDecompress()
208
    if (decompressed && decompressed.length === expectedDecompressedLength) {
209
        return decompressed
210
    }
211
    return null
212
}
213