327 lines
7.9 KiB
JavaScript
327 lines
7.9 KiB
JavaScript
var inflateStream = require('./inflate-stream.js');
|
|
var inflate = require('./inflate.js');
|
|
var deflate = require('./deflate.js');
|
|
var sha1 = require('git-sha1');
|
|
var bodec = require('bodec');
|
|
|
|
var typeToNum = {
|
|
commit: 1,
|
|
tree: 2,
|
|
blob: 3,
|
|
tag: 4,
|
|
"ofs-delta": 6,
|
|
"ref-delta": 7
|
|
};
|
|
var numToType = {};
|
|
for (var type in typeToNum) {
|
|
var num = typeToNum[type];
|
|
numToType[num] = type;
|
|
}
|
|
exports.parseEntry = parseEntry;
|
|
function parseEntry(chunk) {
|
|
var offset = 0;
|
|
var byte = chunk[offset++];
|
|
var type = numToType[(byte >> 4) & 0x7];
|
|
var size = byte & 0xf;
|
|
var left = 4;
|
|
while (byte & 0x80) {
|
|
byte = chunk[offset++];
|
|
size |= (byte & 0x7f) << left;
|
|
left += 7;
|
|
}
|
|
size = size >>> 0;
|
|
var ref;
|
|
if (type === "ref-delta") {
|
|
ref = bodec.toHex(bodec.slice(chunk, offset, offset += 20));
|
|
}
|
|
else if (type === "ofs-delta") {
|
|
byte = chunk[offset++];
|
|
ref = byte & 0x7f;
|
|
while (byte & 0x80) {
|
|
byte = chunk[offset++];
|
|
ref = ((ref + 1) << 7) | (byte & 0x7f);
|
|
}
|
|
}
|
|
|
|
var body = inflate(bodec.slice(chunk, offset));
|
|
if (body.length !== size) {
|
|
throw new Error("Size mismatch");
|
|
}
|
|
var result = {
|
|
type: type,
|
|
body: body
|
|
};
|
|
if (typeof ref !== "undefined") {
|
|
result.ref = ref;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
exports.decodePack = decodePack;
|
|
function decodePack(emit) {
|
|
|
|
var state = $pack;
|
|
var sha1sum = sha1();
|
|
var inf = inflateStream();
|
|
|
|
var offset = 0;
|
|
var position = 0;
|
|
var version = 0x4b434150; // PACK reversed
|
|
var num = 0;
|
|
var type = 0;
|
|
var length = 0;
|
|
var ref = null;
|
|
var checksum = "";
|
|
var start = 0;
|
|
var parts = [];
|
|
|
|
|
|
return function (chunk) {
|
|
if (chunk === undefined) {
|
|
if (num || checksum.length < 40) throw new Error("Unexpected end of input stream");
|
|
return emit();
|
|
}
|
|
|
|
for (var i = 0, l = chunk.length; i < l; i++) {
|
|
// console.log([state, i, chunk[i].toString(16)]);
|
|
if (!state) throw new Error("Unexpected extra bytes: " + bodec.slice(chunk, i));
|
|
state = state(chunk[i], i, chunk);
|
|
position++;
|
|
}
|
|
if (!state) return;
|
|
if (state !== $checksum) sha1sum.update(chunk);
|
|
var buff = inf.flush();
|
|
if (buff.length) {
|
|
parts.push(buff);
|
|
}
|
|
};
|
|
|
|
// The first four bytes in a packfile are the bytes 'PACK'
|
|
function $pack(byte) {
|
|
if ((version & 0xff) === byte) {
|
|
version >>>= 8;
|
|
return version ? $pack : $version;
|
|
}
|
|
throw new Error("Invalid packfile header");
|
|
}
|
|
|
|
// The version is stored as an unsigned 32 integer in network byte order.
|
|
// It must be version 2 or 3.
|
|
function $version(byte) {
|
|
version = (version << 8) | byte;
|
|
if (++offset < 4) return $version;
|
|
if (version >= 2 && version <= 3) {
|
|
offset = 0;
|
|
return $num;
|
|
}
|
|
throw new Error("Invalid version number " + num);
|
|
}
|
|
|
|
// The number of objects in this packfile is also stored as an unsigned 32 bit int.
|
|
function $num(byte) {
|
|
num = (num << 8) | byte;
|
|
if (++offset < 4) return $num;
|
|
offset = 0;
|
|
emit({version: version, num: num});
|
|
return $header;
|
|
}
|
|
|
|
// n-byte type and length (3-bit type, (n-1)*7+4-bit length)
|
|
// CTTTSSSS
|
|
// C is continue bit, TTT is type, S+ is length
|
|
function $header(byte) {
|
|
if (start === 0) start = position;
|
|
type = byte >> 4 & 0x07;
|
|
length = byte & 0x0f;
|
|
if (byte & 0x80) {
|
|
offset = 4;
|
|
return $header2;
|
|
}
|
|
return afterHeader();
|
|
}
|
|
|
|
// Second state in the same header parsing.
|
|
// CSSSSSSS*
|
|
function $header2(byte) {
|
|
length |= (byte & 0x7f) << offset;
|
|
if (byte & 0x80) {
|
|
offset += 7;
|
|
return $header2;
|
|
}
|
|
return afterHeader();
|
|
}
|
|
|
|
// Common helper for finishing tiny and normal headers.
|
|
function afterHeader() {
|
|
offset = 0;
|
|
if (type === 6) {
|
|
ref = 0;
|
|
return $ofsDelta;
|
|
}
|
|
if (type === 7) {
|
|
ref = "";
|
|
return $refDelta;
|
|
}
|
|
// console.log({type: type,length: length})
|
|
return $body;
|
|
}
|
|
|
|
// Big-endian modified base 128 number encoded ref offset
|
|
function $ofsDelta(byte) {
|
|
ref = byte & 0x7f;
|
|
if (byte & 0x80) return $ofsDelta2;
|
|
return $body;
|
|
}
|
|
|
|
function $ofsDelta2(byte) {
|
|
ref = ((ref + 1) << 7) | (byte & 0x7f);
|
|
if (byte & 0x80) return $ofsDelta2;
|
|
return $body;
|
|
}
|
|
|
|
// 20 byte raw sha1 hash for ref
|
|
function $refDelta(byte) {
|
|
ref += toHex(byte);
|
|
if (++offset < 20) return $refDelta;
|
|
return $body;
|
|
}
|
|
|
|
// Common helper for generating 2-character hex numbers
|
|
function toHex(num) {
|
|
return num < 0x10 ? "0" + num.toString(16) : num.toString(16);
|
|
}
|
|
|
|
// Common helper for emitting all three object shapes
|
|
function emitObject() {
|
|
var body = bodec.join(parts);
|
|
if (body.length !== length) {
|
|
throw new Error("Body length mismatch");
|
|
}
|
|
var item = {
|
|
type: numToType[type],
|
|
size: length,
|
|
body: body,
|
|
offset: start
|
|
};
|
|
if (ref) item.ref = ref;
|
|
parts.length = 0;
|
|
start = 0;
|
|
offset = 0;
|
|
type = 0;
|
|
length = 0;
|
|
ref = null;
|
|
emit(item);
|
|
}
|
|
|
|
// Feed the deflated code to the inflate engine
|
|
function $body(byte, i, chunk) {
|
|
if (inf.write(byte)) return $body;
|
|
var buf = inf.flush();
|
|
if (buf.length !== length) throw new Error("Length mismatch, expected " + length + " got " + buf.length);
|
|
inf.recycle();
|
|
if (buf.length) {
|
|
parts.push(buf);
|
|
}
|
|
emitObject();
|
|
// If this was all the objects, start calculating the sha1sum
|
|
if (--num) return $header;
|
|
sha1sum.update(bodec.slice(chunk, 0, i + 1));
|
|
return $checksum;
|
|
}
|
|
|
|
// 20 byte checksum
|
|
function $checksum(byte) {
|
|
checksum += toHex(byte);
|
|
if (++offset < 20) return $checksum;
|
|
var actual = sha1sum.digest();
|
|
if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
exports.encodePack = encodePack;
|
|
function encodePack(emit) {
|
|
var sha1sum = sha1();
|
|
var left;
|
|
return function (item) {
|
|
if (item === undefined) {
|
|
if (left !== 0) throw new Error("Some items were missing");
|
|
return emit();
|
|
}
|
|
if (typeof item.num === "number") {
|
|
if (left !== undefined) throw new Error("Header already sent");
|
|
left = item.num;
|
|
write(packHeader(item.num));
|
|
}
|
|
else if (typeof item.type === "string" && bodec.isBinary(item.body)) {
|
|
// The header must be sent before items.
|
|
if (typeof left !== "number") throw new Error("Headers not sent yet");
|
|
|
|
// Make sure we haven't sent all the items already
|
|
if (!left) throw new Error("All items already sent");
|
|
|
|
// Send the item in packstream format
|
|
write(packFrame(item));
|
|
|
|
// Send the checksum after the last item
|
|
if (!--left) {
|
|
emit(bodec.fromHex(sha1sum.digest()));
|
|
}
|
|
}
|
|
else {
|
|
throw new Error("Invalid item");
|
|
}
|
|
};
|
|
function write(chunk) {
|
|
sha1sum.update(chunk);
|
|
emit(chunk);
|
|
}
|
|
}
|
|
|
|
function packHeader(length) {
|
|
return bodec.fromArray([
|
|
0x50, 0x41, 0x43, 0x4b, // PACK
|
|
0, 0, 0, 2, // version 2
|
|
length >> 24, // Num of objects
|
|
(length >> 16) & 0xff,
|
|
(length >> 8) & 0xff,
|
|
length & 0xff
|
|
]);
|
|
}
|
|
|
|
function packFrame(item) {
|
|
var length = item.body.length;
|
|
|
|
// write TYPE_AND_BASE128_SIZE
|
|
var head = [(typeToNum[item.type] << 4) | (length & 0xf)];
|
|
var i = 0;
|
|
length >>= 4;
|
|
while (length) {
|
|
head[i++] |= 0x80;
|
|
head[i] = length & 0x7f;
|
|
length >>= 7;
|
|
}
|
|
|
|
if (typeof item.ref === "number") {
|
|
// write BIG_ENDIAN_MODIFIED_BASE_128_NUMBER
|
|
var offset = item.ref;
|
|
// Calculate how many digits we need in base 128 and move the pointer
|
|
i += Math.floor(Math.log(offset) / Math.log(0x80)) + 1;
|
|
// Write the last digit
|
|
head[i] = offset & 0x7f;
|
|
// Then write the rest
|
|
while (offset >>= 7) {
|
|
head[--i] = 0x80 | (--offset & 0x7f);
|
|
}
|
|
}
|
|
|
|
var parts = [bodec.fromArray(head)];
|
|
if (typeof item.ref === "string") {
|
|
parts.push(bodec.fromHex(item.ref));
|
|
}
|
|
parts.push(deflate(item.body));
|
|
return bodec.join(parts);
|
|
}
|