Files: 12cc851c71e6ab09d98c2ce054c7ae44d20f85b3 / pack.js
4807 bytesRaw
1 | var buffered = require('pull-buffered') |
2 | var crypto = require('crypto') |
3 | var pull = require('pull-stream') |
4 | var toPull = require('stream-to-pull-stream') |
5 | var Inflate = require('pako/lib/inflate').Inflate |
6 | |
7 | exports.decode = decodePack |
8 | |
9 | var objectTypes = [ |
10 | 'none', 'commit', 'tree', 'blob', |
11 | 'tag', 'unused', 'ofs-delta', 'ref-delta' |
12 | ] |
13 | |
14 | function error(cb) { |
15 | return function (err) { |
16 | cb(err || true) |
17 | } |
18 | } |
19 | |
20 | function createHash(type) { |
21 | var hash = crypto.createHash(type) |
22 | var hasher = pull.through(hash.update.bind(hash)) |
23 | hasher.digest = hash.digest.bind(hash) |
24 | return hasher |
25 | } |
26 | |
27 | function inflateBytes(read) { |
28 | var inflate = new Inflate() |
29 | var ended, dataOut |
30 | |
31 | inflate.onData = function (data) { |
32 | dataOut = new Buffer(data) |
33 | // console.error('inflated data', data.length) |
34 | } |
35 | |
36 | inflate.onEnd = function (status) { |
37 | ended = (status === 0) ? true : new Error(inflate.msg) |
38 | // console.error('inflated end', status, ended) |
39 | } |
40 | |
41 | return function (abort, cb) { |
42 | if (ended) return cb(ended) |
43 | read(abort, function next(end, data) { |
44 | if (end === true) { |
45 | end = null |
46 | data = [] |
47 | } |
48 | if (ended = end) return cb(end) |
49 | if (data.length > 1) return cb(new Error('got more than one byte')) |
50 | dataOut = null |
51 | inflate.push(data, end === true) |
52 | if (dataOut) |
53 | cb(null, dataOut) |
54 | else if (ended) |
55 | cb(ended) |
56 | else |
57 | read(null, next) |
58 | }) |
59 | } |
60 | } |
61 | |
62 | function decodePack(onEnd, read) { |
63 | if (read === undefined) |
64 | return decodePack.bind(this, onEnd) |
65 | |
66 | var ended |
67 | var numObjects = -1 |
68 | var checksum = createHash('sha1') |
69 | var b = buffered(read) |
70 | // TODO: optimize to pass through buffers to checksum |
71 | var readByte = checksum(b.chunks(1)) |
72 | var readWord = checksum(b.chunks(4)) |
73 | var readChecksum = b.chunks(20) |
74 | var expectChecksum = true |
75 | var opts = { |
76 | verbosity: 2 |
77 | } |
78 | |
79 | function readHeader(cb) { |
80 | readWord(null, function (end, header) { |
81 | if (ended = end) return cb(end) |
82 | if (!header.equals(header, new Buffer('PACK'))) |
83 | read(new Error('Invalid packfile header'), error(cb)) |
84 | else |
85 | readVersion(cb) |
86 | }) |
87 | } |
88 | |
89 | function readVersion(cb) { |
90 | readWord(null, function (end, word) { |
91 | if (ended = end) return cb(end) |
92 | var version = word.readUInt32BE() |
93 | if (version < 2 || version > 3) |
94 | read(new Error('Invalid packfile version ' + version), error(cb)) |
95 | else |
96 | readNumObjects(cb) |
97 | }) |
98 | } |
99 | |
100 | function readNumObjects(cb) { |
101 | readWord(null, function (end, word) { |
102 | if (ended = end) return cb(end) |
103 | numObjects = word.readUInt32BE() |
104 | if (opts.verbosity >= 1) |
105 | console.error(numObjects + ' objects') |
106 | readObject(null, cb) |
107 | }) |
108 | } |
109 | |
110 | function readVarInt(cb) { |
111 | var type, value, shift |
112 | // https://codewords.recurse.com/images/three/varint.svg |
113 | readByte(null, function (end, buf) { |
114 | if (ended = end) return cb(end) |
115 | var firstByte = buf[0] |
116 | type = objectTypes[(firstByte >> 4) & 7] |
117 | value = firstByte & 15 |
118 | // console.error('byte1', firstByte, firstByte.toString(2), value, value.toString(2)) |
119 | shift = 4 |
120 | checkByte(firstByte) |
121 | }) |
122 | |
123 | function checkByte(byte) { |
124 | if (byte & 0x80) |
125 | readByte(null, gotByte) |
126 | else |
127 | cb(null, type, value) |
128 | } |
129 | |
130 | function gotByte(end, buf) { |
131 | if (ended = end) return cb(end) |
132 | var byte = buf[0] |
133 | value += (byte & 0x7f) << shift |
134 | shift += 7 |
135 | // console.error('byte', byte, byte.toString(2), value, value.toString(2)) |
136 | checkByte(byte) |
137 | } |
138 | } |
139 | |
140 | function getObject(cb) { |
141 | readVarInt(function (end, type, length) { |
142 | if (opts.verbosity >= 2) |
143 | console.error('read var int', end, type, length) |
144 | numObjects-- |
145 | if (end === true && expectChecksum) |
146 | onEnd(new Error('Missing checksum')) |
147 | if (ended = end) return cb(end) |
148 | // TODO: verify that the inflated data is the correct length |
149 | cb(null, type, inflateBytes(readByte)) |
150 | }) |
151 | } |
152 | |
153 | function readTrailer(cb) { |
154 | // read the checksum before it updates to include the trailer |
155 | var expected = checksum.digest() |
156 | readChecksum(null, function (end, value) { |
157 | cb(true) |
158 | if (end === true && expectChecksum) |
159 | onEnd(new Error('Missing checksum')) |
160 | if (!value.equals(expected)) { |
161 | onEnd(new Error('Checksum mismatch: ' + |
162 | expected.hexSlice() + ' != ' + value.hexSlice())) |
163 | } else { |
164 | if (opts.verbosity >= 2) |
165 | console.error('checksum ok', expected.hexSlice()) |
166 | onEnd(null) |
167 | } |
168 | }) |
169 | } |
170 | |
171 | function readObject(abort, cb) { |
172 | if (ended) cb(ended) |
173 | else if (abort) read(abort) |
174 | else if (numObjects < 0) readHeader(cb) |
175 | else if (numObjects > 0) getObject(cb) |
176 | else if (expectChecksum) readTrailer(cb) |
177 | } |
178 | |
179 | return readObject |
180 | } |
181 |
Built with git-ssb-web