matroska
Version:
Matroska node parser
1,169 lines (906 loc) • 24.1 kB
JavaScript
/*jslint node: true, vars: true, nomen: true */
'use strict';
var assert = require('assert');
var async = require('async');
var crc32 = require('crc').crc32;
var stream = require('stream');
var util = require('util');
var fs = require('fs');
var debug = require('debug')('matroska:element');
var schema = require('./schema');
var tools = require('./tools');
var PADDING = " ";
var PRINT_END = false;
var MASTER_TYPE = "m";
var MillenniumTime = Date.UTC(2001, 0, 1);
/**
* @constructs
* @private
*/
function Element(doc, tagId, ebmlID, start, length) {
if (!doc || doc.type !== 'D') {
throw new Error("Invalid document");
}
this.ownerDocument = doc;
this.tagId = tagId;
var schemaInfo = schema.byEbmlID[ebmlID];
if (!schemaInfo) {
console.error("Unknown schemaID ", ebmlID && ("0x" + ebmlID.toString(16)), "tagId=", tagId, "start=" + start +
" length=" + length + " source=", doc.source);
// throw new Error("Invalid schemaId 0x" + ebmlID.toString(16));
schemaInfo = {
"type": "unknown",
"name": "EBMLID(0x" + ebmlID.toString(16) + ")"
};
}
this.ebmlID = ebmlID;
this.schemaInfo = schemaInfo;
this.type = schemaInfo.type;
this._name = schemaInfo.name;
if (!isNaN(start)) {
this.start = start;
this.length = length || 0;
this.end = this.start + this.length;
}
if (this.type === 'm') {
this.masterType = true;
}
}
module.exports = Element;
Element.prototype._setDataSize = function(dataSize, lengthTagSize) {
this.dataSize = dataSize;
this.lengthTagSize = lengthTagSize;
this.length += dataSize + lengthTagSize;
this.end = this.start + this.length;
};
Element.prototype._setData = function(data) {
this.data = data;
};
Element.prototype.getFirstChildByName = function(name) {
return this.eachChildByName(name, function(child) {
return child;
});
};
Element.prototype.listChildrenByName = function(name) {
var ls = [];
this.eachChildByName(name, function(child) {
ls.push(child);
});
return ls;
};
Element.prototype.eachChildByName = function(name, func) {
var ebmlID = tools.convertEbmlID(name);
if (!func) {
func = function(child) {
return child;
};
}
if (!this.children) {
return undefined;
}
var children = this.children.slice(0);
for (; children.length;) {
var child = children.shift();
if (child.ebmlID === ebmlID) {
var ret = func(child);
if (ret !== undefined) {
return ret;
}
continue;
}
if (child.children) {
var sp = [ 0, 0 ].concat(child.children);
children.splice.apply(children, sp);
}
}
return undefined;
};
Element.prototype.getDirectChildByName = function(name) {
var ebmlID = tools.convertEbmlID(name);
var children = this.children;
if (!children || !children.length) {
return;
}
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.ebmlID === ebmlID) {
return child;
}
}
return null;
};
Element.prototype.loadData = function(callback) {
// XXX TODO
return callback("Not filled yet");
};
Element.prototype.getString = function() {
var data = this.getBuffer();
return data.toString('ascii');
};
Element.prototype.getUTF8 = function() {
var data = this.getBuffer();
return data.toString('utf8');
};
Element.prototype.getValue = function() {
switch (this.type) {
case 's':
return this.getString();
case '8':
return this.getUTF8();
case 'i':
return this.getInt();
case 'u':
var u = this.getUInt();
if (((u === 0) || (u === 1)) && this.schemaInfo && this.schemaInfo.range === "0-1") {
u = (u > 0);
}
return u;
case 'b':
return this.data;
case 'f':
return this.getFloat();
case 'd':
return this.getDate();
}
throw new Error("Type not supported !");
};
Element.prototype.getBuffer = function() {
if (!this.data) {
throw new Error("Data is not loaded ! (tagId=#" + this.tagId + ")");
}
return this.data;
};
Element.prototype.getInt = function() {
var data = this.getBuffer();
var f = data.readIntBE(0, Math.min(data.length, 6));
if (data.length <= 6) {
return f;
}
for (var i = 6; i < data.length; i++) {
f = f * 256 + data[i];
}
return f;
};
Element.prototype.getUInt = function() {
var data = this.getBuffer();
var f = data.readUIntBE(0, Math.min(data.length, 6));
if (data.length <= 6) {
return f;
}
for (var i = 6; i < data.length; i++) {
f = f * 256 + data[i];
}
return f;
};
Element.prototype.getDataSize = function() {
if (this.data) {
return this.data.length;
}
return this.dataSize || 0;
};
Element.prototype.getCRCValue = function() {
var data = this.getBuffer();
if (!data || data.length != 4 || this.type !== 'b') {
throw new Error("Invalid data");
}
var crc = tools.readCRC(data);
return crc;
};
Element.prototype.setFileDataSource = function(path, callback) {
var self = this;
fs.stat(path, function(error, stats) {
// console.log("Stats of ", path, "=", stats);
self.data = undefined;
self.type = 'b';
self.dataSize = stats.size;
self._dataSource = {
getStream: function(options, callback) {
if (typeof (options) === 'function') {
callback = options;
options = null;
}
var stream = fs.createReadStream(path, options);
// console.log("Get stream (3) of ", desc.path, stream);
return callback(null, stream);
},
info: path
};
self._markModified();
return callback(null, self);
});
};
Element.prototype.setCRCValue = function(crc) {
var data = tools.writeCRC(crc);
// console.log("SetCRC ", data);
this.data = data;
this.type = 'b';
this._markModified();
};
Element.prototype.getFloat = function() {
var data = this.getBuffer();
switch (data.length) {
case 4:
return data.readFloatBE(0);
case 8:
return data.readDoubleBE(0);
}
throw new Error("Illegal float size " + data.length + ".");
};
Element.prototype.getDateNanos = function() {
var f = this.getUInt();
return f;
};
Element.prototype.getDate = function() {
var data = this.getBuffer();
var f = data.readUIntBE(0, 6);
var d = new Date(MillenniumTime + (f * 256 * 256 / 1000 / 1000));
return d;
};
Element.prototype.print = function(level) {
level = level || 0;
var s = PADDING + (this.start || 0);
s = s.substring(s.length - 10);
if (PRINT_END) {
var e = PADDING + (this.end || 0);
e = e.substring(e.length - 10);
s += "-" + e;
}
var si = this.tagId + PADDING;
s += "#" + si.substring(0, 5) + " ";
if (level) {
for (var i = 0; i < level; i++) {
s += " ";
}
}
s += "* " + this._name;
var dataLength = (this.data && this.data.length) || this.dataSize || "";
try {
if (this.masterType) {
if (this.type === 'D') {
s += " " + this.source;
}
if (this.start === undefined) {
// s += " children[]";
} else {
s += " children[size=" + (this.end - this.start) + "]";
}
} else if (this.type === 'u') {
s += " u[" + dataLength + "]=" + this.getUInt();
} else if (this.type === 'i') {
s += " i[" + dataLength + "]=" + this.getInt();
} else if (this.type === 's') {
s += " s[" + dataLength + "]='" + this.getString() + "'";
} else if (this.type === '8') {
s += " 8[" + dataLength + "]='" + (dataLength && this.getUTF8()) + "'";
} else if (this.type === 'f') {
s += " f[" + dataLength + "]=" + this.getFloat();
} else if (this.type === 'd') {
s += " d[" + dataLength + "]=" + this.getDate();
} else if (this.type === 'b') {
if (this.dataSize || this.data) {
s += " b[" + dataLength + "]";
if (this.data) {
s += "=" + this.data.slice(0, Math.min(32, this.data.length)).toString('hex');
if (this.ebmlID === schema.byName.SeekID) {
var targetEbmlID = this.getUInt();
var tid = schema.byEbmlID[targetEbmlID];
if (tid) {
s += " => " + tid.name;
} else {
s += " => ? ";
}
}
} else if (this._dataSource) {
s += " {dataSource=" + this._dataSource.info + "}";
}
} else {
s += " b[]";
}
}
if (this._positionTarget) {
s += " [=>#" + this._positionTarget.tagId + "]";
}
if (this._modified) {
s += " [MODIFIED]";
}
} catch (x) {
s += " error=" + x;
if (debug.enabled) {
debug("Error for node #" + this.id, x);
}
}
s += "\n";
// +
// this.start
// +
// "\n";
if (this.children) {
this.children.forEach(function(child) {
s += child.print(level + 1);
});
}
return s;
};
Element.prototype.setString = function(newValue) {
this.data = new Buffer(newValue, "ascii");
this.type = 's';
this._markModified();
};
Element.prototype.setUTF8 = function(newValue) {
this.data = new Buffer(newValue, "utf8");
this.type = '8';
this._markModified();
};
Element.prototype.setInt = function(newValue) {
var b = tools.writeInt(newValue);
this.data = b;
this.type = 'i';
this._markModified();
};
Element.prototype.setUInt = function(newValue) {
var b = tools.writeUInt(newValue);
this.data = b;
this.type = 'u';
this._markModified();
};
Element.prototype.setFloat = function(newValue) {
var b = new Buffer(4);
b.writeFloatBE(newValue, 0);
var n2 = b.readFloatBE(0);
if (n2 !== newValue) {
b = new Buffer(8);
b.writeDoubleBE(newValue, 0);
}
this.type = 'f';
this.data = b;
this._markModified();
};
Element.prototype.setDateNanos = function(newValue) {
return this.setDate(newValue);
};
Element.prototype.setDate = function(newValue) {
var b = new Buffer(8);
if (newValue.getTime) {
var ms = newValue.getTime();
newValue = (ms - MillenniumTime) * (1000 / 256) * (1000 / 256);
} else if (!isNaN(newValue)) {
newValue /= 256 * 256;
}
if (typeof (newValue) !== "number") {
throw new Error("Invalid date value '" + newValue + "'");
}
// console.log("New date=" + newValue);
b.writeUIntBE(newValue, 0, 6);
this.type = 'd';
this.data = b;
this._markModified();
};
Element.prototype.setData = function(newValue) {
this.data = new Buffer(newValue);
this.type = 'b';
this._markModified();
};
Element.prototype._markModified = function() {
if (this._modified) {
return;
}
this._modified = {
start: this.start,
end: this.end
};
this.start = undefined;
this.end = undefined;
this.length = undefined;
this.lengthTagSize = undefined;
var parent = this.parent;
if (parent) {
parent._markModified();
}
return;
};
Element.prototype.setValue = function(newValue) {
if (typeof (newValue) === "string") {
if (this.type === 's') {
this.setString(newValue);
return;
}
this.setUTF8(newValue);
return;
}
if (typeof (newValue) === "boolean") {
newValue = (newValue) ? 1 : 0;
}
if (typeof (newValue) === "number") {
if (Math.floor(newValue) !== newValue) {
this.setFloat(newValue);
return;
}
if (newValue >= 0) {
this.setUInt(newValue);
return;
}
this.setInt(newValue);
return;
}
if (newValue && newValue.getTime) {
this.setDate(newValue);
return;
}
if (Buffer.isBuffer(newValue) || util.isArray(newValue)) {
this.setData(newValue);
return;
}
throw new Error("Unsupported type of value (" + newValue + ")");
};
Element.prototype.setTargetPosition = function(target) {
this._positionTarget = target;
};
Element.prototype.setTargetEbmlID = function(ebmlID) {
if (ebmlID.ebmlID) {
ebmlID = ebmlID.ebmlID;
}
var data = tools.writeEbmlID(ebmlID);
// console.log("Set target ebmlID ", data, " to #" + this.tagId);
this.setData(data);
}
Element.prototype._getSize = function() {
if (!this._modified && this.start !== undefined) {
assert(typeof (this.end) === "number", "End of #" + this.tagId + " is not a number");
assert(typeof (this.start) === "number", "Start of #" + this.tagId + " is not a number");
var sz = this.end - this.start;
// console.log("SizeofNM #" + this.tagId + " => " + sz);
return sz;
}
if (!this.masterType) {
var dataLength = this.getDataSize();
assert(typeof (dataLength) === "number", "Data size of #" + this.tagId + " is not a number");
var sz2 = tools.sizeHInt(this.ebmlID) + tools.sizeVInt(dataLength) + dataLength;
// console.log("Sizeof #" + this.tagId + " => " + sz);
return sz2;
}
var totalSize = 0;
if (this.children) {
this.children.forEach(function(child) {
var s = child._getSize();
assert(typeof (s) === "number", "Size of #" + child.tagId + " is not a number");
totalSize += s;
});
}
return tools.sizeHInt(this.ebmlID) + tools.sizeVInt(totalSize) + totalSize;
};
Element.prototype._optimizeData = function() {
if (!this.data) {
return 0;
}
if (this.type === 'u') {
var u = this.getUInt();
if (tools.sizeUInt(u) !== this.data.length) {
this.setUInt(u);
// console.log("Optimize UINT #" + this.tagId);
return 1;
}
return 0;
}
if (this.type === 'i') {
var i = this.getInt();
if (tools.sizeInt(i) !== this.data.length) {
this.setInt(i);
// console.log("Optimize INT #" + this.tagId);
return 1;
}
return 0;
}
if (this.type === 'f') {
var f = this.getFloat();
if (tools.sizeFloat(f) !== this.data.length) {
this.setFloat(f);
// console.log("Optimize Float #" + this.tagId);
return 1;
}
return 0;
}
return 0;
};
Element.prototype._write = function(output, source, callback) {
if (!this._modified && !this._dataSource) {
source.writeCompleteTag(output, this, callback);
return;
}
var ebmlID = this.schemaInfo._ebmlID;
if (!ebmlID) {
ebmlID = tools.writeUInt(this.ebmlID);
this.schemaInfo._ebmlID = ebmlID;
}
source.writeHInt(output, this.ebmlID);
if (!this.masterType) {
// console.log("Write tag #" + this.tagId + " ", this.data, "/",
// this._dataSource);
if (!this.data && this._dataSource) {
source.writeTagDataSource(output, this.dataSize, this._dataSource, callback);
return;
}
source.writeTagData(output, this.data, callback);
return;
}
var children = this.children;
if (!children) {
source.writeVInt(output, 0);
return callback();
}
var totalSize = 0;
children.forEach(function(child) {
totalSize += child._getSize();
});
source.writeVInt(output, totalSize);
async.eachSeries(children, function(child, callback) {
child._write(output, source, callback);
}, callback);
};
Element.prototype._childrenPosition = function(position) {
var start = this.start;
var end = this.end;
var modified = this._modified;
if (modified) {
start = modified.start;
end = modified.end;
}
// console.log("cp: #" + this.id + " " + start + "<" + position + "<" + end);
if (position < start || position >= end) {
return null;
}
if (position === start) {
return {
position: "start",
target: this
};
}
var children = this.children;
if (children) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
var ret = child._childrenPosition(position);
if (ret) {
return ret;
}
}
}
return {
position: "middle",
target: this
};
};
Element.prototype.getTagByPosition = function(position, contentOffset) {
var old = position;
if (contentOffset) {
position += this.getContentPosition();
} else {
position += this.getPosition();
}
// console.log("Search position=" + position + "/" + old);
return this._childrenPosition(position);
};
Element.prototype.remove = function() {
if (!this.parent) {
throw new Error("No parent !");
}
return this.parent.removeChild(this);
};
Element.prototype.removeChild = function(child) {
if (!this.children) {
return false;
}
var idx = this.children.indexOf(child);
if (idx < 0) {
return false;
}
this.children.splice(idx, 1);
child.parent._markModified();
child.parent = null;
var doc = this.ownerDocument;
child.deepWalk(function(c) {
var schemaInfo = c.schemaInfo;
if (!schemaInfo) {
return;
}
if (c._positionTargetType) {
c._positionTargetType = undefined;
doc._unregisterPosition(c);
}
if (schemaInfo.crc) {
doc._unregisterCRC(c);
}
});
return true;
};
/**
*
* @param child
* @param noUpdate
* @returns
*/
Element.prototype.appendChild = function(child, noUpdate) {
if (!this.masterType) {
console.error(this);
throw new Error("Element " + this._name + "/" + this.ebmlID + "/" + this.type + " is not a master type");
}
return this.insertBefore(child, null, noUpdate);
};
/**
*
* @param {Tag}
* child
* @param {Tag}
* [beforeChild]
*
*/
Element.prototype.insertBefore = function(child, beforeChild, noUpdate) {
if (child.parent) {
child.remove();
}
if (!this.children) {
this.children = [];
}
var doc = this.ownerDocument;
child.deepWalk(function(c) {
var schemaInfo = c.schemaInfo;
if (!schemaInfo) {
return;
}
if (schemaInfo.position) {
c._positionTargetType = schemaInfo.position;
doc._registerPosition(c);
}
if (schemaInfo.crc) {
doc._registerCRC(c);
}
});
if (beforeChild) {
var idx = this.children.indexOf(beforeChild);
if (idx >= 0) {
this.children.splice(idx, 0, child);
child.parent = this;
if (noUpdate !== false) {
this._markModified();
}
return;
}
}
this.children.push(child);
child.parent = this;
if (noUpdate !== false) {
this._markModified();
}
};
Element.prototype.getLevel1 = function() {
for (var p = this; p; p = p.parent) {
if (p.parent.type === 'D') {
return p;
}
}
return undefined;
};
Element.prototype.getPosition = function() {
var parent = this.parent;
if (!parent) {
return 0;
}
var pos = parent.getContentPosition();
var children = parent.children;
if (children) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child === this) {
break;
}
pos += child._getSize();
}
}
return pos;
};
Element.prototype.getContentPosition = function() {
var parent = this.parent;
if (!parent) {
return 0;
}
var pos = this.getPosition() + tools.sizeHInt(this.ebmlID);
if (!this.masterType) {
var dataLength = this.getDataSize();
return pos + tools.sizeVInt(dataLength);
}
if (this.lengthTagSize) {
return pos + this.lengthTagSize;
}
var totalSize = 0;
if (this.children) {
this.children.forEach(function(child) {
totalSize += child._getSize();
});
}
return pos + tools.sizeVInt(totalSize);
};
Element.prototype.eachChild = function(callback) {
var children = this.children;
if (!children || !children.length) {
return;
}
children.forEach(callback);
};
Element.prototype.getDataStream = function(callback) {
if (this.data) {
var bufferStream = new stream.PassThrough();
bufferStream.end(this.data);
callback(null, bufferStream);
return;
}
if (this._dataSource) {
this._dataSource.getStream(callback);
return;
}
this.ownerDocument.source.getTagDataStream(this, callback);
};
function addCRC(crc, value) {
var old = crc.value;
if (old === undefined) {
crc.value = crc32(value);
} else {
crc.value = crc32(value, crc.value);
}
// console.log("Add value=", value, " => 0x" + crc.value.toString(16));
}
function computeCRCStream(stream, crc, callback) {
// console.log("CrcStream reading ...");
stream.on('readable', function() {
var buffer = stream.read();
if (!buffer) {
// console.log("CrcStream END");
return callback(null);
}
// console.log("CrcStream read=", buffer.length);
addCRC(crc, buffer);
});
}
Element.prototype.computeCRC = function(crc, callback) {
crc = crc || {};
addCRC(crc, tools.writeUInt(this.ebmlID));
if (this.masterType) {
var c2 = this.children;
var csize = 0;
if (c2) {
c2.forEach(function(c3) {
csize += c3._getSize();
});
}
// console.log("Add size " + csize);
addCRC(crc, tools.writeVInt(csize));
if (c2) {
setImmediate(this._computeChildrenCRC.bind(this, false, crc, callback));
return;
}
return callback(null, crc.value);
}
if (this.data) {
addCRC(crc, tools.writeVInt(this.data.length));
addCRC(crc, this.data);
return callback(null, crc.value);
}
addCRC(crc, tools.writeVInt(this.dataSize));
this.getDataStream(function(error, stream) {
if (error) {
return callback(error);
}
// console.log("Compute CRC of dataStream ", stream.path);
computeCRCStream(stream, crc, function(error) {
if (error) {
return callback(error);
}
// console.log("CRC computed of dataStream ", crc.value);
callback(null, crc.value);
});
});
};
Element.prototype.moveChildBefore = function(child, beforeChild) {
var children = this.children;
if (!children) {
throw new Error("This tag has no children " + this);
}
var idx = children.indexOf(child);
if (idx < 0) {
throw new Error("Can not find the child '" + child + "' parent=" + this);
}
var bidx = 0;
if (!beforeChild) {
if (idx === children.length - 1) {
return;
}
bidx = children.length;
} else {
bidx = children.indexOf(beforeChild);
if (bidx < 0) {
throw new Error("Can not find the before child '" + beforeChild + "' parent=" + this);
}
}
// console.log("Before=" + children)
children.splice(idx, 1);
// console.log("After1=" + children)
children.splice(bidx - ((idx < bidx) ? 1 : 0), 0, child);
// console.log("After2=" + children)
this._markModified();
return true;
};
Element.prototype._computeChildrenCRC = function(ignoreCRCTag, crc, callback) {
crc = crc || {};
var children = this.children;
if (!children || !children.length) {
return callback(null, crc.value);
}
async.eachSeries(children, function(child, callback) {
if (ignoreCRCTag && child.ebmlID === schema.byName.CRC_32) {
return callback(null);
}
child.computeCRC(crc, callback);
}, function(error) {
if (error) {
return callback(error);
}
return callback(null, crc.value);
});
};
Element.prototype.deepWalk = function(func) {
var ret = func(this);
if (ret !== undefined) {
return ret;
}
var children = this.children;
if (!children) {
return undefined;
}
children = children.slice(0);
for (; children.length;) {
var child = children.shift();
var ret = func(child);
if (ret !== undefined) {
return ret;
}
if (child.children) {
var sp = [ 0, 0 ].concat(child.children);
children.splice.apply(children, sp);
}
}
return undefined;
};
Element.prototype.toString = function() {
return "[Element #" + this.tagId + "]";
};
Element.prototype.setMkvFormatDate = function(date) {
this.setUTF8(tools.formatDate(value));
};
Element.prototype.isModified = function() {
return !!this._modified;
};
Object.defineProperty(Element.prototype, "firstChild", {
iterable: true,
get: function() {
var children = this.children;
if (!children || !children.length) {
return null;
}
return children[0];
}
});
Object.defineProperty(Element.prototype, "lastChild", {
iterable: true,
get: function() {
var children = this.children;
if (!children || !children.length) {
return null;
}
return children[children.length - 1];
}
});
Object.defineProperty(Element.prototype, "empty", {
iterable: true,
get: function() {
var children = this.children;
return (!children || !children.length);
}
});