random-stuff/computercraft/crane.lua

448 lines
13 KiB
Lua

--[[
License for the LZW compression:
MIT License
Copyright (c) 2016 Rochet2
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.
]]
local util_raw = [[
local function canonicalize(path)
return fs.combine(path, "")
end
local function segments(path)
if canonicalize(path) == "" then return {} end
local segs, rest = {}, path
repeat
table.insert(segs, 1, fs.getName(rest))
rest = fs.getDir(rest)
until rest == ""
return segs
end
local function slice(tab, start, end_)
return {table.unpack(tab, start, end_)}
end
local function compact_serialize(x)
local t = type(x)
if t == "number" then
return tostring(x)
elseif t == "string" then
return textutils.serialise(x)
elseif t == "table" then
local out = "{"
for k, v in pairs(x) do
out = out .. string.format("[%s]=%s,", compact_serialize(k), compact_serialize(v))
end
return out .. "}"
elseif t == "boolean" then
return tostring(x)
else
error("Unsupported type " .. t)
end
end
local function drop_last(t)
local clone = slice(t)
local length = #clone
local v = clone[length]
clone[length] = nil
return clone, v
end
]]
local util = loadstring(util_raw .. "\nreturn {segments = segments, slice = slice, drop_last = drop_last, compact_serialize = compact_serialize}")()
local runtime = util_raw .. [[
local savepath = ".crane-persistent/" .. fname
-- Simple string operations
local function starts_with(s, with)
return string.sub(s, 1, #with) == with
end
local function ends_with(s, with)
return string.sub(s, -#with, -1) == with
end
local function contains(s, subs)
return string.find(s, subs) ~= nil
end
local function copy_some_keys(keys)
return function(from)
local new = {}
for _, key_to_copy in pairs(keys) do
local x = from[key_to_copy]
if type(x) == "table" then
x = copy(x)
end
new[key_to_copy] = x
end
return new
end
end
local function find_path(image, path)
local focus = image
local path = path
if type(path) == "string" then path = segments(path) end
for _, seg in pairs(path) do
if type(focus) ~= "table" then error("Path segment " .. seg .. " is nonexistent or not a directory; full path " .. compact_serialize(path)) end
focus = focus[seg]
end
return focus
end
local function get_parent(image, path)
local init, last = drop_last(segments(path))
local parent = find_path(image, init) or image
return parent, last
end
-- magic from http://lua-users.org/wiki/SplitJoin
-- split string into lines
local function lines(str)
local t = {}
local function helper(line)
table.insert(t, line)
return ""
end
helper((str:gsub("(.-)\r?\n", helper)))
return t
end
local function make_read_handle(text, binary)
local lines = lines(text)
local h = {}
local line = 0
function h.close() end
if not binary then
function h.readLine() line = line + 1 return lines[line] end
function h.readAll() return text end
else
local remaining = text
function h.read()
local by = string.byte(remaining:sub(1, 1))
remaining = remaining:sub(2)
return by
end
end
return h
end
local function make_write_handle(writefn, binary)
local h = {}
function h.close() end
function h.flush() end
if not binary then
function h.write(t) return writefn(t) end
function h.writeLine(t) return writefn(t .. "\n") end
else
function h.write(b) return writefn(string.char(b)) end
end
return h
end
local function mount_image(i)
local options = i.options
local image = i.tree
local filesystem = copy_some_keys {"getName", "combine", "getDir"} (_G.fs)
function filesystem.getFreeSpace()
return math.huge -- well, it's in-memory...
end
function filesystem.exists(path)
return find_path(image, path) ~= nil
end
function filesystem.isDir(path)
return type(find_path(image, path)) == "table"
end
function filesystem.makeDir(path)
local p, n = get_parent(image, path)
p[n] = {}
end
function filesystem.delete(path)
local p, n = get_parent(image, path)
p[n] = nil
end
function filesystem.copy(from, to)
local pf, nf = get_parent(image, from)
local contents = pf[nf]
local pt, nt = get_parent(image, to)
pt[nt] = contents
end
function filesystem.move(from, to)
filesystem.copy(from, to)
local pf, nf = get_parent(image, from)
pf[nf] = nil
end
function filesystem.contents(path)
return find_path(image, path)
end
function filesystem.list(path)
local out = {}
local dir = find_path(image, path)
for k, v in pairs(dir) do table.insert(out, k) end
return out
end
function filesystem.open(path, mode)
local parent, childname = get_parent(image, path)
local node = parent[childname]
local is_binary = ends_with(mode, "b")
if starts_with(mode, "r") then
if type(node) ~= "string" then error(path .. ": not a file!") end
return make_read_handle(node, is_binary)
elseif starts_with(mode, "a") or starts_with(mode, "w") then
local function writer(str)
parent[childname] = parent[childname] .. str
if options.save_on_change then filesystem.save() end
end
if not starts_with(mode, "a") or node == nil then parent[childname] = "" end
return make_write_handle(writer, is_binary)
end
end
function filesystem.find(wildcard)
-- Taken from Harbor: https://github.com/hugeblank/harbor/blob/master/harbor.lua
-- Body donated to harbor by gollark, from PotatOS, and apparently indirectly from cclite:
-- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua
local function recurse_spec(results, path, spec)
local segment = spec:match('([^/]*)'):gsub('/', '')
local pattern = '^' .. segment:gsub('[*]', '.+'):gsub('?', '.') .. '$'
if filesystem.isDir(path) then
for _, file in ipairs(filesystem.list(path)) do
if file:match(pattern) then
local f = filesystem.combine(path, file)
if filesystem.isDir(f) then
recurse_spec(results, f, spec:sub(#segment + 2))
end
if spec == segment then
table.insert(results, f)
end
end
end
end
end
local results = {}
recurse_spec(results, '', wildcard)
return results
end
function filesystem.getDrive()
return "crane-vfs-" .. fname
end
function filesystem.isReadOnly(path)
return false
end
local fo = fs.open
function filesystem.save()
local f = fo(savepath, "w")
f.write(compact_serialize(i))
f.close()
end
return filesystem
end
local function deepmerge(t1, t2)
local out = {}
for k, v in pairs(t1) do
local onother = t2[k]
if type(v) == "table" and type(onother) == "table" then
out[k] = deepmerge(v, onother)
else
out[k] = v
end
end
for k, v in pairs(t2) do
if not out[k] then
out[k] = v
end
end
return out
end
local cli_args = {...}
local f = fs.open("/rom/apis/io.lua", "r") -- bodge reloading IO library
local IO_API_code = f.readAll()
f.close()
local function load_API(code, env, name)
local e = setmetatable({}, { __index = env })
load(code, "@" .. name, "t", env)()
env[name] = e
end
local function replacement_require(path)
return dofile(path)
end
local function execute(image, filename)
local image = image
if fs.exists(savepath) then
local f = fs.open(savepath, "r")
image = deepmerge(image, textutils.unserialise(f.readAll()))
end
local f = mount_image(image)
local env = setmetatable({ fs = f, rawfs = _G.fs, require = replacement_require,
os = setmetatable({}, { __index = _ENV.os }), { __index = _ENV })
load_API(IO_API_code, env, "io")
local func, err = load(f.contents(filename), "@" .. filename, "t", env)
if err then error(err)
else
env.os.reboot = function() func() end
return func(unpack(cli_args))
end
end
]]
-- LZW Compressor
local a=string.char;local type=type;local b=string.sub;local c=table.concat;local d={}local e={}for f=0,255 do local g,h=a(f),a(f,0)d[g]=h;e[h]=g end;local function i(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[j]=a(l,m)l=l+1;return k,l,m end;local function compress(n)if type(n)~="string"then return nil,"string expected, got "..type(n)end;local o=#n;if o<=1 then return"u"..n end;local k={}local l,m=0,1;local p={"c"}local q=1;local r=2;local s=""for f=1,o do local t=b(n,f,f)local u=s..t;if not(d[u]or k[u])then local v=d[s]or k[s]if not v then return nil,"algorithm error, could not fetch word"end;p[r]=v;q=q+#v;r=r+1;if o<=q then return"u"..n end;k,l,m=i(u,k,l,m)s=t else s=u end end;p[r]=d[s]or k[s]q=q+#p[r]r=r+1;if o<=q then return"u"..n end;return c(p)end
local wrapper = [[local function y(b)local c="-"local d="__#"..math.random(0,10000)local e="\0";return b:gsub(c,d):gsub(e,c):gsub(d,e)end;local z="decompression failure; please redownload or contact developer";local a=string.char;local type=type;local b=string.sub;local c=table.concat;local d={}local e={}for f=0,255 do local g,h=a(f),a(f,0)d[g]=h;e[h]=g end;local function i(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[j]=a(l,m)l=l+1;return k,l,m end;local function n(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[a(l,m)]=j;l=l+1;return k,l,m end;local function dec(o)if type(o)~="string"then return nil,z end;if#o<1 then return nil,z end;local p=b(o,1,1)if p=="u"then return b(o,2)elseif p~="c"then return nil,z end;o=b(o,2)local q=#o;if q<2 then return nil,z end;local k={}local l,m=0,1;local r={}local s=1;local t=b(o,1,2)r[s]=e[t]or k[t]s=s+1;for f=3,q,2 do local u=b(o,f,f+1)local v=e[t]or k[t]if not v then return nil,z end;local w=e[u]or k[u]if w then r[s]=w;s=s+1;k,l,m=n(v..b(w,1,1),k,l,m)else local x=v..b(v,1,1)r[s]=x;s=s+1;k,l,m=n(x,k,l,m)end;t=u end;return c(r)end;local o,e=dec(y(%s));if e then error(e) end;load(o,"@loader","t",_ENV)(...)]]
local function encode_nuls(txt)
local replace = "\0"
local temp_replacement = ("__#%d#__"):format(math.random(-131072, 131072))
local replace_with = "-"
return txt:gsub(replace, temp_replacement):gsub(replace_with, replace):gsub(temp_replacement, replace_with)
end
local function compress_code(c)
local comp = encode_nuls(compress(c))
local txt = string.format("%q", comp):gsub("\\(%d%d%d)([^0-9])", function(x, y) return string.format("\\%d%s", tonumber(x), y) end)
local out = wrapper:format(txt)
--print(loadstring(out)())
return out
end
local function find_imports(code)
local imports = {}
for i in code:gmatch "%-%-| CRANE ADD [\"'](.-)[\"']" do
table.insert(imports, i)
print("Detected explicit import", i)
end
return imports
end
local function add(path, content, tree)
local segs, last = util.drop_last(util.segments(path))
local deepest = tree
for k, seg in pairs(segs) do
if not deepest[seg] then
deepest[seg] = {}
end
deepest = deepest[seg]
end
deepest[last] = content
end
local function load_from_root(file, tree)
print("Adding", file)
if not fs.exists(file) then error(file .. " does not exist.") end
if fs.isDir(file) then
for _, f in pairs(fs.list(file)) do
load_from_root(fs.combine(file, f), tree)
end
return
end
local f = fs.open(file, "r")
local c = f.readAll()
f.close()
add(file, c, tree)
local imports = find_imports(c)
for _, i in pairs(imports) do
load_from_root(i, tree)
end
end
local args = {...}
if #args < 2 then
error([[Usage:
crane [output] [bundle startup] [other files to bundle] ]])
end
local root = args[2]
local ftree = {}
for _, wildcard in pairs(util.slice(args, 2)) do
for _, possibility in pairs(fs.find(wildcard)) do
load_from_root(possibility, ftree)
end
end
local function minify(code)
local url = "https://osmarks.tk/luamin/"
http.request(url, code)
while true do
local event, result_url, handle = os.pullEvent()
if event == "http_success" then
local text = handle.readAll()
handle.close()
return text
elseif event == "http_failure" then
local text = handle.readAll()
handle.close()
error(text)
end
end
end
ftree[root] = minify(ftree[root])
local serialized_tree = util.compact_serialize({
tree = ftree,
options = {
save_on_change = true
}
})
local function shortest(s1, s2)
if #s1 < #s2 then return s1 else return s2 end
end
local output = minify(([[
local fname = %s
%s
local image = %s
return execute(image, fname)
]]):format(util.compact_serialize(root), runtime, serialized_tree))
local f = fs.open(args[1], "w")
f.write("--| CRANE BUNDLE v2\n" .. shortest(compress_code(output), output))
f.close()
print "Done!"