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/decoder.js ADDED
@@ -0,0 +1,301 @@
1
+ const { Transform } = require('stream');
2
+ const tools = require('./tools');
3
+ const schema = require('./schema');
4
+ const { debugLog } = require('./debug-log');
5
+
6
+ const debug = debugLog('ebml:decoder');
7
+
8
+ const STATE_TAG = 1;
9
+ const STATE_SIZE = 2;
10
+ const STATE_CONTENT = 3;
11
+
12
+ class EbmlDecoder extends Transform {
13
+ /**
14
+ * @property
15
+ * @private
16
+ * @type {Buffer}
17
+ */
18
+ mBuffer = null;
19
+
20
+ /**
21
+ * @private
22
+ * @property
23
+ * @readonly
24
+ */
25
+ mTagStack = [];
26
+
27
+ /**
28
+ * @property
29
+ * @private
30
+ * @type {Number}
31
+ */
32
+ mState = STATE_TAG;
33
+
34
+ /**
35
+ * @property
36
+ * @private
37
+ * @type {Number}
38
+ */
39
+ mCursor = 0;
40
+
41
+ /**
42
+ * @property
43
+ * @private
44
+ * @type {Number}
45
+ */
46
+ mTotal = 0;
47
+
48
+ /**
49
+ * @constructor
50
+ * @param {Object} options The options to be passed along to the super class
51
+ */
52
+ constructor(options = {}) {
53
+ super({ ...options, readableObjectMode: true });
54
+ }
55
+
56
+ get buffer() {
57
+ return this.mBuffer;
58
+ }
59
+
60
+ get cursor() {
61
+ return this.mCursor;
62
+ }
63
+
64
+ get state() {
65
+ return this.mState;
66
+ }
67
+
68
+ get tagStack() {
69
+ return this.mTagStack;
70
+ }
71
+
72
+ get total() {
73
+ return this.mTotal;
74
+ }
75
+
76
+ set buffer(buffer) {
77
+ this.mBuffer = buffer;
78
+ }
79
+
80
+ /**
81
+ * @param {number} cursor
82
+ */
83
+ set cursor(cursor) {
84
+ this.mCursor = cursor;
85
+ }
86
+
87
+ set state(state) {
88
+ this.mState = state;
89
+ }
90
+
91
+ set total(total) {
92
+ this.mTotal = total;
93
+ }
94
+
95
+ _transform(chunk, enc, done) {
96
+ if (!this.buffer) {
97
+ this.buffer = Buffer.from(chunk);
98
+ } else {
99
+ this.buffer = tools.concatenate(this.buffer, Buffer.from(chunk));
100
+ }
101
+
102
+ while (this.cursor < this.buffer.length) {
103
+ if (this.state === STATE_TAG && !this.readTag()) {
104
+ break;
105
+ }
106
+ if (this.state === STATE_SIZE && !this.readSize()) {
107
+ break;
108
+ }
109
+ if (this.state === STATE_CONTENT && !this.readContent()) {
110
+ break;
111
+ }
112
+ }
113
+
114
+ done();
115
+ }
116
+
117
+ static getSchemaInfo(tag) {
118
+ if (Number.isInteger(tag) && schema.has(tag)) {
119
+ return schema.get(tag);
120
+ }
121
+ const tagStr = `0x${tag.toString(16).toUpperCase()}`
122
+ const unknown = {
123
+ type: null,
124
+ name: `unknown-${tagStr}`,
125
+ description: `${tagStr}`,
126
+ level: -1,
127
+ minver: -1,
128
+ multiple: false,
129
+ webm: false,
130
+ };
131
+ schema.set(tag, unknown)
132
+ console.warn('[SCHEMA]', 'unknown tag:', tagStr)
133
+ return unknown
134
+ }
135
+
136
+ readTag() {
137
+ /* istanbul ignore if */
138
+ if (debug.enabled) {
139
+ debug('parsing tag');
140
+ }
141
+
142
+ if (this.cursor >= this.buffer.length) {
143
+ /* istanbul ignore if */
144
+ if (debug.enabled) {
145
+ debug('waiting for more data');
146
+ }
147
+ return false;
148
+ }
149
+
150
+ const start = this.total;
151
+ const tag = tools.readVint(this.buffer, this.cursor);
152
+
153
+ if (tag == null) {
154
+ /* istanbul ignore if */
155
+ if (debug.enabled) {
156
+ debug('waiting for more data');
157
+ }
158
+
159
+ return false;
160
+ }
161
+
162
+ const tagStr = tools.readHexString(
163
+ this.buffer,
164
+ this.cursor,
165
+ this.cursor + tag.length,
166
+ );
167
+ const tagNum = Number.parseInt(tagStr, 16);
168
+ this.cursor += tag.length;
169
+ this.total += tag.length;
170
+ this.state = STATE_SIZE;
171
+
172
+ const tagObj = {
173
+ tag: tag.value,
174
+ tagStr,
175
+ type: EbmlDecoder.getSchemaInfo(tagNum).type,
176
+ name: EbmlDecoder.getSchemaInfo(tagNum).name,
177
+ start,
178
+ end: start + tag.length,
179
+ };
180
+
181
+ this.tagStack.push(tagObj);
182
+ /* istanbul ignore if */
183
+ if (debug.enabled) {
184
+ debug(`read tag: ${tagStr}`);
185
+ }
186
+
187
+ return true;
188
+ }
189
+
190
+ readSize() {
191
+ const tagObj = this.tagStack[this.tagStack.length - 1];
192
+
193
+ /* istanbul ignore if */
194
+ if (debug.enabled) {
195
+ debug(`parsing size for tag: ${tagObj.tagStr}`);
196
+ }
197
+
198
+ if (this.cursor >= this.buffer.length) {
199
+ /* istanbul ignore if */
200
+ if (debug.enabled) {
201
+ debug('waiting for more data');
202
+ }
203
+
204
+ return false;
205
+ }
206
+
207
+ const size = tools.readVint(this.buffer, this.cursor);
208
+
209
+ if (size == null) {
210
+ /* istanbul ignore if */
211
+ if (debug.enabled) {
212
+ debug('waiting for more data');
213
+ }
214
+
215
+ return false;
216
+ }
217
+
218
+ this.cursor += size.length;
219
+ this.total += size.length;
220
+ this.state = STATE_CONTENT;
221
+ tagObj.dataSize = size.value;
222
+
223
+ // unknown size
224
+ if (size.value === -1) {
225
+ tagObj.end = -1;
226
+ } else {
227
+ tagObj.end += size.value + size.length;
228
+ }
229
+ /* istanbul ignore if */
230
+ if (debug.enabled) {
231
+ debug(`read size: ${size.value}`);
232
+ }
233
+
234
+ return true;
235
+ }
236
+
237
+ readContent() {
238
+ const { tagStr, type, dataSize, ...rest } = this.tagStack[this.tagStack.length - 1];
239
+
240
+ /* istanbul ignore if */
241
+ if (debug.enabled) {
242
+ debug(`parsing content for tag: ${tagStr}`);
243
+ }
244
+
245
+ if (type === 'm') {
246
+ /* istanbul ignore if */
247
+ if (debug.enabled) {
248
+ debug('content should be tags');
249
+ }
250
+ this.push(['start', { tagStr, type, dataSize, ...rest }]);
251
+ this.state = STATE_TAG;
252
+
253
+ return true;
254
+ }
255
+
256
+ if (this.buffer.length < this.cursor + dataSize) {
257
+ /* istanbul ignore if */
258
+ if (debug.enabled) {
259
+ debug(`got: ${this.buffer.length}`);
260
+ debug(`need: ${this.cursor + dataSize}`);
261
+ debug('waiting for more data');
262
+ }
263
+
264
+ return false;
265
+ }
266
+
267
+ const data = this.buffer.subarray(this.cursor, this.cursor + dataSize);
268
+ this.total += dataSize;
269
+ this.state = STATE_TAG;
270
+ this.buffer = this.buffer.subarray(this.cursor + dataSize);
271
+ this.cursor = 0;
272
+
273
+ this.tagStack.pop(); // remove the object from the stack
274
+
275
+ this.push([
276
+ 'tag',
277
+ tools.readDataFromTag(
278
+ { tagStr, type, dataSize, ...rest },
279
+ Buffer.from(data),
280
+ ),
281
+ ]);
282
+
283
+ while (this.tagStack.length > 0) {
284
+ const topEle = this.tagStack[this.tagStack.length - 1];
285
+ if (this.total < topEle.end) {
286
+ break;
287
+ }
288
+ this.push(['end', topEle]);
289
+ this.tagStack.pop();
290
+ }
291
+
292
+ /* istanbul ignore if */
293
+ if (debug.enabled) {
294
+ debug(`read data: ${data.toString('hex')}`);
295
+ }
296
+
297
+ return true;
298
+ }
299
+ }
300
+
301
+ module.exports = EbmlDecoder
@@ -0,0 +1,158 @@
1
+ const unexpected = require('unexpected');
2
+ const unexpectedDate = require('unexpected-date');
3
+
4
+ const Decoder = require('./decoder');
5
+
6
+ const expect = unexpected.clone().use(unexpectedDate);
7
+
8
+ const STATE_TAG = 1;
9
+ const STATE_SIZE = 2;
10
+ const STATE_CONTENT = 3;
11
+
12
+ describe('EBML', () => {
13
+ describe('Decoder', () => {
14
+ it('should wait for more data if a tag is longer than the buffer', () => {
15
+ const decoder = new Decoder();
16
+ decoder.write(Buffer.from([0x1a, 0x45]));
17
+
18
+ expect(decoder.state, 'to be', STATE_TAG);
19
+ expect(decoder.buffer.length, 'to be', 2);
20
+ expect(decoder.cursor, 'to be', 0);
21
+ });
22
+
23
+ it('should clear the buffer after a full tag is written in one chunk', () => {
24
+ const decoder = new Decoder();
25
+ decoder.write(Buffer.from([0x42, 0x86, 0x81, 0x01]));
26
+
27
+ expect(decoder.state, 'to be', STATE_TAG);
28
+ expect(decoder.buffer.length, 'to be', 0);
29
+ expect(decoder.cursor, 'to be', 0);
30
+ });
31
+
32
+ it('should clear the buffer after a full tag is written in multiple chunks', () => {
33
+ const decoder = new Decoder();
34
+
35
+ decoder.write(Buffer.from([0x42, 0x86]));
36
+ decoder.write(Buffer.from([0x81, 0x01]));
37
+
38
+ expect(decoder.state, 'to be', STATE_TAG);
39
+ expect(decoder.buffer.length, 'to be', 0);
40
+ expect(decoder.cursor, 'to be', 0);
41
+ });
42
+
43
+ it('should increment the cursor on each step', () => {
44
+ const decoder = new Decoder();
45
+
46
+ decoder.write(Buffer.from([0x42])); // 4
47
+
48
+ expect(decoder.state, 'to be', STATE_TAG);
49
+ expect(decoder.buffer.length, 'to be', 1);
50
+ expect(decoder.cursor, 'to be', 0);
51
+
52
+ decoder.write(Buffer.from([0x86])); // 5
53
+
54
+ expect(decoder.state, 'to be', STATE_SIZE);
55
+ expect(decoder.buffer.length, 'to be', 2);
56
+ expect(decoder.cursor, 'to be', 2);
57
+
58
+ decoder.write(Buffer.from([0x81])); // 6 & 7
59
+
60
+ expect(decoder.state, 'to be', STATE_CONTENT);
61
+ expect(decoder.buffer.length, 'to be', 3);
62
+ expect(decoder.cursor, 'to be', 3);
63
+
64
+ decoder.write(Buffer.from([0x01])); // 6 & 7
65
+
66
+ expect(decoder.state, 'to be', STATE_TAG);
67
+ expect(decoder.buffer.length, 'to be', 0);
68
+ expect(decoder.cursor, 'to be', 0);
69
+ });
70
+
71
+ it('should emit correct tag events for simple data', done => {
72
+ const decoder = new Decoder();
73
+ decoder.on('data', ([state, { dataSize, tag, type, tagStr, data }]) => {
74
+ expect(state, 'to be', 'tag');
75
+ expect(tag, 'to be', 0x286);
76
+ expect(tagStr, 'to be', '4286');
77
+ expect(dataSize, 'to be', 0x01);
78
+ expect(type, 'to be', 'u');
79
+ expect(data, 'to equal', Buffer.from([0x01]));
80
+ done();
81
+ decoder.on('finish', done);
82
+ });
83
+ decoder.on('finish', done);
84
+ decoder.write(Buffer.from([0x42, 0x86, 0x81, 0x01]));
85
+ decoder.end();
86
+ });
87
+
88
+ it('should emit correct EBML tag events for master tags', done => {
89
+ const decoder = new Decoder();
90
+
91
+ decoder.on('data', ([state, { dataSize, tag, type, tagStr, data }]) => {
92
+ expect(state, 'to be', 'start');
93
+ expect(tag, 'to be', 0x0a45dfa3);
94
+ expect(tagStr, 'to be', '1a45dfa3');
95
+ expect(dataSize, 'to be', 0);
96
+ expect(type, 'to be', 'm');
97
+ expect(data, 'to be undefined');
98
+ done();
99
+ decoder.on('finish', done);
100
+ });
101
+ decoder.on('finish', done);
102
+
103
+ decoder.write(Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x80]));
104
+ decoder.end();
105
+ });
106
+
107
+ it('should emit correct EBML:end events for master tags', done => {
108
+ const decoder = new Decoder();
109
+ let tags = 0;
110
+ decoder.on('data', d => {
111
+ const [state, data] = d;
112
+ if (state === 'end') {
113
+ expect(tags, 'to be', 2); // two tags
114
+ expect(data.tag, 'to be', 0x0a45dfa3);
115
+ expect(data.tagStr, 'to be', '1a45dfa3');
116
+ expect(data.dataSize, 'to be', 4);
117
+ expect(data.type, 'to be', 'm');
118
+ expect(data.data, 'to be undefined');
119
+ done();
120
+ decoder.on('finish', done);
121
+ } else {
122
+ tags += 1;
123
+ }
124
+ });
125
+ decoder.on('finish', done);
126
+
127
+ decoder.write(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]));
128
+ decoder.write(Buffer.from([0x84, 0x42, 0x86, 0x81, 0x00]));
129
+ decoder.end();
130
+ });
131
+ describe('::getSchemaInfo', () => {
132
+ it('returns a correct tag if possible', () => {
133
+ expect(Decoder.getSchemaInfo(0x4286), 'to satisfy', {
134
+ name: 'EBMLVersion',
135
+ level: 1,
136
+ type: 'u',
137
+ mandatory: true,
138
+ default: 1,
139
+ minver: 1,
140
+ description: 'The version of EBML parser used to create the file.',
141
+ multiple: false,
142
+ webm: false,
143
+ });
144
+ });
145
+ it('returns a default object if not found', () => {
146
+ expect(Decoder.getSchemaInfo(0x404), 'to satisfy', {
147
+ type: expect.it('to be null'),
148
+ name: expect.it('to be a string').and('to be', 'unknown'),
149
+ description: expect.it('to be a string').and('to be empty'),
150
+ level: expect.it('to be a number').and('not to be positive'),
151
+ minver: expect.it('to be a number').and('not to be positive'),
152
+ multiple: expect.it('to be a boolean'),
153
+ webm: expect.it('to be a boolean'),
154
+ });
155
+ });
156
+ });
157
+ });
158
+ });
package/src/encoder.js ADDED
@@ -0,0 +1,246 @@
1
+ const { Transform } = require('stream');
2
+ const schema = require('./schema');
3
+ const tools = require('./tools');
4
+ const { debugLog } = require('./debug-log');
5
+
6
+ const debug = debugLog('ebml:encoder');
7
+
8
+ /** @typedef {import('./types/tag.types').Tag} Tag */
9
+
10
+ /**
11
+ * @param {number} tagId
12
+ * @param {Buffer} tagData
13
+ * @param {number} [end]
14
+ */
15
+ function encodeTag(tagId, tagData, end) {
16
+ const data = [Buffer.from(tagId.toString(16), 'hex')];
17
+ if (end === -1) {
18
+ data.push(Buffer.from([0xFF]))
19
+ } else {
20
+ data.push(tools.writeVint(tagData.length))
21
+ }
22
+
23
+ // cast ArrayBuffer to Buffer
24
+ if (!Buffer.isBuffer(tagData)) {
25
+ tagData = Buffer.from(tagData); // eslint-disable-line no-param-reassign
26
+ }
27
+ data.push(tagData);
28
+ return Buffer.concat(data);
29
+ }
30
+
31
+ /**
32
+ * Encodes a raw EBML stream
33
+ * @class EbmlEncoder
34
+ * @extends Transform
35
+ */
36
+ class EbmlEncoder extends Transform {
37
+ /**
38
+ * @type {Buffer}
39
+ * @property
40
+ * @private
41
+ */
42
+ mBuffer = null;
43
+
44
+ /**
45
+ * @private
46
+ * @property
47
+ * @type {Boolean}
48
+ */
49
+ mCorked = false;
50
+
51
+ /**
52
+ * @private
53
+ * @property
54
+ * @type {Array<Tag>}
55
+ */
56
+ mStack = [];
57
+
58
+ constructor(options = {}) {
59
+ super({ ...options, writableObjectMode: true });
60
+ }
61
+
62
+ get buffer() {
63
+ return this.mBuffer;
64
+ }
65
+
66
+ get corked() {
67
+ return this.mCorked;
68
+ }
69
+
70
+ get stack() {
71
+ return this.mStack;
72
+ }
73
+
74
+ set buffer(buffer) {
75
+ this.mBuffer = buffer;
76
+ }
77
+
78
+ set corked(corked) {
79
+ this.mCorked = corked;
80
+ }
81
+
82
+ set stack(stak) {
83
+ this.mStack = stak;
84
+ }
85
+
86
+ /**
87
+ *
88
+ * @param {[string, Tag]} chunk array of chunk data, starting with the tag
89
+ * @param {string} enc the encoding type (not used)
90
+ * @param {Function} done a callback method to call after the transformation
91
+ */
92
+ _transform(chunk, enc, done) {
93
+ const [tag, { data, name, ...rest }] = chunk;
94
+ /* istanbul ignore if */
95
+ if (debug.enabled) {
96
+ debug(`encode ${tag} ${name}`);
97
+ }
98
+
99
+ switch (tag) {
100
+ case 'start':
101
+ this.startTag(name, { ...rest });
102
+ break;
103
+ case 'tag':
104
+ this.writeTag(name, data);
105
+ break;
106
+ case 'end':
107
+ this.endTag();
108
+ break;
109
+ default:
110
+ break;
111
+ }
112
+
113
+ return done();
114
+ }
115
+
116
+ /**
117
+ * @private
118
+ * @param {Function} done callback function
119
+ */
120
+ flush(done = () => {}) {
121
+ if (!this.buffer || this.corked) {
122
+ /* istanbul ignore if */
123
+ if (debug.enabled) {
124
+ debug('no buffer/nothing pending');
125
+ }
126
+ return done();
127
+ }
128
+
129
+ if (this.buffer.byteLength === 0) {
130
+ /* istanbul ignore if */
131
+ if (debug.enabled) {
132
+ debug('empty buffer');
133
+ }
134
+ return done();
135
+ }
136
+
137
+ /* istanbul ignore if */
138
+ if (debug.enabled) {
139
+ debug(`writing ${this.buffer.length} bytes`);
140
+ }
141
+
142
+ const chunk = Buffer.from(this.buffer);
143
+ this.buffer = null;
144
+ this.push(chunk);
145
+ return done();
146
+ }
147
+
148
+ /**
149
+ * @private
150
+ * @param {Buffer | Buffer[]} buffer
151
+ */
152
+ bufferAndFlush(buffer) {
153
+ this.buffer = tools.concatenate(this.buffer, buffer);
154
+ this.flush();
155
+ }
156
+
157
+ _flush(done = () => {}) {
158
+ this.flush(done);
159
+ }
160
+
161
+ _bufferAndFlush(buffer) {
162
+ this.bufferAndFlush(buffer);
163
+ }
164
+
165
+ /**
166
+ * gets the ID of the type of tagName
167
+ * @static
168
+ * @param {string} tagName to be looked up
169
+ * @return {number} A buffer containing the schema information
170
+ */
171
+ static getSchemaInfo(tagName) {
172
+ const tagId = Array.from(schema.keys()).find(
173
+ str => schema.get(str).name === tagName,
174
+ );
175
+ if (tagId) {
176
+ return tagId;
177
+ }
178
+
179
+ return null;
180
+ }
181
+
182
+ cork() {
183
+ this.corked = true;
184
+ }
185
+
186
+ uncork() {
187
+ this.corked = false;
188
+ this.flush();
189
+ }
190
+
191
+ /** @private */
192
+ writeTag(tagName, tagData) {
193
+ const tagId = EbmlEncoder.getSchemaInfo(tagName);
194
+ if (!tagId) {
195
+ throw new Error(`No schema entry found for ${tagName}`);
196
+ }
197
+ if (tagData) {
198
+ const data = encodeTag(tagId, tagData);
199
+ if (this.stack.length > 0) {
200
+ this.stack[this.stack.length - 1].children.push({ data });
201
+ } else {
202
+ this.bufferAndFlush(data);
203
+ }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * @private
209
+ * @param {String} tagName The name of the tag to start
210
+ * @param {{end: Number}} info an information object with a `end` parameter
211
+ */
212
+ startTag(tagName, { end }) {
213
+ const tagId = EbmlEncoder.getSchemaInfo(tagName);
214
+ if (!tagId) {
215
+ throw new Error(`No schema entry found for ${tagName}`);
216
+ }
217
+
218
+ const tag = {
219
+ data: null,
220
+ id: tagId,
221
+ name: tagName,
222
+ end,
223
+ children: [],
224
+ };
225
+
226
+ if (this.stack.length > 0) {
227
+ this.stack[this.stack.length - 1].children.push(tag);
228
+ }
229
+ this.stack.push(tag);
230
+ }
231
+
232
+ /** @private */
233
+ endTag() {
234
+ const tag = this.stack.pop() || {
235
+ children: [],
236
+ data: { buffer: Buffer.from([]) },
237
+ };
238
+ const childTagDataBuffers = tag.children.map(child => child.data);
239
+ tag.data = encodeTag(tag.id, Buffer.concat(childTagDataBuffers), tag.end);
240
+ if (this.stack.length < 1) {
241
+ this.bufferAndFlush(tag.data);
242
+ }
243
+ }
244
+ }
245
+
246
+ module.exports = EbmlEncoder