Passed
Pull Request — master (#136)
by
unknown
02:12
created

src/Frame.ts   A

Complexity

Total Complexity 25
Complexity/F 3.57

Size

Lines of Code 144
Function Count 7

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 25
eloc 108
mnd 18
bc 18
fnc 7
dl 0
loc 144
rs 10
bpm 2.5714
cpm 3.5714
noi 0
c 0
b 0
f 0

7 Functions

Rating   Name   Duplication   Size   Complexity  
A Frame.getBuffer 0 12 4
A Frame.ts ➔ decompressBody 0 7 2
B Frame.ts ➔ decompressBuffer 0 32 6
A Frame.ts ➔ getBody 0 11 3
A Frame.getValue 0 3 1
A Frame.ts ➔ getDataLength 0 3 2
B Frame.createFromBuffer 0 40 7
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as Frames from './Frames'
8
import * as ID3Util from './ID3Util'
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(
30
        frameBuffer: Buffer,
31
        version: number
32
    ): Frame | null {
33
        const headerSize = getHeaderSize(version)
34
        // Specification requirement
35
        if (frameBuffer.length < headerSize + 1) {
36
            return null
37
        }
38
        const headerBuffer = frameBuffer.subarray(0, headerSize)
39
        const header: HeaderInfo = {
40
            headerSize,
41
            ...FrameHeader.createFromBuffer(headerBuffer, version)
42
        }
43
        if (header.flags.encryption) {
44
            return null
45
        }
46
47
        const body = decompressBody(
48
            header.flags,
49
            getDataLength(header, frameBuffer),
50
            getBody(header, frameBuffer)
51
        )
52
        if (!body) {
53
            return null
54
        }
55
56
        const identifier = header.identifier
57
        let value = null
58
        if (isKeyOf(identifier, Frames.Frames)) {
59
            value = Frames.Frames[identifier].read(body, version)
60
        } else if (identifier.startsWith('T')) {
61
            value = Frames.GENERIC_TEXT.read(body)
62
        } else if (identifier.startsWith('W')) {
63
            value = Frames.GENERIC_URL.read(body)
64
        } else {
65
            return null
66
        }
67
        return new Frame(identifier, value, header.flags)
68
    }
69
70
    getBuffer() {
71
        if (isKeyOf(this.identifier, Frames.Frames)) {
72
            return Frames.Frames[this.identifier].create(this.value)
73
        }
74
        if (this.identifier.startsWith('T')) {
75
            return Frames.GENERIC_TEXT.create(this.identifier, this.value)
76
        }
77
        if (this.identifier.startsWith('W')) {
78
            return Frames.GENERIC_URL.create(this.identifier, this.value)
79
        }
80
        return null
81
    }
82
83
    getValue() {
84
        return this.value
85
    }
86
}
87
88
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
89
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
90
    const bodyStart = headerSize + bodyOffset
91
    const bodyEnd = bodyStart + bodySize - bodyOffset
92
    const body = buffer.subarray(bodyStart, bodyEnd)
93
    if (flags.unsynchronisation) {
94
        // This method should stay in ID3Util for now because it's also used in the Tag's header which we don't have a class for.
95
        return ID3Util.processUnsynchronisedBuffer(body)
96
    }
97
    return body
98
}
99
100
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
101
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
102
}
103
104
function decompressBody(
105
    {compression}: Flags,
106
    dataLength: number,
107
    body: Buffer
108
) {
109
    return compression ? decompressBuffer(body, dataLength) : body
110
}
111
112
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
113
    if (buffer.length < 5) {
114
        return null
115
    }
116
117
    // ID3 spec defines that compression is stored in ZLIB format,
118
    // but doesn't specify if header is present or not.
119
    // ZLIB has a 2-byte header.
120
    // 1. try if header + body decompression
121
    // 2. else try if header is not stored (assume that all content is deflated "body")
122
    // 3. else try if inflation works if the header is omitted (implementation dependent)
123
    const tryDecompress = () => {
124
        try {
125
            return zlib.inflateSync(buffer)
126
        } catch (error) {
127
            try {
128
                return zlib.inflateRawSync(buffer)
129
            } catch (error) {
130
                try {
131
                    return zlib.inflateRawSync(buffer.subarray(2))
132
                } catch (error) {
133
                    return null
134
                }
135
            }
136
        }
137
    }
138
    const decompressed = tryDecompress()
139
    if (decompressed && decompressed.length === expectedDecompressedLength) {
140
        return decompressed
141
    }
142
    return null
143
}
144