mirror of
https://github.com/osmarks/random-stuff
synced 2024-12-29 11:30:35 +00:00
448 lines
13 KiB
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!"
|