Passed
Pull Request — master (#160)
by
unknown
01:51
created

src/Frame.ts   A

Complexity

Total Complexity 32
Complexity/F 3.56

Size

Lines of Code 172
Function Count 9

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 32
eloc 128
mnd 23
bc 23
fnc 9
dl 0
loc 172
rs 9.84
bpm 2.5555
cpm 3.5555
noi 0
c 0
b 0
f 0

9 Functions

Rating   Name   Duplication   Size   Complexity  
A Frame.getValue 0 3 1
A Frame.ts ➔ decompressBody 0 7 2
A Frame.ts ➔ makeFrameValue 0 17 5
A Frame.ts ➔ processUnsynchronisedBuffer 0 13 4
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
B Frame.ts ➔ decompressBuffer 0 21 7
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 { isKeyOf } from "./util"
10
11
type HeaderInfo = {
12
    identifier: string
13
    headerSize: number
14
    bodySize: number
15
    flags: Flags
16
}
17
18
export class Frame {
19
    identifier: string
20
    private value: unknown
21
    flags: Flags
22
23
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
24
        this.identifier = identifier
25
        this.value = value
26
        this.flags = flags
27
    }
28
29
    static createFromBuffer = createFromBuffer
30
31
    getValue() {
32
        return this.value
33
    }
34
}
35
36
type FrameData = {
37
    header: HeaderInfo
38
    body: Buffer
39
}
40
41
function getFrameDataFromFrameBuffer(
42
    frameBuffer: Buffer,
43
    version: number
44
): FrameData | null {
45
    const headerSize = getHeaderSize(version)
46
    // Specification requirement
47
    if (frameBuffer.length < headerSize + 1) {
48
        return null
49
    }
50
    const headerBuffer = frameBuffer.subarray(0, headerSize)
51
    const header: HeaderInfo = {
52
        headerSize,
53
        ...FrameHeader.createFromBuffer(headerBuffer, version)
54
    }
55
    if (header.flags.encryption) {
56
        return null
57
    }
58
59
    const body = decompressBody(
60
        header.flags,
61
        getDataLength(header, frameBuffer),
62
        getBody(header, frameBuffer)
63
    )
64
    if (!body) {
65
        return null
66
    }
67
    return { header, body }
68
}
69
70
function createFromBuffer(
71
    frameBuffer: Buffer,
72
    version: number
73
): Frame | null {
74
    const frameData = getFrameDataFromFrameBuffer(frameBuffer, version)
75
    if (!frameData) {
76
        return null
77
    }
78
    const { header, body } = frameData
79
    const value = makeFrameValue(header.identifier, body, version)
80
    if (!value) {
81
        return null
82
    }
83
    return new Frame(header.identifier, value, header.flags)
84
}
85
86
function makeFrameValue(identifier:string, body: Buffer, version: number) {
87
    try {
88
        if (isKeyOf(identifier, Frames)) {
89
            return Frames[identifier].read(body, version)
90
        }
91
        if (identifier.startsWith('T')) {
92
            return GenericFrames.GENERIC_TEXT.read(body)
93
        }
94
        if (identifier.startsWith('W')) {
95
            return GenericFrames.GENERIC_URL.read(body)
96
        }
97
    } catch(error) {
98
        // On read ignore frames with errors
99
        return null
100
    }
101
    return null
102
}
103
104
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
105
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
106
    const bodyStart = headerSize + bodyOffset
107
    const bodyEnd = bodyStart + bodySize - bodyOffset
108
    const body = buffer.subarray(bodyStart, bodyEnd)
109
    if (flags.unsynchronisation) {
110
        // This method should stay in ID3Util for now because it's also used
111
        // in the Tag's header which we don't have a class for.
112
        return processUnsynchronisedBuffer(body)
113
    }
114
    return body
115
}
116
117
function processUnsynchronisedBuffer(buffer: Buffer) {
118
    const newDataArr = []
119
    if (buffer.length > 0) {
120
        newDataArr.push(buffer[0])
121
    }
122
    for(let i = 1; i < buffer.length; i++) {
123
        if (buffer[i - 1] === 0xFF && buffer[i] === 0x00) {
124
            continue
125
        }
126
        newDataArr.push(buffer[i])
127
    }
128
    return Buffer.from(newDataArr)
129
}
130
131
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
132
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
133
}
134
135
function decompressBody(
136
    {compression}: Flags,
137
    dataLength: number,
138
    body: Buffer
139
) {
140
    return compression ? decompressBuffer(body, dataLength) : body
141
}
142
143
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
144
    if (buffer.length < 5) {
145
        return null
146
    }
147
148
    // ID3 spec defines that compression is stored in ZLIB format,
149
    // but doesn't specify if header is present or not.
150
    // ZLIB has a 2-byte header.
151
    // 1. try if header + body decompression
152
    // 2. else try if header is not stored (assume that all content is deflated "body")
153
    // 3. else try if inflation works if the header is omitted (implementation dependent)
154
    const decompressed = (
155
        tryFunc(() => zlib.inflateSync(buffer)) ??
156
        tryFunc(() => zlib.inflateRawSync(buffer)) ??
157
        tryFunc(() => zlib.inflateRawSync(buffer.subarray(2)))
158
    )
159
    if (decompressed && decompressed.length === expectedDecompressedLength) {
160
        return decompressed
161
    }
162
    return null
163
}
164
165
const tryFunc = (func: () => Buffer) => {
166
    try {
167
        return func()
168
    } catch(error) {
169
        return null
170
    }
171
}
172