vcdiff
Version:
node.js wrapper for open-vcdiff <https://code.google.com/p/open-vcdiff>
329 lines (259 loc) • 8.66 kB
JavaScript
/*!
* node-vcdiff
* https://github.com/baranov1ch/node-vcdiff
*
* Copyright 2014 Alexey Baranov <me@kotiki.cc>
* Released under the MIT license
*/
var assert = require('assert').ok;
var binding = require('../build/Release/vcdiff')
var stream = require('stream');
var util = require('util');
exports.MAX_MAX_TARGET_FILE_SIZE = 1 << 32 - 1
exports.MIN_MAX_TARGET_FILE_SIZE = 1;
exports.DEFAULT_MAX_TARGET_FILE_SIZE = 1 << 26; // 64Mb
exports.MAX_MAX_TARGET_WINDOW_SIZE = 1 << 32 - 1
exports.MIN_MAX_TARGET_WINDOW_SIZE = 1;
exports.DEFAULT_MAX_TARGET_WINDOW_SIZE = 1 << 26; // 64Mb
// encoding fewer than 64 bytes per chunk is stupid.
exports.MIN_MIN_ENCODE_WINDOW_SIZE = 64;
exports.MAX_MIN_ENCODE_WINDOW_SIZE = Infinity;
exports.DEFAULT_MIN_ENCODE_WINDOW_SIZE = 4 * 1024; // 4Kb
exports.codes = {
VCD_INIT_ERROR : binding.INIT_ERROR,
VCD_ENCODE_ERROR : binding.ENCODE_ERROR,
VCD_DECODE_ERROR : binding.DECODE_ERROR,
};
Object.keys(exports.codes).forEach(function(k) {
exports.codes[exports.codes[k]] = k;
});
exports.HashedDictionary = binding.HashedDictionary;
exports.VcdiffEncoder = VcdiffEncoder;
exports.VcdiffDecoder = VcdiffDecoder;
exports.createVcdiffEncoder = function(o) {
return new VcdiffEncoder(o);
};
exports.createVcdiffDecoder = function(o) {
return new VcdiffDecoder(o);
};
exports.vcdiffEncode = function(buffer, opts, callback) {
return vcdiffBuffer(new VcdiffEncoder(opts), buffer, callback);
};
exports.vcdiffEncodeSync = function(buffer, opts) {
return vcdiffBufferSync(new VcdiffEncoder(opts), buffer);
};
exports.vcdiffDecode = function(buffer, opts, callback) {
return vcdiffBuffer(new VcdiffDecoder(opts), buffer, callback);
};
exports.vcdiffDecodeSync = function(buffer, opts) {
return vcdiffBufferSync(new VcdiffDecoder(opts), buffer);
};
function vcdiffBuffer(engine, buffer, callback) {
if (!(callback instanceof Function))
throw new Error('callback should be a Function instance');
var buffers = [];
var nread = 0;
engine.on('error', onError);
engine.on('end', onEnd);
engine.end(buffer);
flow();
function flow() {
var chunk;
while (null !== (chunk = engine.read())) {
buffers.push(chunk);
nread += chunk.length;
}
engine.once('readable', flow);
}
function onError(err) {
engine.removeListener('end', onEnd);
engine.removeListener('readable', flow);
callback(err);
}
function onEnd() {
var buf = Buffer.concat(buffers, nread);
buffers = [];
callback(null, buf);
engine.close();
}
};
function vcdiffBufferSync(engine, buffer) {
if (typeof buffer === 'string')
buffer = new Buffer(buffer);
if (!Buffer.isBuffer(buffer))
throw new TypeError('Not a string or buffer');
return engine._processChunk(buffer, true, true);
};
function VcdiffEncoder(opts) {
if (!(this instanceof VcdiffEncoder)) return new VcdiffEncoder(opts);
Vcdiff.call(this, opts, binding.ENCODE);
};
function VcdiffDecoder(opts) {
if (!(this instanceof VcdiffDecoder)) return new VcdiffDecoder(opts);
Vcdiff.call(this, opts, binding.DECODE);
};
function Vcdiff(opts, mode) {
opts = opts || {};
this._minEncodeWindowSize =
opts.minEncodeWindowSize || exports.DEFAULT_MIN_ENCODE_WINDOW_SIZE;
stream.Transform.call(this, opts);
if (mode === binding.ENCODE) {
var flags = binding.VCD_STANDARD_FORMAT;
var targetMatches = false;
if (!(opts.hashedDictionary instanceof Object)) {
throw new Error('Must provide HashedDictionary');
}
if (opts.interleaved === true)
flags |= binding.VCD_FORMAT_INTERLEAVED;
if (opts.checksum === true)
flags |= binding.VCD_FORMAT_CHECKSUM;
if (opts.json === true)
flags |= binding.VCD_FORMAT_JSON;
if (opts.targetMatches === true)
targetMatches = true;
if (opts.encodeWindowSize) {
if (opts.encodeWindowSize < exports.MIN_MIN_ENCODE_WINDOW_SIZE ||
opts.encodeWindowSize > exports.MAX_MIN_ENCODE_WINDOW_SIZE)
throw new Error('Invalid encode window size: ' +
opts.encodeWindowSize);
}
this._handle = new binding.Vcdiff(
mode, opts.hashedDictionary, targetMatches, flags);
} else if (mode === binding.DECODE) {
if (!Buffer.isBuffer(opts.dictionary))
throw new Error('Invalid dictionary: it should be a Buffer instance');
var allowVcd = true;
var maxTargetFileSize = exports.DEFAULT_MAX_TARGET_FILE_SIZE;
var maxTargetWindowSize = exports.DEFAULT_MAX_TARGET_WINDOW_SIZE;
if (opts.allowVcdTarget === false)
allowVcd = false;
if (opts.maxTargetFileSize) {
if (opts.maxTargetFileSize < exports.MIN_MAX_TARGET_FILE_SIZE ||
opts.maxTargetFileSize > exports.MAX_MAX_TARGET_FILE_SIZE)
throw new Error('Invalid max target file size: ' +
opts.maxTargetFileSize);
}
if (opts.maxTargetWindowSize) {
if (opts.maxTargetWindowSize < exports.MIN_MAX_TARGET_WINDOW_SIZE ||
opts.maxTargetWindowSize > exports.MAX_MAX_TARGET_WINDOW_SIZE)
throw new Error('Invalid max target window size: ' +
opts.maxTargetWindowSize);
}
this._handle = new binding.Vcdiff(mode,
opts.dictionary,
allowVcd,
maxTargetFileSize,
maxTargetWindowSize);
} else {
throw new Error('invalid mode: neither ENCODE nor DECODE');
}
var self = this;
this._mode = mode;
this._hadError = false;
this._handle.onerror = function(message, errno) {
// there is no way to cleanly recover.
// continuing only obscures problems.
self._handle = null;
self._hadError = true;
var error = new Error(message);
error.errno = errno;
error.code = exports.codes[errno];
self.emit('error', error);
};
this._closed = false;
this._forceFlush = false;
this._nread = 0;
this._buffers = [];
this.once('end', this.close);
};
util.inherits(Vcdiff, stream.Transform);
Vcdiff.prototype.close = function(callback) {
if (callback)
process.nextTick(callback);
if (this._closed)
return;
this._closed = true;
this._handle.close();
var self = this;
process.nextTick(function() {
self.emit('close');
});
};
Vcdiff.prototype.flush = function(callback) {
var ws = this._writableState;
if (ws.ended) {
if (callback)
process.nextTick(callback);
} else if (ws.ending) {
if (callback)
this.once('end', callback);
} else if (ws.needDrain) {
var self = this;
this.once('drain', function() {
self.flush(callback);
});
} else {
this._forceFlush = true;
this.write(new Buffer(0), '', callback);
}
};
Vcdiff.prototype._flush = function(callback) {
this._forceFlush = true;
this._transform(new Buffer(0), '', callback);
};
Vcdiff.prototype._transform = function(chunk, encoding, cb) {
var ws = this._writableState;
var ending = ws.ending || ws.ended;
var last = ending && (!chunk || ws.length === chunk.length);
if (chunk !== null && !Buffer.isBuffer(chunk))
return cb(new Error('invalid input'));
if (this._closed)
return cb(new Error('vcdiff binding closed'));
var forceFlush = this._forceFlush ||
(encoding === 'buffer' && chunk.length === 0);
if (chunk.length >= ws.length) {
this._forceFlush = false;
}
this._processChunk(chunk, last, forceFlush, cb);
};
Vcdiff.prototype._processChunk = function(chunk, isLast, forceFlush, cb) {
var self = this;
var async = cb instanceof Function;
if (!async) {
var error;
this.on('error', function(er) {
error = er;
});
assert(!this._closed, 'vcdiff binding closed');
var res = this._handle.writeSync(isLast, chunk);
if (this._hadError)
throw error;
assert(res[1] === true, 'sync should finish in one pass');
this.close();
return res[0];
}
this._nread += chunk.length;
this._buffers.push(chunk);
if (this._nread >= this._minEncodeWindowSize || forceFlush ||
this._mode === binding.DECODE) {
assert(!this._closed, 'vcdiff binding closed');
chunk = Buffer.concat(this._buffers, this._nread);
self._nread = 0;
self._buffers = [];
var req = this._handle.write(isLast, chunk);
req.buffer = chunk;
req.callback = callback;
} else {
process.nextTick(function() {
cb();
});
}
function callback(out, finished) {
if (self._hadError)
return;
self.push(out);
cb();
}
};
util.inherits(VcdiffEncoder, Vcdiff);
util.inherits(VcdiffDecoder, Vcdiff);