Passed
Pull Request — master (#157)
by
unknown
02:09
created

Frame.ts ➔ makeValueArray   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
c 0
b 0
f 0
rs 10
cc 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
function createFromBuffer(
38
    frameBuffer: Buffer,
39
    version: number
40
): Frame | null {
41
    const headerSize = getHeaderSize(version)
42
    // Specification requirement
43
    if (frameBuffer.length < headerSize + 1) {
44
        return null
45
    }
46
    const headerBuffer = frameBuffer.subarray(0, headerSize)
47
    const header: HeaderInfo = {
48
        headerSize,
49
        ...FrameHeader.createFromBuffer(headerBuffer, version)
50
    }
51
    if (header.flags.encryption) {
52
        return null
53
    }
54
55
    const body = decompressBody(
56
        header.flags,
57
        getDataLength(header, frameBuffer),
58
        getBody(header, frameBuffer)
59
    )
60
    if (body) {
61
        const value = makeFrameValue(header.identifier, body, version)
62
        if (value) {
63
            return new Frame(header.identifier, value, header.flags)
64
        }
65
    }
66
    return null
67
}
68
69
export function makeFrameBuffer(identifier: string, value: unknown) {
70
    if (isKeyOf(identifier, Frames)) {
71
        return handleMultipleAndMakeFrameBuffer(
72
            identifier,
73
            value,
74
            Frames[identifier].create
75
        )
76
    }
77
    if (identifier.startsWith('T')) {
78
        return GenericFrames.GENERIC_TEXT.create(identifier, value)
79
    }
80
    if (identifier.startsWith('W')) {
81
        return handleMultipleAndMakeFrameBuffer(
82
            identifier,
83
            value,
84
            url => GenericFrames.GENERIC_URL.create(identifier, url),
85
            deduplicate
86
        )
87
    }
88
    return null
89
}
90
91
function handleMultipleAndMakeFrameBuffer(
92
    identifier: string,
93
    data: unknown,
94
    create: (value: unknown, index: number) => Buffer | null,
95
    deduplicate = (values: unknown[]) => values
96
) {
97
    const values = makeValueArray(identifier, data)
98
    if (!values) {
99
        return null
100
    }
101
    const frames = deduplicate(values)
102
        .map(create)
103
        .filter(isBuffer)
104
105
    return frames.length ? Buffer.concat(frames) : null
106
}
107
108
/**
109
 * When an array is given but not expected, i.e. the contract is not
110
 * respected, returns null to silently ignore the data, previously the
111
 * behaviour was undefined.
112
 */
113
function makeValueArray(identifier: string, data: unknown) {
114
    const isMultiple = ID3Util.getSpecOptions(identifier).multiple
115
    const isArray = Array.isArray(data)
116
    if (!isMultiple && isArray) {
117
        return null
118
    }
119
    return isMultiple && isArray ? data : [data]
120
}
121
122
function makeFrameValue(identifier:string, body: Buffer, version: number) {
123
    if (isKeyOf(identifier, Frames)) {
124
        return Frames[identifier].read(body, version)
125
    }
126
    if (identifier.startsWith('T')) {
127
        return GenericFrames.GENERIC_TEXT.read(body)
128
    }
129
    if (identifier.startsWith('W')) {
130
        return GenericFrames.GENERIC_URL.read(body)
131
    }
132
    return null
133
}
134
135
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
136
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
137
    const bodyStart = headerSize + bodyOffset
138
    const bodyEnd = bodyStart + bodySize - bodyOffset
139
    const body = buffer.subarray(bodyStart, bodyEnd)
140
    if (flags.unsynchronisation) {
141
        // This method should stay in ID3Util for now because it's also used
142
        // in the Tag's header which we don't have a class for.
143
        return ID3Util.processUnsynchronisedBuffer(body)
144
    }
145
    return body
146
}
147
148
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
149
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
150
}
151
152
function decompressBody(
153
    {compression}: Flags,
154
    dataLength: number,
155
    body: Buffer
156
) {
157
    return compression ? decompressBuffer(body, dataLength) : body
158
}
159
160
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
161
    if (buffer.length < 5) {
162
        return null
163
    }
164
165
    // ID3 spec defines that compression is stored in ZLIB format,
166
    // but doesn't specify if header is present or not.
167
    // ZLIB has a 2-byte header.
168
    // 1. try if header + body decompression
169
    // 2. else try if header is not stored (assume that all content is deflated "body")
170
    // 3. else try if inflation works if the header is omitted (implementation dependent)
171
    const tryDecompress = () => {
172
        try {
173
            return zlib.inflateSync(buffer)
174
        } catch (error) {
175
            try {
176
                return zlib.inflateRawSync(buffer)
177
            } catch (error) {
178
                try {
179
                    return zlib.inflateRawSync(buffer.subarray(2))
180
                } catch (error) {
181
                    return null
182
                }
183
            }
184
        }
185
    }
186
    const decompressed = tryDecompress()
187
    if (decompressed && decompressed.length === expectedDecompressedLength) {
188
        return decompressed
189
    }
190
    return null
191
}
192