• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benrr101 / node-taglib-sharp / 46279786

pending completion
46279786

push

appveyor

GitHub
<a href="https://github.com/benrr101/node-taglib-sharp/commit/<a class=hub.com/benrr101/node-taglib-sharp/commit/2fc21b80185f8e9d20a53e32016042cf735dc642">2fc21b801<a href="https://github.com/benrr101/node-taglib-sharp/commit/2fc21b80185f8e9d20a53e32016042cf735dc642">">Merge </a><a class="double-link" href="https://github.com/benrr101/node-taglib-sharp/commit/<a class="double-link" href="https://github.com/benrr101/node-taglib-sharp/commit/e9b93dc82206d0bb84208e370ecfc11af42e87a6">e9b93dc82</a>">e9b93dc82</a><a href="https://github.com/benrr101/node-taglib-sharp/commit/2fc21b80185f8e9d20a53e32016042cf735dc642"> into a3adc246c">a3adc246c</a>

3096 of 3788 branches covered (81.73%)

Branch coverage included in aggregate %.

2090 of 2090 new or added lines in 30 files covered. (100.0%)

25316 of 26461 relevant lines covered (95.67%)

437.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.04
/src/ebml/ebmlParser.ts
1
import EbmlElement from "./ebmlElement";
1✔
2
import EbmlParserOptions from "./ebmlParserOptions";
3
import {ByteVector} from "../byteVector";
1✔
4
import {UnsupportedFormatError} from "../errors";
1✔
5
import {File} from "../file";
6
import {IDisposable} from "../interfaces"
7
import {Guards, NumberUtils} from "../utils";
1✔
8

