994 lines
27 KiB
Text
994 lines
27 KiB
Text
|
local replaceLine = true -- Setting to false may result in corrupted output
|
||
|
local preload = type(package) == "table" and type(package.preload) == "table" and package.preload or {}
|
||
|
|
||
|
local require = require
|
||
|
if type(require) ~= "function" then
|
||
|
local loading = {}
|
||
|
local loaded = {}
|
||
|
require = function(name)
|
||
|
local result = loaded[name]
|
||
|
|
||
|
if result ~= nil then
|
||
|
if result == loading then
|
||
|
error("loop or previous error loading module '" .. name .. "'", 2)
|
||
|
end
|
||
|
|
||
|
return result
|
||
|
end
|
||
|
|
||
|
loaded[name] = loading
|
||
|
local contents = preload[name]
|
||
|
if contents then
|
||
|
result = contents(name)
|
||
|
else
|
||
|
error("cannot load '" .. name .. "'", 2)
|
||
|
end
|
||
|
|
||
|
if result == nil then result = true end
|
||
|
loaded[name] = result
|
||
|
return result
|
||
|
end
|
||
|
end
|
||
|
preload["objects"] = function(...)
|
||
|
local inflate_zlib = require "deflate".inflate_zlib
|
||
|
local sha = require "metis.crypto.sha1"
|
||
|
|
||
|
local band, bor, lshift, rshift = bit32.band, bit32.bor, bit32.lshift, bit32.rshift
|
||
|
local byte, format, sub = string.byte, string.format, string.sub
|
||
|
|
||
|
local types = { [0] = "none", "commit", "tree", "blob", "tag", nil, "ofs_delta", "ref_delta", "any", "max" }
|
||
|
|
||
|
--- Get the type of a specific object
|
||
|
-- @tparam Object x The object to get the type of
|
||
|
-- @treturn string The object's type.
|
||
|
local function get_type(x) return types[x.ty] or "?" end
|
||
|
|
||
|
local event = ("luagit-%08x"):format(math.random(0, 2^24))
|
||
|
local function check_in()
|
||
|
os.queueEvent(event)
|
||
|
os.pullEvent(event)
|
||
|
end
|
||
|
|
||
|
local sha_format = ("%02x"):rep(20)
|
||
|
|
||
|
local function reader(str)
|
||
|
local expected_checksum = format(sha_format, byte(str, -20, -1))
|
||
|
local actual_checksum = sha(str:sub(1, -21));
|
||
|
if expected_checksum ~= actual_checksum then
|
||
|
error(("checksum mismatch: expected %s, got %s"):format(expected_checksum, actual_checksum))
|
||
|
end
|
||
|
|
||
|
str = str:sub(1, -20)
|
||
|
|
||
|
local pos = 1
|
||
|
|
||
|
local function consume_read(len)
|
||
|
if len <= 0 then error("len < 0", 2) end
|
||
|
if pos > #str then error("end of stream") end
|
||
|
|
||
|
local cur_pos = pos
|
||
|
pos = pos + len
|
||
|
local res = sub(str, cur_pos, pos - 1)
|
||
|
if #res ~= len then error("expected " .. len .. " bytes, got" .. #res) end
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
local function read8()
|
||
|
if pos > #str then error("end of stream") end
|
||
|
local cur_pos = pos
|
||
|
pos = pos + 1
|
||
|
return byte(str, cur_pos)
|
||
|
end
|
||
|
|
||
|
return {
|
||
|
offset = function() return pos - 1 end,
|
||
|
read8 = read8,
|
||
|
read16 = function() return (read8() * (2^8)) + read8() end,
|
||
|
read32 = function() return (read8() * (2^24)) + (read8() * (2^16)) + (read8() * (2^8)) + read8() end,
|
||
|
read = consume_read,
|
||
|
|
||
|
close = function()
|
||
|
if pos ~= #str then error(("%d of %d bytes remaining"):format(#str - pos + 1, #str)) end
|
||
|
end,
|
||
|
}
|
||
|
end
|
||
|
|
||
|
--- Consume a string from the given input buffer
|
||
|
--
|
||
|
-- @tparam Reader handle The handle to read from
|
||
|
-- @tparam number size The number of decompressed bytes to read
|
||
|
-- @treturn string The decompressed data
|
||
|
local function get_data(handle, size)
|
||
|
local tbl, n = {}, 1
|
||
|
|
||
|
inflate_zlib {
|
||
|
input = handle.read8,
|
||
|
output = function(x) tbl[n], n = string.char(x), n + 1 end
|
||
|
}
|
||
|
|
||
|
local res = table.concat(tbl)
|
||
|
if #res ~= size then error(("expected %d decompressed bytes, got %d"):format(size, #res)) end
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
--- Decode a binary delta file, applying it to the original
|
||
|
--
|
||
|
-- The format is described in more detail in [the Git documentation][git_pack]
|
||
|
--
|
||
|
-- [git_pack]: https://git-scm.com/docs/pack-format#_deltified_representation
|
||
|
--
|
||
|
-- @tparam string original The original string
|
||
|
-- @tparam string delta The binary delta
|
||
|
-- @treturn string The patched string
|
||
|
local function apply_delta(original, delta)
|
||
|
local delta_offset = 1
|
||
|
local function read_size()
|
||
|
local c = byte(delta, delta_offset)
|
||
|
delta_offset = delta_offset + 1
|
||
|
|
||
|
local size = band(c, 0x7f)
|
||
|
local shift = 7
|
||
|
while band(c, 0x80) ~= 0 do
|
||
|
c, delta_offset = byte(delta, delta_offset), delta_offset + 1
|
||
|
size, shift = size + lshift(band(c, 0x7f), shift), shift + 7
|
||
|
end
|
||
|
|
||
|
return size
|
||
|
end
|
||
|
|
||
|
local original_length = read_size()
|
||
|
local patched_length = read_size()
|
||
|
if original_length ~= #original then
|
||
|
error(("expected original of size %d, got size %d"):format(original_length, #original))
|
||
|
end
|
||
|
|
||
|
local parts, n = {}, 1
|
||
|
while delta_offset <= #delta do
|
||
|
local b = byte(delta, delta_offset)
|
||
|
delta_offset = delta_offset + 1
|
||
|
|
||
|
if band(b, 0x80) ~= 0 then
|
||
|
-- Copy from the original file. Each bit represents which optional length/offset
|
||
|
-- bits are used.
|
||
|
local offset, length = 0, 0
|
||
|
|
||
|
if band(b, 0x01) ~= 0 then
|
||
|
offset, delta_offset = bor(offset, byte(delta, delta_offset)), delta_offset + 1
|
||
|
end
|
||
|
if band(b, 0x02) ~= 0 then
|
||
|
offset, delta_offset = bor(offset, lshift(byte(delta, delta_offset), 8)), delta_offset + 1
|
||
|
end
|
||
|
if band(b, 0x04) ~= 0 then
|
||
|
offset, delta_offset = bor(offset, lshift(byte(delta, delta_offset), 16)), delta_offset + 1
|
||
|
end
|
||
|
if band(b, 0x08) ~= 0 then
|
||
|
offset, delta_offset = bor(offset, lshift(byte(delta, delta_offset), 24)), delta_offset + 1
|
||
|
end
|
||
|
|
||
|
if band(b, 0x10) ~= 0 then
|
||
|
length, delta_offset = bor(length, byte(delta, delta_offset)), delta_offset + 1
|
||
|
end
|
||
|
if band(b, 0x20) ~= 0 then
|
||
|
length, delta_offset = bor(length, lshift(byte(delta, delta_offset), 8)), delta_offset + 1
|
||
|
end
|
||
|
if band(b, 0x40) ~= 0 then
|
||
|
length, delta_offset = bor(length, lshift(byte(delta, delta_offset), 16)), delta_offset + 1
|
||
|
end
|
||
|
if length == 0 then length = 0x10000 end
|
||
|
|
||
|
parts[n], n = sub(original, offset + 1, offset + length), n + 1
|
||
|
elseif b > 0 then
|
||
|
-- Copy from the delta. The opcode encodes the length
|
||
|
parts[n], n = sub(delta, delta_offset, delta_offset + b - 1), n + 1
|
||
|
delta_offset = delta_offset + b
|
||
|
else
|
||
|
error(("unknown opcode '%02x'"):format(b))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local patched = table.concat(parts)
|
||
|
if patched_length ~= #patched then
|
||
|
error(("expected patched of size %d, got size %d"):format(patched_length, #patched))
|
||
|
end
|
||
|
|
||
|
return patched
|
||
|
end
|
||
|
|
||
|
--- Unpack a single object, populating the output table
|
||
|
--
|
||
|
-- @tparam Reader handle The handle to read from
|
||
|
-- @tparam { [string] = Object } out The populated data
|
||
|
local function unpack_object(handle, out)
|
||
|
local c = handle.read8()
|
||
|
local ty = band(rshift(c, 4), 7)
|
||
|
local size = band(c, 15)
|
||
|
local shift = 4
|
||
|
while band(c, 0x80) ~= 0 do
|
||
|
c = handle.read8()
|
||
|
size = size + lshift(band(c, 0x7f), shift)
|
||
|
shift = shift + 7
|
||
|
end
|
||
|
|
||
|
local data
|
||
|
if ty >= 1 and ty <= 4 then
|
||
|
-- commit/tree/blob/tag
|
||
|
data = get_data(handle, size)
|
||
|
elseif ty == 6 then
|
||
|
-- ofs_delta
|
||
|
data = get_data(handle, size)
|
||
|
error("ofs_delta not yet implemented")
|
||
|
|
||
|
elseif ty == 7 then
|
||
|
-- ref_delta
|
||
|
local base_hash = sha_format:format(handle.read(20):byte(1, 20))
|
||
|
local delta = get_data(handle, size)
|
||
|
|
||
|
local original = out[base_hash]
|
||
|
if not original then error(("cannot find object %d to apply diff"):format(base_hash)) return end
|
||
|
ty = original.ty
|
||
|
data = apply_delta(original.data, delta)
|
||
|
else
|
||
|
error(("unknown object of type '%d'"):format(ty))
|
||
|
end
|
||
|
|
||
|
-- We've got to do these separately. Format doesn't like null bytes
|
||
|
local whole = ("%s %d\0"):format(types[ty], #data) .. data
|
||
|
local sha = sha(whole)
|
||
|
out[sha] = { ty = ty, data = data, sha = sha }
|
||
|
end
|
||
|
|
||
|
local function unpack(handle, progress)
|
||
|
local header = handle.read(4)
|
||
|
if header ~= "PACK" then error("expected PACK, got " .. header, 0) end
|
||
|
|
||
|
local version = handle.read32()
|
||
|
local entries = handle.read32()
|
||
|
|
||
|
local out = {}
|
||
|
for i = 1, entries do
|
||
|
if progress then progress(i, entries) end
|
||
|
check_in()
|
||
|
|
||
|
unpack_object(handle, out)
|
||
|
end
|
||
|
|
||
|
return out
|
||
|
end
|
||
|
|
||
|
local function build_tree(objects, object, prefix, out)
|
||
|
if not prefix then prefix = "" end
|
||
|
if not out then out = {} end
|
||
|
|
||
|
local idx = 1
|
||
|
|
||
|
while idx <= #object do
|
||
|
-- dddddd NAME\0<SHA>
|
||
|
local _, endidx, mode, name = object:find("^(%x+) ([^%z]+)%z", idx)
|
||
|
if not endidx then break end
|
||
|
name = prefix .. name
|
||
|
|
||
|
local sha = object:sub(endidx + 1, endidx + 20):gsub(".", function(x) return ("%02x"):format(string.byte(x)) end)
|
||
|
|
||
|
local entry = objects[sha]
|
||
|
if not entry then error(("cannot find %s %s (%s)"):format(mode, name, sha)) end
|
||
|
|
||
|
if entry.ty == 3 then
|
||
|
out[name] = entry.data
|
||
|
elseif entry.ty == 2 then
|
||
|
build_tree(objects, entry.data, name .. "/", out)
|
||
|
else
|
||
|
error("unknown type for " .. name .. " (" .. sha .. "): " .. get_type(entry))
|
||
|
end
|
||
|
|
||
|
idx = endidx + 21
|
||
|
end
|
||
|
|
||
|
return out
|
||
|
end
|
||
|
|
||
|
local function build_commit(objects, sha)
|
||
|
local commit = objects[sha]
|
||
|
if not commit then error("cannot find commit " .. sha) end
|
||
|
if commit.ty ~= 1 then error("Expected commit, got " .. types[commit.ty]) end
|
||
|
|
||
|
local tree_sha = commit.data:match("tree (%x+)\n")
|
||
|
if not tree_sha then error("Cannot find tree from commit") end
|
||
|
|
||
|
local tree = objects[tree_sha]
|
||
|
if not tree then error("cannot find tree " .. tree_sha) end
|
||
|
if tree.ty ~= 2 then error("Expected tree, got " .. tree[tree.ty]) end
|
||
|
|
||
|
return build_tree(objects, tree.data)
|
||
|
end
|
||
|
|
||
|
return {
|
||
|
reader = reader,
|
||
|
unpack = unpack,
|
||
|
build_tree = build_tree,
|
||
|
build_commit = build_commit,
|
||
|
type = get_type,
|
||
|
}
|
||
|
end
|
||
|
preload["network"] = function(...)
|
||
|
local function pkt_line(msg)
|
||
|
return ("%04x%s\n"):format(5 + #msg, msg)
|
||
|
end
|
||
|
|
||
|
local function pkt_linef(fmt, ...)
|
||
|
return pkt_line(fmt:format(...))
|
||
|
end
|
||
|
|
||
|
local flush_line = "0000"
|
||
|
|
||
|
local function read_pkt_line(handle)
|
||
|
local data = handle.read(4)
|
||
|
if data == nil or data == "" then return nil end
|
||
|
|
||
|
local len = tonumber(data, 16)
|
||
|
if len == nil then
|
||
|
error(("read_pkt_line: cannot convert %q to a number"):format(data))
|
||
|
elseif len == 0 then
|
||
|
return false, data
|
||
|
else
|
||
|
return handle.read(len - 4), data
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function fetch(url, lines, content_type)
|
||
|
if type(lines) == "table" then lines = table.concat(lines) end
|
||
|
|
||
|
local ok, err = http.request(url, lines, {
|
||
|
['User-Agent'] = 'CCGit/1.0',
|
||
|
['Content-Type'] = content_type,
|
||
|
}, true)
|
||
|
|
||
|
if ok then
|
||
|
while true do
|
||
|
local event, event_url, param1, param2 = os.pullEvent()
|
||
|
if event == "http_success" and event_url == url then
|
||
|
return true, param1
|
||
|
elseif event == "http_failure" and event_url == url then
|
||
|
printError("Cannot fetch " .. url .. ": " .. param1)
|
||
|
return false, param2
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
printError("Cannot fetch " .. url .. ": " .. err)
|
||
|
return false, nil
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function force_fetch(...)
|
||
|
local ok, handle, err_handle = fetch(...)
|
||
|
if not ok then
|
||
|
if err_handle then
|
||
|
print(err_handle.getStatusCode())
|
||
|
print(textutils.serialize(err_handle.getResponseHeaders()))
|
||
|
print(err_handle.readAll())
|
||
|
end
|
||
|
error("Cannot fetch", 0)
|
||
|
end
|
||
|
|
||
|
return handle
|
||
|
end
|
||
|
|
||
|
local function receive(handle)
|
||
|
local out = {}
|
||
|
while true do
|
||
|
local line = read_pkt_line(handle)
|
||
|
if line == nil then break end
|
||
|
out[#out + 1] = line
|
||
|
end
|
||
|
|
||
|
handle.close()
|
||
|
return out
|
||
|
end
|
||
|
|
||
|
return {
|
||
|
read_pkt_line = read_pkt_line,
|
||
|
force_fetch = force_fetch,
|
||
|
receive = receive,
|
||
|
|
||
|
pkt_linef = pkt_linef,
|
||
|
flush_line = flush_line,
|
||
|
}
|
||
|
end
|
||
|
preload["deflate"] = function(...)
|
||
|
--[[
|
||
|
(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
of this software and associated documentation files (the "Software"), to deal
|
||
|
in the Software without restriction, including without limitation the rights
|
||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
copies of the Software, and to permit persons to whom the Software is
|
||
|
furnished to do so, subject to the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be included in
|
||
|
all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
THE SOFTWARE.
|
||
|
(end license)
|
||
|
--]]
|
||
|
|
||
|
local assert, error, ipairs, pairs, tostring, type, setmetatable, io, math
|
||
|
= assert, error, ipairs, pairs, tostring, type, setmetatable, io, math
|
||
|
local table_sort, math_max, string_char = table.sort, math.max, string.char
|
||
|
local band, lshift, rshift = bit32.band, bit32.lshift, bit32.rshift
|
||
|
|
||
|
local function make_outstate(outbs)
|
||
|
local outstate = {}
|
||
|
outstate.outbs = outbs
|
||
|
outstate.len = 0
|
||
|
outstate.window = {}
|
||
|
outstate.window_pos = 1
|
||
|
return outstate
|
||
|
end
|
||
|
|
||
|
|
||
|
local function output(outstate, byte)
|
||
|
local window_pos = outstate.window_pos
|
||
|
outstate.outbs(byte)
|
||
|
outstate.len = outstate.len + 1
|
||
|
outstate.window[window_pos] = byte
|
||
|
outstate.window_pos = window_pos % 32768 + 1 -- 32K
|
||
|
end
|
||
|
|
||
|
|
||
|
local function noeof(val)
|
||
|
return assert(val, 'unexpected end of file')
|
||
|
end
|
||
|
|
||
|
local function memoize(f)
|
||
|
return setmetatable({}, {
|
||
|
__index = function(self, k)
|
||
|
local v = f(k)
|
||
|
self[k] = v
|
||
|
return v
|
||
|
end
|
||
|
})
|
||
|
end
|
||
|
|
||
|
-- small optimization (lookup table for powers of 2)
|
||
|
local pow2 = memoize(function(n) return 2^n end)
|
||
|
|
||
|
local function bitstream_from_bytestream(bys)
|
||
|
local buf_byte = 0
|
||
|
local buf_nbit = 0
|
||
|
local o = { type = "bitstream" }
|
||
|
|
||
|
function o:nbits_left_in_byte()
|
||
|
return buf_nbit
|
||
|
end
|
||
|
|
||
|
function o:read(nbits)
|
||
|
nbits = nbits or 1
|
||
|
while buf_nbit < nbits do
|
||
|
local byte = bys()
|
||
|
if not byte then return end -- note: more calls also return nil
|
||
|
buf_byte = buf_byte + lshift(byte, buf_nbit)
|
||
|
buf_nbit = buf_nbit + 8
|
||
|
end
|
||
|
local bits
|
||
|
if nbits == 0 then
|
||
|
bits = 0
|
||
|
elseif nbits == 32 then
|
||
|
bits = buf_byte
|
||
|
buf_byte = 0
|
||
|
else
|
||
|
bits = band(buf_byte, rshift(0xffffffff, 32 - nbits))
|
||
|
buf_byte = rshift(buf_byte, nbits)
|
||
|
end
|
||
|
buf_nbit = buf_nbit - nbits
|
||
|
return bits
|
||
|
end
|
||
|
|
||
|
return o
|
||
|
end
|
||
|
|
||
|
local function get_bitstream(o)
|
||
|
if type(o) == "table" and o.type == "bitstream" then
|
||
|
return o
|
||
|
elseif io.type(o) == 'file' then
|
||
|
return bitstream_from_bytestream(function() local sb = o:read(1) if sb then return sb:byte() end end)
|
||
|
elseif type(o) == "function" then
|
||
|
return bitstream_from_bytestream(o)
|
||
|
else
|
||
|
error 'unrecognized type'
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
local function get_obytestream(o)
|
||
|
local bs
|
||
|
if io.type(o) == 'file' then
|
||
|
bs = function(sbyte) o:write(string_char(sbyte)) end
|
||
|
elseif type(o) == 'function' then
|
||
|
bs = o
|
||
|
else
|
||
|
error('unrecognized type: ' .. tostring(o))
|
||
|
end
|
||
|
return bs
|
||
|
end
|
||
|
|
||
|
|
||
|
local function HuffmanTable(init, is_full)
|
||
|
local t = {}
|
||
|
if is_full then
|
||
|
for val,nbits in pairs(init) do
|
||
|
if nbits ~= 0 then
|
||
|
t[#t+1] = {val=val, nbits=nbits}
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
for i=1,#init-2,2 do
|
||
|
local firstval, nbits, nextval = init[i], init[i+1], init[i+2]
|
||
|
if nbits ~= 0 then
|
||
|
for val=firstval,nextval-1 do
|
||
|
t[#t+1] = {val=val, nbits=nbits}
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
table_sort(t, function(a,b)
|
||
|
return a.nbits == b.nbits and a.val < b.val or a.nbits < b.nbits
|
||
|
end)
|
||
|
|
||
|
-- assign codes
|
||
|
local code = 1 -- leading 1 marker
|
||
|
local nbits = 0
|
||
|
for _, s in ipairs(t) do
|
||
|
if s.nbits ~= nbits then
|
||
|
code = code * pow2[s.nbits - nbits]
|
||
|
nbits = s.nbits
|
||
|
end
|
||
|
s.code = code
|
||
|
code = code + 1
|
||
|
end
|
||
|
|
||
|
local minbits = math.huge
|
||
|
local look = {}
|
||
|
for _, s in ipairs(t) do
|
||
|
minbits = math.min(minbits, s.nbits)
|
||
|
look[s.code] = s.val
|
||
|
end
|
||
|
|
||
|
local msb = function(bits, nbits)
|
||
|
local res = 0
|
||
|
for _ = 1, nbits do
|
||
|
res = lshift(res, 1) + band(bits, 1)
|
||
|
bits = rshift(bits, 1)
|
||
|
end
|
||
|
return res
|
||
|
end
|
||
|
|
||
|
local tfirstcode = memoize(
|
||
|
function(bits) return pow2[minbits] + msb(bits, minbits) end)
|
||
|
|
||
|
function t:read(bs)
|
||
|
local code = 1 -- leading 1 marker
|
||
|
local nbits = 0
|
||
|
while 1 do
|
||
|
if nbits == 0 then -- small optimization (optional)
|
||
|
code = tfirstcode[noeof(bs:read(minbits))]
|
||
|
nbits = nbits + minbits
|
||
|
else
|
||
|
local b = noeof(bs:read())
|
||
|
nbits = nbits + 1
|
||
|
code = code * 2 + b -- MSB first
|
||
|
end
|
||
|
local val = look[code]
|
||
|
if val then
|
||
|
return val
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return t
|
||
|
end
|
||
|
|
||
|
local function parse_zlib_header(bs)
|
||
|
local cm = bs:read(4) -- Compression Method
|
||
|
local cinfo = bs:read(4) -- Compression info
|
||
|
local fcheck = bs:read(5) -- FLaGs: FCHECK (check bits for CMF and FLG)
|
||
|
local fdict = bs:read(1) -- FLaGs: FDICT (present dictionary)
|
||
|
local flevel = bs:read(2) -- FLaGs: FLEVEL (compression level)
|
||
|
local cmf = cinfo * 16 + cm -- CMF (Compresion Method and flags)
|
||
|
local flg = fcheck + fdict * 32 + flevel * 64 -- FLaGs
|
||
|
|
||
|
if cm ~= 8 then -- not "deflate"
|
||
|
error("unrecognized zlib compression method: " .. cm)
|
||
|
end
|
||
|
if cinfo > 7 then
|
||
|
error("invalid zlib window size: cinfo=" .. cinfo)
|
||
|
end
|
||
|
local window_size = 2^(cinfo + 8)
|
||
|
|
||
|
if (cmf*256 + flg) % 31 ~= 0 then
|
||
|
error("invalid zlib header (bad fcheck sum)")
|
||
|
end
|
||
|
|
||
|
if fdict == 1 then
|
||
|
error("FIX:TODO - FDICT not currently implemented")
|
||
|
local dictid_ = bs:read(32)
|
||
|
end
|
||
|
|
||
|
return window_size
|
||
|
end
|
||
|
|
||
|
local function parse_huffmantables(bs)
|
||
|
local hlit = bs:read(5) -- # of literal/length codes - 257
|
||
|
local hdist = bs:read(5) -- # of distance codes - 1
|
||
|
local hclen = noeof(bs:read(4)) -- # of code length codes - 4
|
||
|
|
||
|
local ncodelen_codes = hclen + 4
|
||
|
local codelen_init = {}
|
||
|
local codelen_vals = {
|
||
|
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}
|
||
|
for i=1,ncodelen_codes do
|
||
|
local nbits = bs:read(3)
|
||
|
local val = codelen_vals[i]
|
||
|
codelen_init[val] = nbits
|
||
|
end
|
||
|
local codelentable = HuffmanTable(codelen_init, true)
|
||
|
|
||
|
local function decode(ncodes)
|
||
|
local init = {}
|
||
|
local nbits
|
||
|
local val = 0
|
||
|
while val < ncodes do
|
||
|
local codelen = codelentable:read(bs)
|
||
|
--FIX:check nil?
|
||
|
local nrepeat
|
||
|
if codelen <= 15 then
|
||
|
nrepeat = 1
|
||
|
nbits = codelen
|
||
|
elseif codelen == 16 then
|
||
|
nrepeat = 3 + noeof(bs:read(2))
|
||
|
-- nbits unchanged
|
||
|
elseif codelen == 17 then
|
||
|
nrepeat = 3 + noeof(bs:read(3))
|
||
|
nbits = 0
|
||
|
elseif codelen == 18 then
|
||
|
nrepeat = 11 + noeof(bs:read(7))
|
||
|
nbits = 0
|
||
|
else
|
||
|
error 'ASSERT'
|
||
|
end
|
||
|
for _ = 1, nrepeat do
|
||
|
init[val] = nbits
|
||
|
val = val + 1
|
||
|
end
|
||
|
end
|
||
|
local huffmantable = HuffmanTable(init, true)
|
||
|
return huffmantable
|
||
|
end
|
||
|
|
||
|
local nlit_codes = hlit + 257
|
||
|
local ndist_codes = hdist + 1
|
||
|
|
||
|
local littable = decode(nlit_codes)
|
||
|
local disttable = decode(ndist_codes)
|
||
|
|
||
|
return littable, disttable
|
||
|
end
|
||
|
|
||
|
|
||
|
local tdecode_len_base
|
||
|
local tdecode_len_nextrabits
|
||
|
local tdecode_dist_base
|
||
|
local tdecode_dist_nextrabits
|
||
|
local function parse_compressed_item(bs, outstate, littable, disttable)
|
||
|
local val = littable:read(bs)
|
||
|
if val < 256 then -- literal
|
||
|
output(outstate, val)
|
||
|
elseif val == 256 then -- end of block
|
||
|
return true
|
||
|
else
|
||
|
if not tdecode_len_base then
|
||
|
local t = {[257]=3}
|
||
|
local skip = 1
|
||
|
for i=258,285,4 do
|
||
|
for j=i,i+3 do t[j] = t[j-1] + skip end
|
||
|
if i ~= 258 then skip = skip * 2 end
|
||
|
end
|
||
|
t[285] = 258
|
||
|
tdecode_len_base = t
|
||
|
end
|
||
|
if not tdecode_len_nextrabits then
|
||
|
local t = {}
|
||
|
for i=257,285 do
|
||
|
local j = math_max(i - 261, 0)
|
||
|
t[i] = rshift(j, 2)
|
||
|
end
|
||
|
t[285] = 0
|
||
|
tdecode_len_nextrabits = t
|
||
|
end
|
||
|
local len_base = tdecode_len_base[val]
|
||
|
local nextrabits = tdecode_len_nextrabits[val]
|
||
|
local extrabits = bs:read(nextrabits)
|
||
|
local len = len_base + extrabits
|
||
|
|
||
|
if not tdecode_dist_base then
|
||
|
local t = {[0]=1}
|
||
|
local skip = 1
|
||
|
for i=1,29,2 do
|
||
|
for j=i,i+1 do t[j] = t[j-1] + skip end
|
||
|
if i ~= 1 then skip = skip * 2 end
|
||
|
end
|
||
|
tdecode_dist_base = t
|
||
|
end
|
||
|
if not tdecode_dist_nextrabits then
|
||
|
local t = {}
|
||
|
for i=0,29 do
|
||
|
local j = math_max(i - 2, 0)
|
||
|
t[i] = rshift(j, 1)
|
||
|
end
|
||
|
tdecode_dist_nextrabits = t
|
||
|
end
|
||
|
local dist_val = disttable:read(bs)
|
||
|
local dist_base = tdecode_dist_base[dist_val]
|
||
|
local dist_nextrabits = tdecode_dist_nextrabits[dist_val]
|
||
|
local dist_extrabits = bs:read(dist_nextrabits)
|
||
|
local dist = dist_base + dist_extrabits
|
||
|
|
||
|
for _ = 1,len do
|
||
|
local pos = (outstate.window_pos - 1 - dist) % 32768 + 1 -- 32K
|
||
|
output(outstate, assert(outstate.window[pos], 'invalid distance'))
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
|
||
|
local function parse_block(bs, outstate)
|
||
|
local bfinal = bs:read(1)
|
||
|
local btype = bs:read(2)
|
||
|
|
||
|
local BTYPE_NO_COMPRESSION = 0
|
||
|
local BTYPE_FIXED_HUFFMAN = 1
|
||
|
local BTYPE_DYNAMIC_HUFFMAN = 2
|
||
|
local _BTYPE_RESERVED = 3
|
||
|
|
||
|
if btype == BTYPE_NO_COMPRESSION then
|
||
|
bs:read(bs:nbits_left_in_byte())
|
||
|
local len = bs:read(16)
|
||
|
local _nlen = noeof(bs:read(16))
|
||
|
|
||
|
for i=1,len do
|
||
|
local by = noeof(bs:read(8))
|
||
|
output(outstate, by)
|
||
|
end
|
||
|
elseif btype == BTYPE_FIXED_HUFFMAN or btype == BTYPE_DYNAMIC_HUFFMAN then
|
||
|
local littable, disttable
|
||
|
if btype == BTYPE_DYNAMIC_HUFFMAN then
|
||
|
littable, disttable = parse_huffmantables(bs)
|
||
|
else
|
||
|
littable = HuffmanTable {0,8, 144,9, 256,7, 280,8, 288,nil}
|
||
|
disttable = HuffmanTable {0,5, 32,nil}
|
||
|
end
|
||
|
|
||
|
repeat
|
||
|
local is_done = parse_compressed_item(
|
||
|
bs, outstate, littable, disttable)
|
||
|
until is_done
|
||
|
else
|
||
|
error('unrecognized compression type '..btype)
|
||
|
end
|
||
|
|
||
|
return bfinal ~= 0
|
||
|
end
|
||
|
|
||
|
|
||
|
local function inflate(t)
|
||
|
local bs = get_bitstream(t.input)
|
||
|
local outbs = get_obytestream(t.output)
|
||
|
local outstate = make_outstate(outbs)
|
||
|
|
||
|
repeat
|
||
|
local is_final = parse_block(bs, outstate)
|
||
|
until is_final
|
||
|
end
|
||
|
|
||
|
local function adler32(byte, crc)
|
||
|
local s1 = crc % 65536
|
||
|
local s2 = (crc - s1) / 65536
|
||
|
s1 = (s1 + byte) % 65521
|
||
|
s2 = (s2 + s1) % 65521
|
||
|
return s2*65536 + s1
|
||
|
end -- 65521 is the largest prime smaller than 2^16
|
||
|
|
||
|
local function inflate_zlib(t)
|
||
|
local bs = get_bitstream(t.input)
|
||
|
local outbs = get_obytestream(t.output)
|
||
|
local disable_crc = t.disable_crc
|
||
|
if disable_crc == nil then disable_crc = false end
|
||
|
|
||
|
local _window_size = parse_zlib_header(bs)
|
||
|
|
||
|
local data_adler32 = 1
|
||
|
|
||
|
inflate {
|
||
|
input=bs,
|
||
|
output = disable_crc and outbs or function(byte)
|
||
|
data_adler32 = adler32(byte, data_adler32)
|
||
|
outbs(byte)
|
||
|
end,
|
||
|
len = t.len,
|
||
|
}
|
||
|
|
||
|
bs:read(bs:nbits_left_in_byte())
|
||
|
|
||
|
local b3 = bs:read(8)
|
||
|
local b2 = bs:read(8)
|
||
|
local b1 = bs:read(8)
|
||
|
local b0 = bs:read(8)
|
||
|
local expected_adler32 = ((b3*256 + b2)*256 + b1)*256 + b0
|
||
|
|
||
|
if not disable_crc then
|
||
|
if data_adler32 ~= expected_adler32 then
|
||
|
error('invalid compressed data--crc error')
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return {
|
||
|
inflate = inflate,
|
||
|
inflate_zlib = inflate_zlib,
|
||
|
}
|
||
|
end
|
||
|
preload["clone"] = function(...)
|
||
|
--- Git clone in Lua, from the bottom up
|
||
|
--
|
||
|
-- http://stefan.saasen.me/articles/git-clone-in-haskell-from-the-bottom-up/#the_clone_process
|
||
|
-- https://github.com/creationix/lua-git
|
||
|
|
||
|
do -- metis loader
|
||
|
local modules = {
|
||
|
["metis.argparse"] = "src/metis/argparse.lua",
|
||
|
["metis.crypto.sha1"] = "src/metis/crypto/sha1.lua",
|
||
|
["metis.timer"] = "src/metis/timer.lua",
|
||
|
}
|
||
|
package.loaders[#package.loaders + 1] = function(name)
|
||
|
local path = modules[name]
|
||
|
if not path then return nil, "not a metis module" end
|
||
|
|
||
|
local local_path = "/.cache/metis/ae11085f261e5b506654162c80d21954c0d54e5e/" .. path
|
||
|
if not fs.exists(local_path) then
|
||
|
local url = "https://raw.githubusercontent.com/SquidDev-CC/metis/ae11085f261e5b506654162c80d21954c0d54e5e/" .. path
|
||
|
local request, err = http.get(url)
|
||
|
if not request then return nil, "Cannot download " .. url .. ": " .. err end
|
||
|
|
||
|
local out = fs.open(local_path, "w")
|
||
|
out.write(request.readAll())
|
||
|
out.close()
|
||
|
|
||
|
request.close()
|
||
|
end
|
||
|
|
||
|
|
||
|
local fn, err = loadfile(local_path, nil, _ENV)
|
||
|
if fn then return fn, local_path else return nil, err end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local network = require "network"
|
||
|
local objects = require "objects"
|
||
|
|
||
|
local url, name = ...
|
||
|
if not url or url == "-h" or url == "--help" then error("clone.lua URL [name]", 0) end
|
||
|
|
||
|
if url:sub(-1) == "/" then url = url:sub(1, -2) end
|
||
|
name = name or fs.getName(url):gsub("%.git$", "")
|
||
|
|
||
|
local destination = shell.resolve(name)
|
||
|
if fs.exists(destination) then
|
||
|
error(("%q already exists"):format(name), 0)
|
||
|
end
|
||
|
|
||
|
local function report(msg)
|
||
|
local last = ""
|
||
|
for line in msg:gmatch("[^\n]+") do last = line end
|
||
|
if replaceLine then
|
||
|
term.setCursorPos(1, select(2, term.getCursorPos()))
|
||
|
term.clearLine()
|
||
|
term.write(last)
|
||
|
else
|
||
|
print(last)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local head
|
||
|
do -- Request a list of all refs
|
||
|
report("Cloning from " .. url)
|
||
|
|
||
|
local handle = network.force_fetch(url .. "/info/refs?service=git-upload-pack")
|
||
|
local res = network.receive(handle)
|
||
|
|
||
|
local sha_ptrn = ("%x"):rep(40)
|
||
|
|
||
|
local caps = {}
|
||
|
local refs = {}
|
||
|
for i = 1, #res do
|
||
|
local line = res[i]
|
||
|
if line ~= false and line:sub(1, 1) ~= "#" then
|
||
|
local sha, name = line:match("(" .. sha_ptrn .. ") ([^%z\n]+)")
|
||
|
if sha and name then
|
||
|
refs[name] = sha
|
||
|
|
||
|
local capData = line:match("%z([^\n]+)\n")
|
||
|
if capData then
|
||
|
for cap in (capData .. " "):gmatch("%S+") do
|
||
|
local eq = cap:find("=")
|
||
|
if eq then
|
||
|
caps[cap:sub(1, eq - 1)] = cap:sub(eq + 1)
|
||
|
else
|
||
|
caps[cap] = true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
printError("Unexpected line: " .. line)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
head = refs['HEAD'] or refs['refs/heads/master'] or error("Cannot find master", 0)
|
||
|
|
||
|
if not caps['shallow'] then error("Server does not support shallow fetching", 0) end
|
||
|
|
||
|
-- TODO: Handle both. We don't even need the side-band really?
|
||
|
if not caps['side-band-64k'] then error("Server does not support side band", 0) end
|
||
|
end
|
||
|
|
||
|
do -- Now actually perform the clone
|
||
|
local handle = network.force_fetch(url .. "/git-upload-pack", {
|
||
|
network.pkt_linef("want %s side-band-64k shallow", head),
|
||
|
network.pkt_linef("deepen 1"),
|
||
|
network.flush_line,
|
||
|
network.pkt_linef("done"),
|
||
|
}, "application/x-git-upload-pack-request")
|
||
|
|
||
|
local pack, head = {}, nil
|
||
|
while true do
|
||
|
local line = network.read_pkt_line(handle)
|
||
|
if line == nil then break end
|
||
|
|
||
|
if line == false or line == "NAK\n" then
|
||
|
-- Skip
|
||
|
elseif line:byte(1) == 1 then
|
||
|
table.insert(pack, line:sub(2))
|
||
|
elseif line:byte(1) == 2 or line:byte(1) == 3 then
|
||
|
report(line:sub(2):gsub("\r", "\n"))
|
||
|
elseif line:find("^shallow ") then
|
||
|
head = line:sub(#("shallow ") + 1)
|
||
|
else
|
||
|
printError("Unknown line: " .. tostring(line))
|
||
|
end
|
||
|
end
|
||
|
handle.close()
|
||
|
|
||
|
local stream = objects.reader(table.concat(pack))
|
||
|
local objs = objects.unpack(stream, function(x, n)
|
||
|
report(("Extracting %d/%d (%.2f%%)"):format(x, n, x/n*100))
|
||
|
end)
|
||
|
stream.close()
|
||
|
|
||
|
if not head then error("Cannot find HEAD commit", 0) end
|
||
|
|
||
|
for k, v in pairs(objects.build_commit(objs, head)) do
|
||
|
local out = fs.open(fs.combine(destination, fs.combine(k, "")), "wb")
|
||
|
out.write(v)
|
||
|
out.close()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
report(("Cloned to %q"):format(name))
|
||
|
print()
|
||
|
end
|
||
|
return preload["clone"](...)
|