Completed
Push — master ( d25395...d83c14 )
by Jan
15s queued 14s
created

Frame.ts ➔ decompressBuffer   B

Complexity

Conditions 6

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 32
rs 8.4666
c 0
b 0
f 0
cc 6
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 { deduplicate, isBuffer, 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
function createFromBuffer(
37
    frameBuffer: Buffer,
38
    version: number
39
): Frame | null {
40
    const headerSize = getHeaderSize(version)
41
    // Specification requirement
42
    if (frameBuffer.length < headerSize + 1) {
43
        return null
44
    }
45
    const headerBuffer = frameBuffer.subarray(0, headerSize)
46
    const header: HeaderInfo = {
47
        headerSize,
48
        ...FrameHeader.createFromBuffer(headerBuffer, version)
49
    }
50
    if (header.flags.encryption) {
51
        return null
52
    }
53
54
    const body = decompressBody(
55
        header.flags,
56
        getDataLength(header, frameBuffer),
57
        getBody(header, frameBuffer)
58
    )
59
    if (body) {
60
        const value = makeFrameValue(header.identifier, body, version)
61
        if (value) {
62
            return new Frame(header.identifier, value, header.flags)
63
        }
64
    }
65
    return null
66
}
67
68
export function makeFrameBuffer(identifier: string, value: unknown) {
69
    if (isKeyOf(identifier, Frames.Frames)) {
70
        return Frames.Frames[identifier].create(value)
71
    }
72
    if (identifier.startsWith('T')) {
73
        return Frames.GENERIC_TEXT.create(identifier, value)
74
    }
75
    if (identifier.startsWith('W')) {
76
        return makeUrlBuffer(identifier, value)
77
    }
78
    return null
79
}
80
81
function makeUrlBuffer(identifier: string, value: unknown) {
82
    const values =
83
        ID3Util.getSpecOptions(identifier).multiple && Array.isArray(value)
84
        ? value : [value]
85
86
    const frames =
87
        deduplicate(values)
88
        .map(url => Frames.GENERIC_URL.create(identifier, url))
89
        .filter(isBuffer)
90
91
    return frames.length ? Buffer.concat(frames) : null
92
}
93
94
function makeFrameValue(identifier:string, body: Buffer, version: number) {
95
    if (isKeyOf(identifier, Frames.Frames)) {
96
        return Frames.Frames[identifier].read(body, version)
97
    }
98
    if (identifier.startsWith('T')) {
99
        return Frames.GENERIC_TEXT.read(body)
100
    }
101
    if (identifier.startsWith('W')) {
102
        return Frames.GENERIC_URL.read(body)
103
    }
104
    return null
105
}
106
107
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
108
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
109
    const bodyStart = headerSize + bodyOffset
110
    const bodyEnd = bodyStart + bodySize - bodyOffset
111
    const body = buffer.subarray(bodyStart, bodyEnd)
112
    if (flags.unsynchronisation) {
113
        // 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.
114
        return ID3Util.processUnsynchronisedBuffer(body)
115
    }
116
    return body
117
}
118
119
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
120
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
121
}
122
123
function decompressBody(
124
    {compression}: Flags,
125
    dataLength: number,
126
    body: Buffer
127
) {
128
    return compression ? decompressBuffer(body, dataLength) : body
129
}
130
131
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
132
    if (buffer.length < 5) {
133
        return null
134
    }
135
136
    // ID3 spec defines that compression is stored in ZLIB format,
137
    // but doesn't specify if header is present or not.
138
    // ZLIB has a 2-byte header.
139
    // 1. try if header + body decompression
140
    // 2. else try if header is not stored (assume that all content is deflated "body")
141
    // 3. else try if inflation works if the header is omitted (implementation dependent)
142
    const tryDecompress = () => {
143
        try {
144
            return zlib.inflateSync(buffer)
145
        } catch (error) {
146
            try {
147
                return zlib.inflateRawSync(buffer)
148
            } catch (error) {
149
                try {
150
                    return zlib.inflateRawSync(buffer.subarray(2))
151
                } catch (error) {
152
                    return null
153
                }
154
            }
155
        }
156
    }
157
    const decompressed = tryDecompress()
158
    if (decompressed && decompressed.length === expectedDecompressedLength) {
159
        return decompressed
160
    }
161
    return null
162
}
163