9
export default class EbmlParser implements IDisposable {
1✔
10

11
    private readonly _file: File;
12
    private readonly _maxOffset: number;
13

14
    // private _childParser: EbmlParser;
15
    private _currentElement: EbmlElement;
16
    private _dataOffset: number;
17
    private _dataSize: number;
18
    private _headerSize: number;
19
    private _id: number;
20
    private _options: EbmlParserOptions;
21

22
    /**
23
     * Absolute position within the file where the reader is currently pointing. This will always
24
     * be the *next* element to read.
25
     * @private
26
     */
27
    private _offset: number;
28
    // private _parent: EbmlParser;
29

30
    // #region Constructors
31

32
    /**
33
     * Constructs and initializes a new instance using a file and optionally a position within the
34
     * file where the parser should begin reading. If the parser should process a subset of the
35
     * file, `length` can be provided.
36
     * @param file EBML file to process
37
     * @param offset Position in the file to begin parsing
38
     * @param maxOffset: Maximum position in the file to read up to
39
     * @param options Optional options for reading the EBML file
40
     */
41
    public constructor(file: File, offset: number, maxOffset: number, options?: EbmlParserOptions) {
42
        Guards.truthy(file, "file");
64✔
43
        Guards.safeUint(offset, "offset");
62✔
44
        Guards.safeUint(maxOffset, "maxOffset");
57✔
45

46
        this._file = file;
52✔
47
        this._offset = offset;
52✔
48
        this._maxOffset = maxOffset;
52✔
49

50
        this.setOptions(options?.maxIdLength ?? 4, options?.maxSizeLength ?? 8);
52✔
51
    }
52

53
    // #endregion
54

55
    // #region Properties
56

57
    public get currentElement(): EbmlElement { return this._currentElement; }
150✔
58

59
    // #endregion
60

61
    // #region Methods
62

63
    public static getAllElements(parser: EbmlParser): Map<number, EbmlElement> {
64
        try {
12✔
65
            const elements = new Map<number, EbmlElement>();
12✔
66
            while (parser.read()) {
12✔
67
                elements.set(
22✔
68
                    parser.currentElement.id,
69
                    parser.currentElement
70
                );
71
            }
72

73
            return elements;
12✔
74
        } finally {
75
            parser.dispose();
12✔
76
        }
77
    }
78

79
    public static processElements(parser: EbmlParser, actionMap: Map<number, (element: EbmlElement) => void>): void {
80
        try {
9✔
81
            while (parser.read()) {
9✔
82
                const action = actionMap.get(parser.currentElement.id);
48✔
83
                if (action) {
48!
84
                    action(parser.currentElement);
48✔
85
                }
86
            }
87
        }
88
        finally {
89
            parser.dispose();
9✔
90
        }
91
    }
92

93
    public dispose(): void {
94
        // this._parent?.onChildDisposed();
95
    }
96

97
    /**
98
     * Reads the next element in the file at the current level.
99
     * @returns boolean `true` if an element was successfully read. `false` otherwise.
100
     */
101
    public read(): boolean {
102
        // if (this._childParser) {
103
        //     throw new Error("Cannot advance parser when child parser exists. Dispose existing one first.");
104
        // }
105

106
        if (this._offset >= (this._maxOffset - 1)) {
99✔
107
            // We've reached the end of the element
108
            return false;
22✔
109
        }
110

111
        // Read the ID
112
        this._file.seek(this._offset);
77✔
113
        const idReadResult = this.readElementId(this._options.maxIdLength);
77✔
114
        this._id = idReadResult.value;
77✔
115

116
        // Read the data size
117
        this._file.seek(this._offset + idReadResult.bytes);
77✔
118
        const dataSizeReadResult = this.readVariableInteger(this._options.maxSizeLength);
77✔
119
        this._dataSize = dataSizeReadResult.value;
77✔
120

121
        // Update the state of the reader within the file
122
        this._headerSize = idReadResult.bytes + dataSizeReadResult.bytes;
77✔
123
        this._dataOffset = this._offset + this._headerSize;
77✔
124
        this._offset = this._dataOffset + this._dataSize;
77✔
125
        this._currentElement = new EbmlElement(this._file, this._dataOffset, this._id, this._dataSize, this._options)
77✔
126

127
        return true;
77✔
128
    }
129

130
    public setOptions(maxIdLength: number|undefined, maxSizeLength: number|undefined): void {
131
        Guards.safeUint(maxIdLength, "options.maxIdLength");
52✔
132
        Guards.safeUint(maxSizeLength, "options.maxSizeLength");
49✔
133
        if (maxIdLength > 8 || maxSizeLength > 8) {
46✔
134
            throw new UnsupportedFormatError(
2✔
135
                "Not supported: This EBML file is not supported in this version of node-taglib-sharp."
136
            )
137
        }
138

139
        this._options = <EbmlParserOptions>{
44✔
140
            maxIdLength: maxIdLength,
141
            maxSizeLength: maxSizeLength
142
        };
143
    }
144

145
    // /**
146
    //  * Stores raw binary bytes in the current element's data section.
147
    //  * @param value Raw bytes to store in the element
148
    //  */
149
    // public setBytes(value: ByteVector): void {
150
    //     Guards.truthy(value, "value");
151
    //
152
    //     // Write the bytes, re-render the header
153
    //     this._file.insert(value, this._dataOffset, this._dataSize);
154
    //     const headerBytes = ByteVector.concatenate(
155
    //         this.renderVariableInteger(this._id),
156
    //         this.renderVariableInteger(value.length)
157
    //     )
158
    //     this._file.insert(headerBytes, this._dataOffset - this._headerSize, this._headerSize);
159
    //
160
    //     // Update the current state of the parser
161
    //     const headerDifference = headerBytes.length - this._headerSize;
162
    //     this._headerSize = headerBytes.length;
163
    //     const dataDifference = value.length - this._dataSize;
164
    //     this._dataSize = value.length;
165
    //     this._maxOffset += headerDifference + dataDifference;
166
    //
167
    //     // Update the parent if necessary
168
    //     this._parent?.onChildDataSizeChange(headerDifference + dataDifference);
169
    // }
170

171
    private static lastSevenBitsTruthy(
172
        bytes: ByteVector,
173
        index: number,
174
        upperMask: number,
175
        lowerMask: number
176
    ): boolean {
177
        const upperByteCheck = index > 1
35✔
178
            ? NumberUtils.hasFlag(bytes.get(index - 1), upperMask)
35✔
179
            : false;
180
        return upperByteCheck || NumberUtils.hasFlag(bytes.get(index), lowerMask);
35✔
181

182
    }
183

184
    // private onChildDisposed(): void {
185
    //     this._childParser = undefined;
186
    // }
187
    //
188
    // private onChildDataSizeChange(difference: number): void {
189
    //     this._offset += difference;
190
    //     this._dataSize += difference;
191
    //     this._maxOffset += difference;
192
    // }
193

194
    private renderVariableInteger(value: number|bigint): ByteVector {
195
        // The theory behind this algorithm: The maximum size the EBML spec supports at time of
196
        // writing is 56-bits. Since this is greater than the maximum uint javascript safely
197
        // handles, we convert the number to bytes. If the uppermost 7 bits of a 56-bit value (the
198
        // 1st byte) contain something, then we are using 8 bytes to store the value (bytes 1-7 and
199
        // a 0x01 length descriptor). If those bits were empty, we check the next 7 bits. This
200
        // crosses a byte boundary, so we check it using a mask on the lower byte and a mask on the
201
        // upper byte. If those bits contained something, we OR the upper bits with the length
202
        // descriptor and take the 2-7 bytes. If the bits are empty, we shift the masks to the
203
        // left, shift the length descriptor, increment the lower byte index, and repeat.
204
        // FUCK this took too long to solve.
205

206
        const valueBytes = ByteVector.fromUlong(value);
9✔
207
        if (valueBytes.get(0)) {
9✔
208
            throw new Error("Not supported: Values > 56 bits are not supported by EBML spec at this time.")
1✔
209
        }
210

211
        let upperMask = 0x00;
8✔
212
        let lowerMask = 0xFE;
8✔
213
        let lengthDescriptor = 0x01;
8✔
214
        let lowerByteIndex = 1;
8✔
215
        while (lowerByteIndex < 8) {
8✔
216
            if (EbmlParser.lastSevenBitsTruthy(valueBytes, lowerByteIndex, upperMask, lowerMask)) {
35✔
217
                const upperMostByte = lowerByteIndex > 0 ? valueBytes.get(lowerByteIndex - 1) : 0x00;
7!
218
                return ByteVector.concatenate(
7✔
219
                    NumberUtils.uintOr(lengthDescriptor, upperMostByte),
220
                    valueBytes.subarray(lowerByteIndex)
221
                )
222
            }
223

224
            upperMask = NumberUtils.uintAnd(NumberUtils.uintLShift(upperMask, 1) + 1, 0xFF);
28✔
225
            lowerMask = NumberUtils.uintAnd(NumberUtils.uintLShift(lowerMask, 1), 0xFF);
28✔
226
            lengthDescriptor = NumberUtils.uintLShift(lengthDescriptor, 1);
28✔
227
            lowerByteIndex++;
28✔
228
        }
229

230
        // If we made it this far, the value fits in 7 bits
231
        const byte = NumberUtils.uintOr(0x80, valueBytes.get(7));
1✔
232
        return ByteVector.fromByteArray([byte]);
1✔
233
    }
234

235
    private readElementId(maxBytes: number): { bytes: number, value: number } {
236
        // For whatever reason, element IDs seem to always include the variable integer length
237
        // bits. This isn't described in the specification, but all documented element IDs start
238
        // with the variable integer length bits, so that must be how it is. As such, we just need
239
        // to determine the length of the variable integer and return the entire thing as a number.
240
        const bytes = this.readVariableIntegerBytes(maxBytes);
77✔
241
        return {
77✔
242
            bytes: bytes.length,
243
            value: bytes.toUint()
244
        }
245
    }
246

247
    private readVariableInteger(maxBytes: number): { bytes: number, value: number } {
248
        const bytes = this.readVariableIntegerBytes(maxBytes);
87✔
249
        const additionalBytes = bytes.length - 1;
86✔
250

251
        // Put together the bytes into the output value
252
        let outputValue = BigInt(0);
86✔
253
        for (let i = 0; i < additionalBytes; i++) {
86✔
254
            const byteIndex = additionalBytes - i;
35✔
255
            const byte = bytes.get(byteIndex);
35✔
256

257
            // SPECIAL CASE: We operate under the assumption that data sizes will *not* be >52
258
            // bits. If we encounter too many bits in the number, give up.
259
            if (additionalBytes >= 7 && i >= 6 && NumberUtils.hasFlag(byte, 0xF0)) {
35✔
260
                throw new Error(
1✔
261
                    "Not supported: EBML data sizes > 52 bits are not supported in this version of node-taglib-sharp"
262
                );
263
            }
264

265
            outputValue |= BigInt(byte) << BigInt(i * 8)
34✔
266
        }
267

268
        // @TODO: Support unknown element size?
269

270
        const upperMostMask = NumberUtils.uintRShift(0xFF, additionalBytes + 1);
85✔
271
        const upperMostByte = NumberUtils.uintAnd(bytes.get(0), upperMostMask);
85✔
272
        outputValue |= BigInt(upperMostByte) << BigInt(additionalBytes * 8);
85✔
273

274
        return {
85✔
275
            bytes: additionalBytes + 1,
276
            value: Number(outputValue)
277
        };
278
    }
279

280
    private readVariableIntegerBytes(maxBytes: number): ByteVector {
281
        // The theory behind this algorithm: Certain numbers in EBML are stored like UTF8 values.
282
        // The first x bits are used to indicate how many bytes are used to store the value.
283
        // Starting from the most significant bit, a 1 in this position indicates the value only
284
        // uses one byte. If the first position is 0, the next bit is checked. A 1 in that position
285
        // indicates the value uses two bytes. If that position is 0, the next bit is checked, etc.
286
        // The remaining bits after the first 1 are used as the uppermost bits of the value.
287
        // For example, a value that uses at most 4 bytes:
288
        //    1xxx xxxx                                  - Value stored in 7 bits
289
        //    01xx xxxx  xxxx xxxx                       - Value stored in 14 bits
290
        //    001x xxxx  xxxx xxxx  xxxx xxxx            - Value stored in 21 bits
291
        //    0001 xxxx  xxxx xxxx  xxxx xxxx  xxxx xxxx - Value stored in 28 bits
292

293
        const bytes = this._file.readBlock(maxBytes);
164✔
294

295
        // Determine how many bytes are needed to store the value
296
        let mask = 0x80;
164✔
297
        let additionalBytes = 0
164✔
298
        while (additionalBytes <= maxBytes) {
164✔
299
            if (NumberUtils.hasFlag(bytes.get(0), mask)) {
278✔
300
                break;
164✔
301
            }
302

303
            additionalBytes++;
114✔
304
            mask = NumberUtils.uintRShift(mask, 1);
114✔
305
        }
306

307
        if (additionalBytes > maxBytes) {
164!
308
            throw new Error("Invalid EBML format read: Missing length descriptor")
×
309
        }
310
        if (bytes.length < additionalBytes + 1) {
164✔
311
            throw new Error("Invalid EBML format read: Could not read enough bytes");
1✔
312
        }
313

314
        return bytes.subarray(0, additionalBytes + 1);
163✔
315
    }
316

317
    // #endregion
318
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc