Passed
Pull Request — master (#136)
by
unknown
01:50
created

ID3Frame.ts ➔ decompressBuffer   B

Complexity

Conditions 6

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
dl 0
loc 33
rs 8.4666
c 0
b 0
f 0
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as ID3Frames from './ID3Frames'
8
import * as ID3Util from './ID3Util'
9
10
type FrameKeys = keyof typeof ID3Frames.Frames
11
12
export class ID3Frame {
13
    private identifier: string
14
    private value: unknown
15
    flags: Flags
16
17
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
18
        this.identifier = identifier
19
        this.value = value
20
        this.flags = flags
21
    }
22
23
    static createFromBuffer(
24
        frameBuffer: Buffer,
25
        version: number
26
    ): ID3Frame | null {
27
        const frameHeaderSize = getHeaderSize(version)
28
        // Specification requirement
29
        if (frameBuffer.length < frameHeaderSize + 1) {
30
            return null
31
        }
32
        const frameHeaderBuffer = frameBuffer.subarray(0, frameHeaderSize)
33
        const frameHeader = FrameHeader.createFromBuffer(
34
            frameHeaderBuffer, version
35
        )
36
        if (frameHeader.flags.encryption) {
37
            return null
38
        }
39
40
        const frameBodyOffset = frameHeader.flags.dataLengthIndicator ? 4 : 0
41
        const frameBodyStart = frameHeaderSize + frameBodyOffset
42
        let frameBody = frameBuffer.subarray(frameBodyStart, frameBodyStart + frameHeader.bodySize - frameBodyOffset)
43
        if (frameHeader.flags.unsynchronisation) {
44
            // 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.
45
            frameBody = ID3Util.processUnsynchronisedBuffer(frameBody)
46
        }
47
48
        const decompressedFrameBody = decompressBody(
49
            frameHeader.flags, frameBuffer, frameHeaderSize, frameBody
50
        )
51
        if (!decompressedFrameBody) {
52
            return null
53
        }
54
        frameBody = decompressedFrameBody
55
56
        const identifier = frameHeader.identifier
57
        const Frames = ID3Frames.Frames
58
        let value = null
59
        if (identifier in Frames) {
60
            value = Frames[identifier as FrameKeys].read(frameBody, version)
61
        } else if (identifier.startsWith('T')) {
62
            value = ID3Frames.GENERIC_TEXT.read(frameBody)
63
        } else if (identifier.startsWith('W')) {
64
            value = ID3Frames.GENERIC_URL.read(frameBody)
65
        } else {
66
            return null
67
        }
68
        return new ID3Frame(identifier, value, frameHeader.flags)
69
    }
70
71
    getBuffer() {
72
        const Frames = ID3Frames.Frames
73
        if (this.identifier in Frames) {
74
            return Frames[this.identifier as FrameKeys].create(this.value)
75
        }
76
        if (this.identifier.startsWith('T')) {
77
            return ID3Frames.GENERIC_TEXT.create(this.identifier, this.value)
78
        }
79
        if (this.identifier.startsWith('W')) {
80
            return ID3Frames.GENERIC_URL.create(this.identifier, this.value)
81
        }
82
        return null
83
    }
84
85
    getValue() {
86
        return this.value
87
    }
88
}
89
90
function decompressBody(
91
    flags: Flags,
92
    frameBuffer: Buffer,
93
    dataLengthOffset: number,
94
    frameBody: Buffer
95
) {
96
    let dataLength = 0
97
    if (flags.dataLengthIndicator) {
98
        dataLength = frameBuffer.readInt32BE(dataLengthOffset)
99
    }
100
    if (flags.compression) {
101
        return decompressBuffer(frameBody, dataLength)
102
    }
103
    return frameBody
104
}
105
106
107
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
108
    if (buffer.length < 5 || expectedDecompressedLength === undefined) {
109
        return null
110
    }
111
112
    // ID3 spec defines that compression is stored in ZLIB format,
113
    // but doesn't specify if header is present or not.
114
    // ZLIB has a 2-byte header.
115
    // 1. try if header + body decompression
116
    // 2. else try if header is not stored (assume that all content is deflated "body")
117
    // 3. else try if inflation works if the header is omitted (implementation dependent)
118
    const tryDecompress = () => {
119
        try {
120
            return zlib.inflateSync(buffer)
121
        } catch (error) {
122
            try {
123
                return zlib.inflateRawSync(buffer)
124
            } catch (error) {
125
                try {
126
                    return zlib.inflateRawSync(buffer.subarray(2))
127
                } catch (error) {
128
                    return null
129
                }
130
            }
131
        }
132
    }
133
    const decompressed = tryDecompress()
134
    if (!decompressed || decompressed.length !== expectedDecompressedLength) {
135
        return null
136
    }
137
    return decompressed
138
}
139