ebml.js 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools.js ADDED
@@ -0,0 +1,355 @@
1
+ const schema = require("./schema");
2
+
3
+ class Tools {
4
+ /**
5
+ * read variable length integer per
6
+ * https://www.matroska.org/technical/specs/index.html#EBML_ex
7
+ * @static
8
+ * @param {Buffer} buffer containing input
9
+ * @param {Number} [start=0] position in buffer
10
+ * @returns {{length: Number, value: number}} value / length object
11
+ */
12
+ static readVint(buffer, start = 0) {
13
+ // Calculate length based on leading zeros in the first byte
14
+ const firstByte = buffer[start];
15
+
16
+ // Find the first 1 bit to determine length (VINT_WIDTH)
17
+ let length = 1;
18
+ let mask = 0x80; // 10000000 binary
19
+ while ((firstByte & mask) === 0 && length <= 8) {
20
+ length++;
21
+ mask >>= 1;
22
+ }
23
+
24
+ if (length > 8) {
25
+ const number = Tools.readHexString(buffer, start, start + length);
26
+ throw new Error(`Unrepresentable length: ${length} ${number}`);
27
+ }
28
+
29
+ if (start + length > buffer.length) {
30
+ return null;
31
+ }
32
+
33
+ // Check if all data bits are 1 (indicating unknown/infinite size)
34
+ const dataMask = (1 << (8 - length)) - 1;
35
+ let value = firstByte & dataMask;
36
+
37
+ // Check for "all bits set" pattern (unknown size)
38
+ let allBitsSet = (firstByte & dataMask) === dataMask;
39
+ for (let i = 1; i < length && allBitsSet; i++) {
40
+ if (buffer[start + i] !== 0xFF) {
41
+ allBitsSet = false;
42
+ }
43
+ }
44
+
45
+ // If all data bits are 1, it represents unknown/infinite size
46
+ if (allBitsSet) {
47
+ return { length, value: -1 };
48
+ }
49
+
50
+ // Parse the remaining bytes
51
+ for (let i = 1; i < length; i++) {
52
+ value = (value << 8) | buffer[start + i];
53
+ }
54
+
55
+ return { length, value };
56
+ }
57
+
58
+ /**
59
+ * write variable length integer
60
+ * @static
61
+ * @param {Number} value to store into buffer
62
+ * @returns {Buffer} containing the value
63
+ */
64
+ static writeVint(value, minLength = 1) {
65
+ if (value < 0 || value > 2 ** 53) {
66
+ throw new Error(`Unrepresentable value: ${value}`);
67
+ }
68
+
69
+ minLength = typeof minLength === 'number' && !isNaN(minLength) ? Math.max(1, minLength) : 1;
70
+ minLength = Math.min(8, minLength);
71
+ let length = 1;
72
+ for (length = minLength; length <= 8; length += 1) {
73
+ if (value < 2 ** (7 * length) - 1) {
74
+ break;
75
+ }
76
+ }
77
+
78
+ const buffer = Buffer.alloc(length);
79
+ let val = value;
80
+ for (let i = 1; i <= length; i += 1) {
81
+ const b = val & 0xff;
82
+ buffer[length - i] = b;
83
+ val -= b;
84
+ val /= 2 ** 8;
85
+ }
86
+ buffer[0] |= 1 << (8 - length);
87
+
88
+ return buffer;
89
+ }
90
+
91
+ /**
92
+ * *
93
+ * concatenate two arrays of bytes
94
+ * @static
95
+ * @param {Buffer} a1 First array
96
+ * @param {Buffer} a2 Second array
97
+ * @returns {Buffer} concatenated arrays
98
+ */
99
+ static concatenate(a1, a2) {
100
+ // both null or undefined
101
+ if (!a1 && !a2) {
102
+ return Buffer.from([]);
103
+ }
104
+ if (!a1 || a1.byteLength === 0) {
105
+ return a2;
106
+ }
107
+ if (!a2 || a2.byteLength === 0) {
108
+ return a1;
109
+ }
110
+
111
+ return Buffer.concat([a1, a2]);
112
+ }
113
+
114
+ /**
115
+ * get a hex text string from Buff[start,end)
116
+ * @param {Buffer} buff from which to read the string
117
+ * @param {Number} [start=0] starting point (default 0)
118
+ * @param {Number} [end=buff.byteLength] ending point (default the whole buffer)
119
+ * @returns {string} the hex string
120
+ */
121
+ static readHexString(buff, start = 0, end = buff.byteLength) {
122
+ return Array.from(buff.slice(start, end))
123
+ .map(q => Number(q).toString(16))
124
+ .reduce((acc, current) => `${acc}${current.padStart(2, '0')}`, '');
125
+ }
126
+
127
+ /**
128
+ * tries to read out a UTF-8 encoded string
129
+ * @param {Buffer} buff the buffer to attempt to read from
130
+ * @return {string|null} the decoded text, or null if unable to
131
+ */
132
+ static readUtf8(buff) {
133
+ try {
134
+ return Buffer.from(buff).toString('utf8');
135
+ } catch (exception) {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * get an unsigned number from a buffer
142
+ * @param {Buffer} buff from which to read variable-length unsigned number
143
+ * @returns {number|string} result (in hex for lengths > 6)
144
+ */
145
+ static readUnsigned(buff) {
146
+ const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
147
+ switch (buff.byteLength) {
148
+ case 1:
149
+ return b.getUint8(0);
150
+ case 2:
151
+ return b.getUint16(0);
152
+ case 4:
153
+ return b.getUint32(0);
154
+ default:
155
+ break;
156
+ }
157
+ if (buff.byteLength <= 6) {
158
+ return buff.reduce((acc, current) => acc * 256 + current, 0);
159
+ }
160
+
161
+ return Tools.readHexString(buff, 0, buff.byteLength);
162
+ }
163
+
164
+ /**
165
+ * get an signed number from a buffer
166
+ * @static
167
+ * @param {Buffer} buff from which to read variable-length signed number
168
+ * @returns {number} result
169
+ */
170
+ static readSigned(buff) {
171
+ const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
172
+ switch (buff.byteLength) {
173
+ case 1:
174
+ return b.getInt8(0);
175
+ case 2:
176
+ return b.getInt16(0);
177
+ case 4:
178
+ return b.getInt32(0);
179
+ default:
180
+ return NaN;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * get an floating-point number from a buffer
186
+ * @static
187
+ * @param {Buffer} buff from which to read variable-length floating-point number
188
+ * @returns {number} result
189
+ */
190
+ static readFloat(buff) {
191
+ const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
192
+ switch (buff.byteLength) {
193
+ case 4:
194
+ return b.getFloat32(0);
195
+ case 8:
196
+ return b.getFloat64(0);
197
+ default:
198
+ return NaN;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * get a date from a buffer
204
+ * @static
205
+ * @param {Buffer} buff from which to read the date
206
+ * @return {Date} result
207
+ */
208
+ static readDate(buff) {
209
+ const b = new DataView(buff.buffer, buff.byteOffset, buff.byteLength);
210
+ switch (buff.byteLength) {
211
+ case 1:
212
+ return new Date(b.getUint8(0));
213
+ case 2:
214
+ return new Date(b.getUint16(0));
215
+ case 4:
216
+ return new Date(b.getUint32(0));
217
+ case 8:
218
+ return new Date(Number.parseInt(Tools.readHexString(buff), 16));
219
+ default:
220
+ return new Date(0);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Reads the data from a tag
226
+ * @static
227
+ * @param {TagData} tagObj The tag object to be read
228
+ * @param {Buffer} data Data to be transformed
229
+ * @return {Tag} result
230
+ */
231
+ static readDataFromTag(tagObj, data) {
232
+ const { type, name } = tagObj;
233
+ let { track } = tagObj;
234
+ let discardable = tagObj.discardable || false;
235
+ let keyframe = tagObj.keyframe || false;
236
+ let payload = null;
237
+ let value;
238
+
239
+ switch (type) {
240
+ case 'u':
241
+ value = Tools.readUnsigned(data);
242
+ break;
243
+ case 'f':
244
+ value = Tools.readFloat(data);
245
+ break;
246
+ case 'i':
247
+ value = Tools.readSigned(data);
248
+ break;
249
+ case 's':
250
+ value = String.fromCharCode(...data);
251
+ break;
252
+ case '8':
253
+ value = Tools.readUtf8(data);
254
+ break;
255
+ case 'd':
256
+ value = Tools.readDate(data);
257
+ break;
258
+ default:
259
+ break;
260
+ }
261
+
262
+ if (name === 'SimpleBlock' || name === 'Block') {
263
+ let p = 0;
264
+ const { length, value: trak } = Tools.readVint(data, p);
265
+ p += length;
266
+ track = trak;
267
+ value = Tools.readSigned(data.subarray(p, p + 2));
268
+ p += 2;
269
+ if (name === 'SimpleBlock') {
270
+ keyframe = Boolean(data[length + 2] & 0x80);
271
+ discardable = Boolean(data[length + 2] & 0x01);
272
+ }
273
+ p += 1;
274
+ payload = data.subarray(p);
275
+ }
276
+
277
+ return {
278
+ ...tagObj,
279
+ data,
280
+ discardable,
281
+ keyframe,
282
+ payload,
283
+ track,
284
+ value,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * @param {Array<[number, import("./types/schema.types").EBMLSchema]>} items
290
+ */
291
+ static addSchema(items) {
292
+ if (!Array.isArray(items)) {
293
+ return schema
294
+ }
295
+ for (const item of items) {
296
+ schema.set(item[0], item[1])
297
+ }
298
+ return schema
299
+ }
300
+
301
+ /**
302
+ * @param {number} value
303
+ * @param {number} size
304
+ */
305
+ static toUint8(value, size) {
306
+ var buf = new Uint8Array(size)
307
+ for (let i = 0, k = size - 1; i < size; ++i, --k) {
308
+ buf[i] = (value >> (8 * k)) & 0xFF
309
+ }
310
+ return buf
311
+ }
312
+
313
+ /** @param {number} value */
314
+ static measureUnsignedInt(value) {
315
+ // Force to 32-bit unsigned integer
316
+ if (value < (1 << 8)) {
317
+ return 1;
318
+ } else if (value < (1 << 16)) {
319
+ return 2;
320
+ } else if (value < (1 << 24)) {
321
+ return 3;
322
+ } else if (value < 2 ** 32) {
323
+ return 4;
324
+ } else if (value < 2 ** 40) {
325
+ return 5;
326
+ } else {
327
+ return 6;
328
+ }
329
+ };
330
+
331
+ /** @param {number} value */
332
+ static measureEBMLVarInt(value) {
333
+ if (value < (1 << 7) - 1) {
334
+ /** Top bit is set, leaving 7 bits to hold the integer, but we can't store
335
+ * 127 because "all bits set to one" is a reserved value. Same thing for the
336
+ * other cases below:
337
+ */
338
+ return 1;
339
+ } else if (value < (1 << 14) - 1) {
340
+ return 2;
341
+ } else if (value < (1 << 21) - 1) {
342
+ return 3;
343
+ } else if (value < (1 << 28) - 1) {
344
+ return 4;
345
+ } else if (value < 2 ** 35 - 1) {
346
+ return 5;
347
+ } else if (value < 2 ** 42 - 1) {
348
+ return 6;
349
+ } else {
350
+ throw new Error('EBML VINT size not supported ' + value);
351
+ }
352
+ };
353
+ }
354
+
355
+ module.exports = Tools