pack-codec.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. var inflateStream = require('./inflate-stream.js');
  2. var inflate = require('./inflate.js');
  3. var deflate = require('./deflate.js');
  4. var sha1 = require('git-sha1');
  5. var bodec = require('bodec');
  6. var typeToNum = {
  7. commit: 1,
  8. tree: 2,
  9. blob: 3,
  10. tag: 4,
  11. "ofs-delta": 6,
  12. "ref-delta": 7
  13. };
  14. var numToType = {};
  15. for (var type in typeToNum) {
  16. var num = typeToNum[type];
  17. numToType[num] = type;
  18. }
  19. exports.parseEntry = parseEntry;
  20. function parseEntry(chunk) {
  21. var offset = 0;
  22. var byte = chunk[offset++];
  23. var type = numToType[(byte >> 4) & 0x7];
  24. var size = byte & 0xf;
  25. var left = 4;
  26. while (byte & 0x80) {
  27. byte = chunk[offset++];
  28. size |= (byte & 0x7f) << left;
  29. left += 7;
  30. }
  31. size = size >>> 0;
  32. var ref;
  33. if (type === "ref-delta") {
  34. ref = bodec.toHex(bodec.slice(chunk, offset, offset += 20));
  35. }
  36. else if (type === "ofs-delta") {
  37. byte = chunk[offset++];
  38. ref = byte & 0x7f;
  39. while (byte & 0x80) {
  40. byte = chunk[offset++];
  41. ref = ((ref + 1) << 7) | (byte & 0x7f);
  42. }
  43. }
  44. var body = inflate(bodec.slice(chunk, offset));
  45. if (body.length !== size) {
  46. throw new Error("Size mismatch");
  47. }
  48. var result = {
  49. type: type,
  50. body: body
  51. };
  52. if (typeof ref !== "undefined") {
  53. result.ref = ref;
  54. }
  55. return result;
  56. }
  57. exports.decodePack = decodePack;
  58. function decodePack(emit) {
  59. var state = $pack;
  60. var sha1sum = sha1();
  61. var inf = inflateStream();
  62. var offset = 0;
  63. var position = 0;
  64. var version = 0x4b434150; // PACK reversed
  65. var num = 0;
  66. var type = 0;
  67. var length = 0;
  68. var ref = null;
  69. var checksum = "";
  70. var start = 0;
  71. var parts = [];
  72. return function (chunk) {
  73. if (chunk === undefined) {
  74. if (num || checksum.length < 40) throw new Error("Unexpected end of input stream");
  75. return emit();
  76. }
  77. for (var i = 0, l = chunk.length; i < l; i++) {
  78. // console.log([state, i, chunk[i].toString(16)]);
  79. if (!state) throw new Error("Unexpected extra bytes: " + bodec.slice(chunk, i));
  80. state = state(chunk[i], i, chunk);
  81. position++;
  82. }
  83. if (!state) return;
  84. if (state !== $checksum) sha1sum.update(chunk);
  85. var buff = inf.flush();
  86. if (buff.length) {
  87. parts.push(buff);
  88. }
  89. };
  90. // The first four bytes in a packfile are the bytes 'PACK'
  91. function $pack(byte) {
  92. if ((version & 0xff) === byte) {
  93. version >>>= 8;
  94. return version ? $pack : $version;
  95. }
  96. throw new Error("Invalid packfile header");
  97. }
  98. // The version is stored as an unsigned 32 integer in network byte order.
  99. // It must be version 2 or 3.
  100. function $version(byte) {
  101. version = (version << 8) | byte;
  102. if (++offset < 4) return $version;
  103. if (version >= 2 && version <= 3) {
  104. offset = 0;
  105. return $num;
  106. }
  107. throw new Error("Invalid version number " + num);
  108. }
  109. // The number of objects in this packfile is also stored as an unsigned 32 bit int.
  110. function $num(byte) {
  111. num = (num << 8) | byte;
  112. if (++offset < 4) return $num;
  113. offset = 0;
  114. emit({version: version, num: num});
  115. return $header;
  116. }
  117. // n-byte type and length (3-bit type, (n-1)*7+4-bit length)
  118. // CTTTSSSS
  119. // C is continue bit, TTT is type, S+ is length
  120. function $header(byte) {
  121. if (start === 0) start = position;
  122. type = byte >> 4 & 0x07;
  123. length = byte & 0x0f;
  124. if (byte & 0x80) {
  125. offset = 4;
  126. return $header2;
  127. }
  128. return afterHeader();
  129. }
  130. // Second state in the same header parsing.
  131. // CSSSSSSS*
  132. function $header2(byte) {
  133. length |= (byte & 0x7f) << offset;
  134. if (byte & 0x80) {
  135. offset += 7;
  136. return $header2;
  137. }
  138. return afterHeader();
  139. }
  140. // Common helper for finishing tiny and normal headers.
  141. function afterHeader() {
  142. offset = 0;
  143. if (type === 6) {
  144. ref = 0;
  145. return $ofsDelta;
  146. }
  147. if (type === 7) {
  148. ref = "";
  149. return $refDelta;
  150. }
  151. // console.log({type: type,length: length})
  152. return $body;
  153. }
  154. // Big-endian modified base 128 number encoded ref offset
  155. function $ofsDelta(byte) {
  156. ref = byte & 0x7f;
  157. if (byte & 0x80) return $ofsDelta2;
  158. return $body;
  159. }
  160. function $ofsDelta2(byte) {
  161. ref = ((ref + 1) << 7) | (byte & 0x7f);
  162. if (byte & 0x80) return $ofsDelta2;
  163. return $body;
  164. }
  165. // 20 byte raw sha1 hash for ref
  166. function $refDelta(byte) {
  167. ref += toHex(byte);
  168. if (++offset < 20) return $refDelta;
  169. return $body;
  170. }
  171. // Common helper for generating 2-character hex numbers
  172. function toHex(num) {
  173. return num < 0x10 ? "0" + num.toString(16) : num.toString(16);
  174. }
  175. // Common helper for emitting all three object shapes
  176. function emitObject() {
  177. var body = bodec.join(parts);
  178. if (body.length !== length) {
  179. throw new Error("Body length mismatch");
  180. }
  181. var item = {
  182. type: numToType[type],
  183. size: length,
  184. body: body,
  185. offset: start
  186. };
  187. if (ref) item.ref = ref;
  188. parts.length = 0;
  189. start = 0;
  190. offset = 0;
  191. type = 0;
  192. length = 0;
  193. ref = null;
  194. emit(item);
  195. }
  196. // Feed the deflated code to the inflate engine
  197. function $body(byte, i, chunk) {
  198. if (inf.write(byte)) return $body;
  199. var buf = inf.flush();
  200. if (buf.length !== length) throw new Error("Length mismatch, expected " + length + " got " + buf.length);
  201. inf.recycle();
  202. if (buf.length) {
  203. parts.push(buf);
  204. }
  205. emitObject();
  206. // If this was all the objects, start calculating the sha1sum
  207. if (--num) return $header;
  208. sha1sum.update(bodec.slice(chunk, 0, i + 1));
  209. return $checksum;
  210. }
  211. // 20 byte checksum
  212. function $checksum(byte) {
  213. checksum += toHex(byte);
  214. if (++offset < 20) return $checksum;
  215. var actual = sha1sum.digest();
  216. if (checksum !== actual) throw new Error("Checksum mismatch: " + actual + " != " + checksum);
  217. }
  218. }
  219. exports.encodePack = encodePack;
  220. function encodePack(emit) {
  221. var sha1sum = sha1();
  222. var left;
  223. return function (item) {
  224. if (item === undefined) {
  225. if (left !== 0) throw new Error("Some items were missing");
  226. return emit();
  227. }
  228. if (typeof item.num === "number") {
  229. if (left !== undefined) throw new Error("Header already sent");
  230. left = item.num;
  231. write(packHeader(item.num));
  232. }
  233. else if (typeof item.type === "string" && bodec.isBinary(item.body)) {
  234. // The header must be sent before items.
  235. if (typeof left !== "number") throw new Error("Headers not sent yet");
  236. // Make sure we haven't sent all the items already
  237. if (!left) throw new Error("All items already sent");
  238. // Send the item in packstream format
  239. write(packFrame(item));
  240. // Send the checksum after the last item
  241. if (!--left) {
  242. emit(bodec.fromHex(sha1sum.digest()));
  243. }
  244. }
  245. else {
  246. throw new Error("Invalid item");
  247. }
  248. };
  249. function write(chunk) {
  250. sha1sum.update(chunk);
  251. emit(chunk);
  252. }
  253. }
  254. function packHeader(length) {
  255. return bodec.fromArray([
  256. 0x50, 0x41, 0x43, 0x4b, // PACK
  257. 0, 0, 0, 2, // version 2
  258. length >> 24, // Num of objects
  259. (length >> 16) & 0xff,
  260. (length >> 8) & 0xff,
  261. length & 0xff
  262. ]);
  263. }
  264. function packFrame(item) {
  265. var length = item.body.length;
  266. // write TYPE_AND_BASE128_SIZE
  267. var head = [(typeToNum[item.type] << 4) | (length & 0xf)];
  268. var i = 0;
  269. length >>= 4;
  270. while (length) {
  271. head[i++] |= 0x80;
  272. head[i] = length & 0x7f;
  273. length >>= 7;
  274. }
  275. if (typeof item.ref === "number") {
  276. // write BIG_ENDIAN_MODIFIED_BASE_128_NUMBER
  277. var offset = item.ref;
  278. // Calculate how many digits we need in base 128 and move the pointer
  279. i += Math.floor(Math.log(offset) / Math.log(0x80)) + 1;
  280. // Write the last digit
  281. head[i] = offset & 0x7f;
  282. // Then write the rest
  283. while (offset >>= 7) {
  284. head[--i] = 0x80 | (--offset & 0x7f);
  285. }
  286. }
  287. var parts = [bodec.fromArray(head)];
  288. if (typeof item.ref === "string") {
  289. parts.push(bodec.fromHex(item.ref));
  290. }
  291. parts.push(deflate(item.body));
  292. return bodec.join(parts);
  293. }