forked from osmarks/potatOS
initial commit of vaguely working ish build
This commit is contained in:
330
src/lib/binary-serialization.lua
Normal file
330
src/lib/binary-serialization.lua
Normal file
@@ -0,0 +1,330 @@
|
||||
-- by "Yevano", tweaked due to issues with signed integer handling
|
||||
--[[
|
||||
BLODS - Binary Lua Object (De)Serialization
|
||||
]]
|
||||
|
||||
--[[
|
||||
Save on table access.
|
||||
]]
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local loadstring = loadstring
|
||||
local mathabs = math.abs
|
||||
local mathfloor = math.floor
|
||||
local mathfrexp = math.frexp
|
||||
local mathmodf = math.modf
|
||||
local mathpow = math.pow
|
||||
local stringbyte = string.byte
|
||||
local stringchar = string.char
|
||||
local stringdump = string.dump
|
||||
local stringsub = string.sub
|
||||
local tableconcat = table.concat
|
||||
|
||||
--[[
|
||||
Float conversions. Modified from http://snippets.luacode.org/snippets/IEEE_float_conversion_144.
|
||||
]]
|
||||
local function double2str(value)
|
||||
local s=value<0 and 1 or 0
|
||||
if mathabs(value)==1/0 then
|
||||
return (s==1 and "\0\0\0\0\0\0\240\255" or "\0\0\0\0\0\0\240\127")
|
||||
end
|
||||
if value~=value then
|
||||
return "\170\170\170\170\170\170\250\255"
|
||||
end
|
||||
local fr,exp=mathfrexp(mathabs(value))
|
||||
fr,exp=fr*2,exp-1
|
||||
exp=exp+1023
|
||||
return tableconcat({stringchar(mathfloor(fr*2^52)%256),
|
||||
stringchar(mathfloor(fr*2^44)%256),
|
||||
stringchar(mathfloor(fr*2^36)%256),
|
||||
stringchar(mathfloor(fr*2^28)%256),
|
||||
stringchar(mathfloor(fr*2^20)%256),
|
||||
stringchar(mathfloor(fr*2^12)%256),
|
||||
stringchar(mathfloor(fr*2^4)%16+mathfloor(exp)%16*16),
|
||||
stringchar(mathfloor(exp/2^4)%128+128*s)})
|
||||
end
|
||||
|
||||
local function str2double(str)
|
||||
local fr=stringbyte(str, 1)/2^52+stringbyte(str, 2)/2^44+stringbyte(str, 3)/2^36+stringbyte(str, 4)/2^28+stringbyte(str, 5)/2^20+stringbyte(str, 6)/2^12+(stringbyte(str, 7)%16)/2^4+1
|
||||
local exp=(stringbyte(str, 8)%128)*16+mathfloor(str:byte(7)/16)-1023
|
||||
local s=mathfloor(stringbyte(str, 8)/128)
|
||||
if exp==1024 then
|
||||
return fr==1 and (1-2*s)/0 or 0/0
|
||||
end
|
||||
return (1-2*s)*fr*2^exp
|
||||
end
|
||||
|
||||
--[[
|
||||
Integer conversions. Taken from http://lua-users.org/wiki/ReadWriteFormat.
|
||||
Modified to support signed ints.
|
||||
]]
|
||||
|
||||
local function signedstringtonumber(str)
|
||||
local function _b2n(exp, num, digit, ...)
|
||||
if not digit then return num end
|
||||
return _b2n(exp*256, num + digit*exp, ...)
|
||||
end
|
||||
return _b2n(256, stringbyte(str, 1, -1)) - mathpow(2, #str * 8 - 1)
|
||||
end
|
||||
|
||||
local function stringtonumber(str)
|
||||
local function _b2n(exp, num, digit, ...)
|
||||
if not digit then return num end
|
||||
return _b2n(exp*256, num + digit*exp, ...)
|
||||
end
|
||||
return _b2n(256, stringbyte(str, 1, -1))
|
||||
end
|
||||
|
||||
local function numbertobytes(num, width)
|
||||
local function _n2b(width, num, rem)
|
||||
rem = rem * 256
|
||||
if width == 0 then return rem end
|
||||
return rem, _n2b(width-1, mathmodf(num/256))
|
||||
end
|
||||
return stringchar(_n2b(width-1, mathmodf((num)/256)))
|
||||
end
|
||||
|
||||
local function log2(x)
|
||||
return math.log10(x) / math.log10(2)
|
||||
end
|
||||
|
||||
--[[
|
||||
(De)Serialization for Lua types.
|
||||
]]
|
||||
|
||||
local function intWidth(int)
|
||||
local out = math.ceil((log2(int) + 1) / 8)
|
||||
if out == math.huge or out == -math.huge then return 1 end
|
||||
return out
|
||||
end
|
||||
|
||||
local types = {
|
||||
boolean = "b",
|
||||
double = "d",
|
||||
posinteger = "p",
|
||||
neginteger = "n",
|
||||
string = "s",
|
||||
table = "t",
|
||||
["function"] = "f",
|
||||
["nil"] = "_"
|
||||
}
|
||||
|
||||
local serialization = { }
|
||||
local deserialization = { }
|
||||
|
||||
function serialization.boolean(obj)
|
||||
return obj and "\1" or "\0"
|
||||
end
|
||||
|
||||
function serialization.double(obj)
|
||||
return double2str(obj)
|
||||
end
|
||||
|
||||
function serialization.integer(obj)
|
||||
local width = intWidth(obj)
|
||||
return stringchar(width) .. numbertobytes(obj, width)
|
||||
end
|
||||
|
||||
function serialization.string(obj)
|
||||
local len = #obj
|
||||
local width = intWidth(len)
|
||||
return tableconcat({ stringchar(width), numbertobytes(len, width), obj })
|
||||
end
|
||||
|
||||
serialization["function"] = function(obj)
|
||||
local ok, s = pcall(stringdump, obj)
|
||||
if not ok then return "_" end
|
||||
return numbertobytes(#s, 4) .. s
|
||||
end
|
||||
|
||||
function deserialization.b(idx, ser)
|
||||
local ret = stringsub(ser[1], idx, idx) == "\1"
|
||||
return ret, idx + 1
|
||||
end
|
||||
|
||||
function deserialization.d(idx, ser)
|
||||
local ret = str2double(stringsub(ser[1], idx, idx + 8))
|
||||
return ret, idx + 8
|
||||
end
|
||||
|
||||
function deserialization.p(idx, ser)
|
||||
local width = stringtonumber(stringsub(ser[1], idx, idx))
|
||||
local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
|
||||
return ret, idx + width + 1
|
||||
end
|
||||
|
||||
function deserialization.n(idx, ser)
|
||||
local width = stringtonumber(stringsub(ser[1], idx, idx))
|
||||
local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
|
||||
return -ret, idx + width + 1
|
||||
end
|
||||
|
||||
function deserialization.s(idx, ser)
|
||||
local width = stringtonumber(stringsub(ser[1], idx, idx))
|
||||
local len = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
|
||||
local ret = stringsub(ser[1], idx + width + 1, idx + width + len)
|
||||
return ret, idx + width + len + 1
|
||||
end
|
||||
|
||||
function deserialization.f(idx, ser)
|
||||
local len = stringtonumber(stringsub(ser[1], idx, idx + 3))
|
||||
local ret = loadstring(stringsub(ser[1], idx + 4, idx + len + 3))
|
||||
return ret, idx + len + 4
|
||||
end
|
||||
|
||||
function deserialization._(idx, ser)
|
||||
return nil, idx
|
||||
end
|
||||
|
||||
local function yield()
|
||||
os.queueEvent ""
|
||||
os.pullEvent ""
|
||||
end
|
||||
|
||||
if not os.queueEvent then yield = function() end end
|
||||
|
||||
function serialize(obj)
|
||||
-- State vars.
|
||||
local ntables = 1
|
||||
local tables = { }
|
||||
local tableIDs = { }
|
||||
local tableSerial = { }
|
||||
|
||||
-- Internal recursive function.
|
||||
local function serialize(obj)
|
||||
yield()
|
||||
local t = type(obj)
|
||||
if t == "table" then
|
||||
local len = #obj
|
||||
|
||||
if tables[obj] then
|
||||
-- We already serialized this table. Just return the id.
|
||||
return tableIDs[obj]
|
||||
end
|
||||
|
||||
-- Insert table info.
|
||||
local id = ntables
|
||||
tables[obj] = true
|
||||
local width = intWidth(ntables)
|
||||
local ser = "t" .. numbertobytes(width, 1) .. numbertobytes(ntables, width)
|
||||
tableIDs[obj] = ser
|
||||
|
||||
-- Important to increment here so tables inside this one don't use the same id.
|
||||
ntables = ntables + 1
|
||||
|
||||
-- Serialize the table.
|
||||
local serialConcat = { }
|
||||
|
||||
-- Array part.
|
||||
for i = 1, len do
|
||||
if obj[i] == nil then
|
||||
len = i - 1
|
||||
break
|
||||
end
|
||||
serialConcat[#serialConcat + 1] = serialize(obj[i])
|
||||
end
|
||||
serialConcat[#serialConcat + 1] = "\0"
|
||||
|
||||
-- Table part.
|
||||
for k, v in pairs(obj) do
|
||||
if type(k) ~= "number" or ((k > len or k < 1) or mathfloor(k) ~= k) then
|
||||
-- For each pair, serialize both the key and the value.
|
||||
local idx = #serialConcat
|
||||
serialConcat[idx + 1] = serialize(k)
|
||||
serialConcat[idx + 2] = serialize(v)
|
||||
end
|
||||
end
|
||||
serialConcat[#serialConcat + 1] = "\0"
|
||||
|
||||
-- tableconcat is way faster than normal concatenation using .. when dealing with lots of strings.
|
||||
-- Add this serialization to the table of serialized tables for quick access and later more concatenation.
|
||||
tableSerial[id] = tableconcat(serialConcat)
|
||||
return ser
|
||||
else
|
||||
-- Do serialization on a non-recursive type.
|
||||
if t == "number" then
|
||||
-- Space optimization can be done for ints, so serialize them differently from doubles.
|
||||
-- OSMARKS EDIT: handle sign in type and not actual serialization
|
||||
if mathfloor(obj) == obj then
|
||||
local intval = serialization.integer(math.abs(obj))
|
||||
local typespec = "p"
|
||||
if obj < 0 then typespec = "n" end
|
||||
return typespec .. intval
|
||||
end
|
||||
return "d" .. serialization.double(obj)
|
||||
end
|
||||
local ser = types[t]
|
||||
return obj == nil and ser or ser .. serialization[t](obj)
|
||||
end
|
||||
end
|
||||
|
||||
-- Either serialize for a table or for a non-recursive type.
|
||||
local ser = serialize(obj)
|
||||
if type(obj) == "table" then
|
||||
return tableconcat({ "t", tableconcat(tableSerial) })
|
||||
end
|
||||
return ser
|
||||
end
|
||||
|
||||
function deserialize(ser)
|
||||
local idx = 1
|
||||
local tables = { { } }
|
||||
local serref = { ser }
|
||||
|
||||
local function getchar()
|
||||
local ret = stringsub(serref[1], idx, idx)
|
||||
return ret ~= "" and ret or nil
|
||||
end
|
||||
|
||||
local function deserializeValue()
|
||||
yield()
|
||||
local t = getchar()
|
||||
idx = idx + 1
|
||||
if t == "t" then
|
||||
-- Get table id.
|
||||
local width = stringtonumber(getchar())
|
||||
idx = idx + 1
|
||||
local id = stringtonumber(stringsub(serref[1], idx, idx + width - 1))
|
||||
idx = idx + width
|
||||
|
||||
-- Create an empty table as a placeholder.
|
||||
if not tables[id] then
|
||||
tables[id] = { }
|
||||
end
|
||||
|
||||
return tables[id]
|
||||
else
|
||||
local ret
|
||||
ret, idx = deserialization[t](idx, serref)
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
||||
-- Either deserialize for a table or for a non-recursive type.
|
||||
local i = 1
|
||||
if getchar() == "t" then
|
||||
idx = idx + 1
|
||||
while getchar() do
|
||||
if not tables[i] then tables[i] = { } end
|
||||
local curtbl = tables[i]
|
||||
|
||||
-- Array part.
|
||||
while getchar() ~= "\0" do
|
||||
curtbl[#curtbl + 1] = deserializeValue()
|
||||
end
|
||||
|
||||
-- Table part.
|
||||
idx = idx + 1
|
||||
while getchar() ~= "\0" do
|
||||
curtbl[deserializeValue()] = deserializeValue()
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
idx = idx + 1
|
||||
end
|
||||
return tables[1]
|
||||
end
|
||||
return deserializeValue()
|
||||
end
|
||||
|
||||
return { serialize = serialize, deserialize = deserialize }
|
1311
src/lib/ecc-168.lua
Normal file
1311
src/lib/ecc-168.lua
Normal file
File diff suppressed because it is too large
Load Diff
2135
src/lib/ecc.lua
Normal file
2135
src/lib/ecc.lua
Normal file
File diff suppressed because it is too large
Load Diff
213
src/lib/gps.lua
Normal file
213
src/lib/gps.lua
Normal file
@@ -0,0 +1,213 @@
|
||||
local expect
|
||||
if _G["~expect"] then
|
||||
expect = _G["~expect"]
|
||||
else
|
||||
local native_select, native_type = select, type
|
||||
expect = function(index, value, ...)
|
||||
local t = native_type(value)
|
||||
for i = 1, native_select("#", ...) do
|
||||
if t == native_select(i, ...) then return true end
|
||||
end
|
||||
local types = table.pack(...)
|
||||
for i = types.n, 1, -1 do
|
||||
if types[i] == "nil" then table.remove(types, i) end
|
||||
end
|
||||
local type_names
|
||||
if #types <= 1 then
|
||||
type_names = tostring(...)
|
||||
else
|
||||
type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types]
|
||||
end
|
||||
-- If we can determine the function name with a high level of confidence, try to include it.
|
||||
local name
|
||||
if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then
|
||||
local ok, info = pcall(debug.getinfo, 3, "nS")
|
||||
if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end
|
||||
end
|
||||
if name then
|
||||
error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 )
|
||||
else
|
||||
error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
CHANNEL_GPS = 65534
|
||||
|
||||
local function trilaterate( A, B, C )
|
||||
local a2b = B.vPosition - A.vPosition
|
||||
local a2c = C.vPosition - A.vPosition
|
||||
|
||||
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local d = a2b:length()
|
||||
local ex = a2b:normalize( )
|
||||
local i = ex:dot( a2c )
|
||||
local ey = (a2c - ex * i):normalize()
|
||||
local j = ey:dot( a2c )
|
||||
local ez = ex:cross( ey )
|
||||
|
||||
local r1 = A.nDistance
|
||||
local r2 = B.nDistance
|
||||
local r3 = C.nDistance
|
||||
|
||||
local x = (r1 * r1 - r2 * r2 + d * d) / (2 * d)
|
||||
local y = (r1 * r1 - r3 * r3 - x * x + (x - i) * (x - i) + j * j) / (2 * j)
|
||||
|
||||
local result = A.vPosition + ex * x + ey * y
|
||||
|
||||
local zSquared = r1 * r1 - x * x - y * y
|
||||
if zSquared > 0 then
|
||||
local z = math.sqrt( zSquared )
|
||||
local result1 = result + ez * z
|
||||
local result2 = result - ez * z
|
||||
|
||||
local rounded1, rounded2 = result1:round( 0.01 ), result2:round( 0.01 )
|
||||
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
|
||||
return rounded1, rounded2
|
||||
else
|
||||
return rounded1
|
||||
end
|
||||
end
|
||||
return result:round( 0.01 )
|
||||
|
||||
end
|
||||
|
||||
local function narrow( p1, p2, fix )
|
||||
local dist1 = math.abs( (p1 - fix.vPosition):length() - fix.nDistance )
|
||||
local dist2 = math.abs( (p2 - fix.vPosition):length() - fix.nDistance )
|
||||
|
||||
if math.abs(dist1 - dist2) < 0.01 then
|
||||
return p1, p2
|
||||
elseif dist1 < dist2 then
|
||||
return p1:round( 0.01 )
|
||||
else
|
||||
return p2:round( 0.01 )
|
||||
end
|
||||
end
|
||||
|
||||
function locate( _nTimeout, _bDebug )
|
||||
expect(1, _nTimeout, "number", "nil")
|
||||
expect(2, _bDebug, "boolean", "nil")
|
||||
-- Let command computers use their magic fourth-wall-breaking special abilities
|
||||
if commands then
|
||||
return commands.getBlockPosition()
|
||||
end
|
||||
|
||||
-- Find a modem
|
||||
local sModemSide = nil
|
||||
for _, sSide in ipairs( rs.getSides() ) do
|
||||
if peripheral.getType( sSide ) == "modem" and peripheral.call( sSide, "isWireless" ) then
|
||||
sModemSide = sSide
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if sModemSide == nil then
|
||||
if _bDebug then
|
||||
print( "No wireless modem attached" )
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if _bDebug then
|
||||
print( "Finding position..." )
|
||||
end
|
||||
|
||||
-- Open it
|
||||
local modem = peripheral.wrap( sModemSide )
|
||||
local bCloseChannel = false
|
||||
if not modem.isOpen( CHANNEL_GPS ) then
|
||||
modem.open( CHANNEL_GPS )
|
||||
bCloseChannel = true
|
||||
end
|
||||
|
||||
-- Send a ping to listening GPS hosts
|
||||
modem.transmit( CHANNEL_GPS, CHANNEL_GPS, "PING" )
|
||||
|
||||
-- Wait for the responses
|
||||
local tFixes = {}
|
||||
local pos1, pos2, dimension
|
||||
local timeout = os.startTimer( _nTimeout or 2 )
|
||||
while true do
|
||||
local e, p1, p2, p3, p4, p5 = os.pullEvent()
|
||||
if e == "modem_message" then
|
||||
-- We received a reply from a modem
|
||||
local sSide, sChannel, sReplyChannel, tMessage, nDistance = p1, p2, p3, p4, p5
|
||||
if sSide == sModemSide and sChannel == CHANNEL_GPS and sReplyChannel == CHANNEL_GPS and nDistance then
|
||||
-- Received the correct message from the correct modem: use it to determine position
|
||||
if type(tMessage) == "table" and #tMessage == 3 and tonumber(tMessage[1]) and tonumber(tMessage[2]) and tonumber(tMessage[3]) then
|
||||
local tFix = { vPosition = vector.new( tMessage[1], tMessage[2], tMessage[3] ), nDistance = nDistance, dimension = tMessage.dimension }
|
||||
if _bDebug then
|
||||
local text = tFix.nDistance .. " metres from " .. tostring(tFix.vPosition)
|
||||
if type(tFix.dimension) == "number" or type(tFix.dimension) == "string" then text = text .. " " .. tFix.dimension end
|
||||
print(text)
|
||||
end
|
||||
if tFix.nDistance == 0 then
|
||||
pos1, pos2 = tFix.vPosition, nil
|
||||
else
|
||||
table.insert( tFixes, tFix )
|
||||
if #tFixes >= 3 then
|
||||
if not pos1 then
|
||||
pos1, pos2 = trilaterate( tFixes[1], tFixes[2], tFixes[#tFixes] )
|
||||
else
|
||||
pos1, pos2 = narrow( pos1, pos2, tFixes[#tFixes] )
|
||||
end
|
||||
end
|
||||
end
|
||||
if pos1 and not pos2 then
|
||||
local dimension_claims = {}
|
||||
for _, fix in pairs(tFixes) do
|
||||
if type(fix.dimension) == "string" or type(fix.dimension) == "number" then
|
||||
dimension_claims[fix.dimension] = (dimension_claims[fix.dimension] or 0) + 1
|
||||
end
|
||||
end
|
||||
local highest_count = 0
|
||||
for dim, count in pairs(dimension_claims) do
|
||||
if count > highest_count then
|
||||
highest_count = count
|
||||
dimension = dim
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
elseif e == "timer" then
|
||||
-- We received a timeout
|
||||
local timer = p1
|
||||
if timer == timeout then
|
||||
break
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
-- Close the channel, if we opened one
|
||||
if bCloseChannel then
|
||||
modem.close( CHANNEL_GPS )
|
||||
end
|
||||
|
||||
-- Return the response
|
||||
if pos1 and pos2 then
|
||||
if _bDebug then
|
||||
print( "Ambiguous position" )
|
||||
print( "Could be " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z .. " or " .. pos2.x .. "," .. pos2.y .. "," .. pos2.z )
|
||||
end
|
||||
return nil
|
||||
elseif pos1 then
|
||||
if _bDebug then
|
||||
print( "Position is " .. pos1.x .. "," .. pos1.y .. "," .. pos1.z )
|
||||
if dimension then print("Dimension is " .. dimension) end
|
||||
end
|
||||
return pos1.x, pos1.y, pos1.z, dimension
|
||||
else
|
||||
if _bDebug then
|
||||
print( "Could not determine position" )
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
400
src/lib/json.lua
Normal file
400
src/lib/json.lua
Normal file
@@ -0,0 +1,400 @@
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2018 rxi
|
||||
--
|
||||
-- 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 json = {}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
local line_count = 1
|
||||
local col_count = 1
|
||||
for i = 1, idx - 1 do
|
||||
col_count = col_count + 1
|
||||
if str:sub(i, i) == "\n" then
|
||||
line_count = line_count + 1
|
||||
col_count = 1
|
||||
end
|
||||
end
|
||||
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
||||
idx = next_char(str, idx, space_chars, true)
|
||||
if idx <= #str then
|
||||
decode_error(str, idx, "trailing garbage")
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
return json
|
28
src/lib/meta.lua
Normal file
28
src/lib/meta.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
local function new()
|
||||
local x = { level = 1, new = new }
|
||||
local m = {}
|
||||
setmetatable(x, m)
|
||||
|
||||
m.__eq = function(p1,p2)
|
||||
if getmetatable(p1) == getmetatable(p2) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
m.__index = function(inst, key)
|
||||
local lvl = rawget(inst, "level")
|
||||
if key == "level" then
|
||||
return lvl
|
||||
else
|
||||
return setmetatable({ level = lvl + 1 }, m)
|
||||
end
|
||||
end
|
||||
|
||||
m.__tostring = function(inst)
|
||||
return ("RECURSION "):rep(rawget(inst, "level"))
|
||||
end
|
||||
|
||||
return x
|
||||
end
|
||||
|
||||
return new()
|
49
src/lib/persistence.lua
Normal file
49
src/lib/persistence.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
--[[
|
||||
Fixes PS#DE7E9F99
|
||||
Out-of-sandbox code/data files could be overwritten using this due to environment weirdness.
|
||||
Now persistence data is in its own folder so this will not happen.
|
||||
]]
|
||||
local persist_dir = "persistence_data"
|
||||
|
||||
local function load_data(name)
|
||||
local file = fs.combine(persist_dir, name)
|
||||
if not fs.exists(file) then error "does not exist" end
|
||||
local f = fs.open(file, "r")
|
||||
local x = f.readAll()
|
||||
f.close()
|
||||
return textutils.unserialise(x)
|
||||
end
|
||||
|
||||
local function save_data(name, value)
|
||||
if not fs.isDir(persist_dir) then fs.delete(persist_dir) fs.makeDir(persist_dir) end
|
||||
local f = fs.open(fs.combine(persist_dir, name), "w")
|
||||
f.write(textutils.serialise(value))
|
||||
f.close()
|
||||
end
|
||||
|
||||
return function(name)
|
||||
if type(name) ~= "string" then error "Name of persistence volume must be a string." end
|
||||
local ok, data = pcall(load_data, name)
|
||||
if not ok or not data then data = {} end
|
||||
local mt = {}
|
||||
setmetatable(data, mt)
|
||||
|
||||
mt.__index = {
|
||||
save = function()
|
||||
save_data(name, data)
|
||||
end,
|
||||
reload = function()
|
||||
-- swap table in place to keep references valid
|
||||
for k in pairs(data) do data[k] = nil end
|
||||
for k, v in pairs(load_data(name)) do
|
||||
data[k] = v
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
function mt.__tostring(_)
|
||||
return ("[PersistenceStore %s]"):format(name)
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
72
src/lib/registry.lua
Normal file
72
src/lib/registry.lua
Normal file
@@ -0,0 +1,72 @@
|
||||
local ser = require "binary-serialization"
|
||||
|
||||
function split(self, separator, max)
|
||||
local out = {}
|
||||
if self:len() > 0 then
|
||||
max = max or -1
|
||||
|
||||
local field, start = 1, 1
|
||||
local first, last = self:find(separator, start, true)
|
||||
while first and max ~= 0 do
|
||||
out[field] = self:sub(start, first - 1)
|
||||
field = field + 1
|
||||
start = last + 1
|
||||
first , last = self:find(separator, start, true)
|
||||
max = max - 1
|
||||
end
|
||||
out[field] = self:sub(start)
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
local function fwrite(n, c)
|
||||
local f = fs.open(n, "wb")
|
||||
f.write(c)
|
||||
f.close()
|
||||
end
|
||||
|
||||
local function fread(n)
|
||||
local f = fs.open(n, "rb")
|
||||
local out = f.readAll()
|
||||
f.close()
|
||||
return out
|
||||
end
|
||||
|
||||
local fsopen = fs.open
|
||||
local registry_path = ".registry"
|
||||
local registry = {}
|
||||
|
||||
function registry.set(key, value)
|
||||
local path = split(key, ".")
|
||||
local ok, orig_data
|
||||
if fs.exists(registry_path) then
|
||||
ok, orig_data = pcall(ser.deserialize, fread(registry_path))
|
||||
if not ok then orig_data = {} end
|
||||
else
|
||||
orig_data = {}
|
||||
end
|
||||
local last_bit = table.remove(path)
|
||||
local data = orig_data
|
||||
for _, seg in ipairs(path) do
|
||||
local new_bit = data[seg]
|
||||
if type(new_bit) ~= "table" then data[seg] = {} end
|
||||
data = data[seg]
|
||||
end
|
||||
data[last_bit] = value
|
||||
fwrite(registry_path, ser.serialize(orig_data))
|
||||
end
|
||||
|
||||
function registry.get(key)
|
||||
if not fs.exists(registry_path) then return nil end
|
||||
local path = split(key, ".")
|
||||
local ok, data = pcall(ser.deserialize, fread(registry_path))
|
||||
if not ok then data = {} end
|
||||
for _, seg in ipairs(path) do
|
||||
data = data[seg]
|
||||
if not data then return nil end
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
return registry
|
172
src/lib/sha256.lua
Normal file
172
src/lib/sha256.lua
Normal file
@@ -0,0 +1,172 @@
|
||||
-- SHA-256, HMAC and PBKDF2 functions in ComputerCraft
|
||||
-- By Anavrins
|
||||
-- MIT License
|
||||
-- Pastebin: https://pastebin.com/6UV4qfNF
|
||||
-- Usage: https://pastebin.com/q2SQ7eRg
|
||||
-- Last updated: March 27 2020
|
||||
|
||||
-- HMAC/PBKFD2 removed
|
||||
|
||||
local mod32 = 2^32
|
||||
local band = bit32 and bit32.band or bit.band
|
||||
local bnot = bit32 and bit32.bnot or bit.bnot
|
||||
local bxor = bit32 and bit32.bxor or bit.bxor
|
||||
local blshift = bit32 and bit32.lshift or bit.blshift
|
||||
local upack = unpack
|
||||
|
||||
local function rrotate(n, b)
|
||||
local s = n/(2^b)
|
||||
local f = s%1
|
||||
return (s-f) + f*mod32
|
||||
end
|
||||
local function brshift(int, by)
|
||||
local s = int / (2^by)
|
||||
return s - s%1
|
||||
end
|
||||
|
||||
local H = {
|
||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
||||
}
|
||||
|
||||
local K = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
}
|
||||
|
||||
local function counter(incr)
|
||||
local t1, t2 = 0, 0
|
||||
if 0xFFFFFFFF - t1 < incr then
|
||||
t2 = t2 + 1
|
||||
t1 = incr - (0xFFFFFFFF - t1) - 1
|
||||
else t1 = t1 + incr
|
||||
end
|
||||
return t2, t1
|
||||
end
|
||||
|
||||
local function BE_toInt(bs, i)
|
||||
return blshift((bs[i] or 0), 24) + blshift((bs[i+1] or 0), 16) + blshift((bs[i+2] or 0), 8) + (bs[i+3] or 0)
|
||||
end
|
||||
|
||||
local function preprocess(data)
|
||||
local len = #data
|
||||
local proc = {}
|
||||
data[#data+1] = 0x80
|
||||
while #data%64~=56 do data[#data+1] = 0 end
|
||||
local blocks = math.ceil(#data/64)
|
||||
for i = 1, blocks do
|
||||
proc[i] = {}
|
||||
for j = 1, 16 do
|
||||
proc[i][j] = BE_toInt(data, 1+((i-1)*64)+((j-1)*4))
|
||||
end
|
||||
end
|
||||
proc[blocks][15], proc[blocks][16] = counter(len*8)
|
||||
return proc
|
||||
end
|
||||
|
||||
local function digestblock(w, C)
|
||||
for j = 17, 64 do
|
||||
local v = w[j-15]
|
||||
local s0 = bxor(rrotate(w[j-15], 7), rrotate(w[j-15], 18), brshift(w[j-15], 3))
|
||||
local s1 = bxor(rrotate(w[j-2], 17), rrotate(w[j-2], 19),brshift(w[j-2], 10))
|
||||
w[j] = (w[j-16] + s0 + w[j-7] + s1)%mod32
|
||||
end
|
||||
local a, b, c, d, e, f, g, h = upack(C)
|
||||
for j = 1, 64 do
|
||||
local S1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
|
||||
local ch = bxor(band(e, f), band(bnot(e), g))
|
||||
local temp1 = (h + S1 + ch + K[j] + w[j])%mod32
|
||||
local S0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
|
||||
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
|
||||
local temp2 = (S0 + maj)%mod32
|
||||
h, g, f, e, d, c, b, a = g, f, e, (d+temp1)%mod32, c, b, a, (temp1+temp2)%mod32
|
||||
end
|
||||
C[1] = (C[1] + a)%mod32
|
||||
C[2] = (C[2] + b)%mod32
|
||||
C[3] = (C[3] + c)%mod32
|
||||
C[4] = (C[4] + d)%mod32
|
||||
C[5] = (C[5] + e)%mod32
|
||||
C[6] = (C[6] + f)%mod32
|
||||
C[7] = (C[7] + g)%mod32
|
||||
C[8] = (C[8] + h)%mod32
|
||||
return C
|
||||
end
|
||||
|
||||
local mt = {
|
||||
__tostring = function(a) return string.char(unpack(a)) end,
|
||||
__index = {
|
||||
toHex = function(self, s) return ("%02x"):rep(#self):format(unpack(self)) end,
|
||||
isEqual = function(self, t)
|
||||
if type(t) ~= "table" then return false end
|
||||
if #self ~= #t then return false end
|
||||
local ret = 0
|
||||
for i = 1, #self do
|
||||
ret = bit32.bor(ret, bxor(self[i], t[i]))
|
||||
end
|
||||
return ret == 0
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
local function toBytes(t, n)
|
||||
local b = {}
|
||||
for i = 1, n do
|
||||
b[(i-1)*4+1] = band(brshift(t[i], 24), 0xFF)
|
||||
b[(i-1)*4+2] = band(brshift(t[i], 16), 0xFF)
|
||||
b[(i-1)*4+3] = band(brshift(t[i], 8), 0xFF)
|
||||
b[(i-1)*4+4] = band(t[i], 0xFF)
|
||||
end
|
||||
return setmetatable(b, mt)
|
||||
end
|
||||
|
||||
local function digest(data)
|
||||
local data = data or ""
|
||||
data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)}
|
||||
|
||||
data = preprocess(data)
|
||||
local C = {upack(H)}
|
||||
for i = 1, #data do C = digestblock(data[i], C) end
|
||||
return toBytes(C, 8)
|
||||
end
|
||||
|
||||
local function hmac(data, key)
|
||||
local data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)}
|
||||
local key = type(key) == "table" and {upack(key)} or {tostring(key):byte(1,-1)}
|
||||
|
||||
local blocksize = 64
|
||||
|
||||
key = #key > blocksize and digest(key) or key
|
||||
|
||||
local ipad = {}
|
||||
local opad = {}
|
||||
local padded_key = {}
|
||||
|
||||
for i = 1, blocksize do
|
||||
ipad[i] = bxor(0x36, key[i] or 0)
|
||||
opad[i] = bxor(0x5C, key[i] or 0)
|
||||
end
|
||||
|
||||
for i = 1, #data do
|
||||
ipad[blocksize+i] = data[i]
|
||||
end
|
||||
|
||||
ipad = digest(ipad)
|
||||
|
||||
for i = 1, blocksize do
|
||||
padded_key[i] = opad[i]
|
||||
padded_key[blocksize+i] = ipad[i]
|
||||
end
|
||||
|
||||
return digest(padded_key)
|
||||
end
|
||||
|
||||
return {
|
||||
hmac = hmac,
|
||||
digest = digest
|
||||
}
|
94
src/lib/stack_trace.lua
Normal file
94
src/lib/stack_trace.lua
Normal file
@@ -0,0 +1,94 @@
|
||||
-- see pastebin.com/NdUKJ07j for license info
|
||||
-- mostly written by SquidDev, bodged by gollark/osmarks
|
||||
|
||||
local type = type
|
||||
local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback
|
||||
|
||||
local _pcall, _xpcall = pcall, xpcall
|
||||
|
||||
local function traceback(x)
|
||||
-- Attempt to detect error() and error("xyz", 0).
|
||||
-- This probably means they're erroring the program intentionally and so we
|
||||
-- shouldn't display anything.
|
||||
if x == nil or (type(x) == "string" and not x:find(":%d+:")) or type(x) ~= "string" then
|
||||
return x
|
||||
end
|
||||
|
||||
if debug_traceback then
|
||||
-- The parens are important, as they prevent a tail call occuring, meaning
|
||||
-- the stack level is preserved. This ensures the code behaves identically
|
||||
-- on LuaJ and PUC Lua.
|
||||
return (debug_traceback(tostring(x), 2))
|
||||
else
|
||||
local level = 3
|
||||
local out = { tostring(x), "stack traceback:" }
|
||||
while true do
|
||||
local _, msg = _pcall(error, "", level)
|
||||
if msg == "" then break end
|
||||
|
||||
out[#out + 1] = " " .. msg
|
||||
level = level + 1
|
||||
end
|
||||
|
||||
return table.concat(out, "\n")
|
||||
end
|
||||
end
|
||||
|
||||
local function trim_traceback(target, marker)
|
||||
local target = tostring(target)
|
||||
local ttarget, tmarker = {}, {}
|
||||
for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end
|
||||
for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end
|
||||
|
||||
-- Trim identical suffixes
|
||||
local t_len, m_len = #ttarget, #tmarker
|
||||
while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do
|
||||
table.remove(ttarget, t_len)
|
||||
t_len, m_len = t_len - 1, m_len - 1
|
||||
end
|
||||
|
||||
-- Trim elements from this file and xpcall invocations
|
||||
while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or
|
||||
ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do
|
||||
table.remove(ttarget, t_len)
|
||||
t_len = t_len - 1
|
||||
end
|
||||
|
||||
return ttarget
|
||||
end
|
||||
|
||||
--- Run a function with
|
||||
local function xpcall_with(fn, ...)
|
||||
local args = {...}
|
||||
-- So this is rather grim: we need to get the full traceback and current one and remove
|
||||
-- the common prefix
|
||||
local trace
|
||||
local res = table.pack(_xpcall(function() return fn(unpack(args)) end, traceback)) if not res[1] then trace = traceback("stack_trace.lua:1:") end
|
||||
local ok, err = res[1], res[2]
|
||||
|
||||
if not ok and err ~= nil then
|
||||
trace = trim_traceback(err, trace)
|
||||
|
||||
-- Find the position where the stack traceback actually starts
|
||||
local trace_starts
|
||||
for i = #trace, 1, -1 do
|
||||
if trace[i] == "stack traceback:" then trace_starts = i; break end
|
||||
end
|
||||
|
||||
-- If this traceback is more than 15 elements long, keep the first 9, last 5
|
||||
-- and put an ellipsis between the rest
|
||||
local max = 15
|
||||
if trace_starts and #trace - trace_starts > max then
|
||||
local keep_starts = trace_starts + 10
|
||||
for i = #trace - trace_starts - max, 0, -1 do table.remove(trace, keep_starts + i) end
|
||||
table.insert(trace, keep_starts, " ...")
|
||||
end
|
||||
|
||||
return false, table.concat(trace, "\n")
|
||||
end
|
||||
|
||||
return table.unpack(res, 1, res.n)
|
||||
end
|
||||
|
||||
_G.pcall = xpcall_with
|
||||
return xpcall_with
|
40
src/lib/urandom.lua
Normal file
40
src/lib/urandom.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
-- ComputerCraft Event Entropy Extractor
|
||||
|
||||
local sha256 = require("sha256")
|
||||
|
||||
local entropy = tostring(math.random()) .. tostring(os.epoch("utc"))
|
||||
local maxEntropySize = 1024
|
||||
local state = ""
|
||||
|
||||
if process then
|
||||
process.spawn(function()
|
||||
while true do
|
||||
local event = {coroutine.yield()}
|
||||
entropy = {
|
||||
entropy,
|
||||
event[1],
|
||||
tostring(event[2]),
|
||||
tostring(event[3]),
|
||||
tostring(event[4]),
|
||||
tostring(os.epoch("utc")),
|
||||
tostring({}),
|
||||
}
|
||||
entropy = table.concat(entropy, "|")
|
||||
|
||||
if #entropy > maxEntropySize then
|
||||
state = sha256.digest(entropy)
|
||||
entropy = tostring(state)
|
||||
end
|
||||
end
|
||||
end, "entropy")
|
||||
end
|
||||
os.urandom = function()
|
||||
os.queueEvent("random")
|
||||
os.pullEvent()
|
||||
|
||||
local result = sha256.hmac("out", state)
|
||||
|
||||
state = sha256.digest(state)
|
||||
|
||||
return result
|
||||
end
|
469
src/lib/yafss.lua
Normal file
469
src/lib/yafss.lua
Normal file
@@ -0,0 +1,469 @@
|
||||
-- Deep-copy a table
|
||||
local function copy(tabl)
|
||||
local new = {}
|
||||
for k, v in pairs(tabl) do
|
||||
if type(v) == "table" and tabl ~= v then
|
||||
new[k] = copy(v)
|
||||
else
|
||||
new[k] = v
|
||||
end
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
-- Deep-map all values in a table
|
||||
local function deepmap(table, f, path)
|
||||
local path = path or ""
|
||||
local new = {}
|
||||
for k, v in pairs(table) do
|
||||
local thisp = path .. "." .. k
|
||||
if type(v) == "table" and v ~= table then -- bodge it to not stackoverflow
|
||||
new[k] = deepmap(v, f, thisp)
|
||||
else
|
||||
new[k] = f(v, k, thisp)
|
||||
end
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
-- Takes a list of keys to copy, returns a function which takes a table and copies the given keys to a new table
|
||||
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
|
||||
|
||||
-- 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
|
||||
|
||||
-- Maps function f over table t. f is passed the value and key and can return a new value and key.
|
||||
local function map(f, t)
|
||||
local mapper = function(t)
|
||||
local new = {}
|
||||
for k, v in pairs(t) do
|
||||
local new_v, new_k = f(v, k)
|
||||
new[new_k or k] = new_v
|
||||
end
|
||||
return new
|
||||
end
|
||||
if t then return mapper(t) else return mapper end
|
||||
end
|
||||
|
||||
-- Copies stuff from t2 into t1
|
||||
local function add_to_table(t1, t2)
|
||||
for k, v in pairs(t2) do
|
||||
if type(v) == "table" and v ~= t2 and v ~= t1 then
|
||||
if not t1[k] then t1[k] = {} end
|
||||
add_to_table(t1[k], v)
|
||||
else
|
||||
t1[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Convert path to canonical form
|
||||
local function canonicalize(path)
|
||||
return fs.combine(path, "")
|
||||
end
|
||||
|
||||
-- Checks whether a path is in a directory
|
||||
local function path_in(p, dir)
|
||||
return starts_with(canonicalize(p), canonicalize(dir))
|
||||
end
|
||||
|
||||
local function make_mappings(root)
|
||||
return {
|
||||
["/disk"] = "/disk",
|
||||
["/rom"] = "/rom",
|
||||
default = root
|
||||
}
|
||||
end
|
||||
|
||||
local function get_root(path, mappings)
|
||||
for mapfrom, mapto in pairs(mappings) do
|
||||
if path_in(path, mapfrom) then
|
||||
return mapto, mapfrom
|
||||
end
|
||||
end
|
||||
return mappings.default, "/"
|
||||
end
|
||||
|
||||
-- Escapes lua patterns in a string. Should not be needed, but lua is stupid so the only string.replace thing is gsub
|
||||
local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
|
||||
local function escape(str)
|
||||
return str:gsub(quotepattern, "%%%1")
|
||||
end
|
||||
|
||||
local function strip(p, root)
|
||||
return p:gsub("^" .. escape(canonicalize(root)), "")
|
||||
end
|
||||
|
||||
local function resolve_path(path, mappings)
|
||||
local root, to_strip = get_root(path, mappings)
|
||||
local newpath = strip(fs.combine(root, path), to_strip)
|
||||
if path_in(newpath, root) then return newpath end
|
||||
return resolve_path(newpath, mappings)
|
||||
end
|
||||
|
||||
local function segments(path)
|
||||
local segs, rest = {}, ""
|
||||
repeat
|
||||
table.insert(segs, 1, fs.getName(rest))
|
||||
rest = fs.getDir(rest)
|
||||
until rest == ""
|
||||
return segs
|
||||
end
|
||||
|
||||
local function combine(segs)
|
||||
local out = ""
|
||||
for _, p in pairs(segs) do
|
||||
out = fs.combine(out, p)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function difference(p1, p2)
|
||||
local s1, s2 = segments(p1), segments(p2)
|
||||
if #s2 == 0 then return combine(s1) end
|
||||
local segs = {}
|
||||
for _, p in pairs(s1) do
|
||||
local item = table.remove(s1, 1)
|
||||
table.insert(segs, item)
|
||||
if p == s2[1] then break end
|
||||
end
|
||||
return combine(segs)
|
||||
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
|
||||
|
||||
-- Fetch the contents of URL "u"
|
||||
local function fetch(u)
|
||||
local h = http.get(u)
|
||||
local c = h.readAll()
|
||||
h.close()
|
||||
return c
|
||||
end
|
||||
|
||||
-- Make a read handle for a string
|
||||
local function make_handle(text)
|
||||
local lines = lines(text)
|
||||
local h = {line = 0}
|
||||
function h.close() end
|
||||
function h.readLine() h.line = h.line + 1 return lines[h.line] end
|
||||
function h.readAll() return text end
|
||||
return h
|
||||
end
|
||||
|
||||
-- Get a path from a filesystem overlay
|
||||
local function path_in_overlay(overlay, path)
|
||||
return overlay[canonicalize(path)]
|
||||
end
|
||||
|
||||
local this_level_env = _G
|
||||
|
||||
-- Create a modified FS table which confines you to root and has some extra read-only pseudofiles.
|
||||
local function create_FS(root, overlay)
|
||||
local mappings = make_mappings(root)
|
||||
|
||||
local new_overlay = {}
|
||||
for k, v in pairs(overlay) do
|
||||
new_overlay[canonicalize(k)] = v
|
||||
end
|
||||
|
||||
local function lift_to_sandbox(f, n)
|
||||
return function(...)
|
||||
local args = map(function(x) return resolve_path(x, mappings) end, {...})
|
||||
return f(table.unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
local new = copy_some_keys {"getDir", "getName", "combine"} (fs)
|
||||
|
||||
function new.isReadOnly(path)
|
||||
return path_in_overlay(new_overlay, path) or starts_with(canonicalize(path), "rom")
|
||||
end
|
||||
|
||||
function new.open(path, mode)
|
||||
if (contains(mode, "w") or contains(mode, "a")) and new.isReadOnly(path) then
|
||||
error "Access denied"
|
||||
else
|
||||
local overlay_data = path_in_overlay(new_overlay, path)
|
||||
if overlay_data then
|
||||
if type(overlay_data) == "function" then overlay_data = overlay_data(this_level_env) end
|
||||
return make_handle(overlay_data), "YAFSS overlay"
|
||||
end
|
||||
return fs.open(resolve_path(path, mappings), mode)
|
||||
end
|
||||
end
|
||||
|
||||
function new.exists(path)
|
||||
if path_in_overlay(new_overlay, path) ~= nil then return true end
|
||||
return fs.exists(resolve_path(path, mappings))
|
||||
end
|
||||
|
||||
function new.overlay()
|
||||
return map(function(x)
|
||||
if type(x) == "function" then return x(this_level_env)
|
||||
else return x end
|
||||
end, new_overlay)
|
||||
end
|
||||
|
||||
function new.list(dir)
|
||||
local sdir = canonicalize(resolve_path(dir, mappings))
|
||||
local contents = fs.list(sdir)
|
||||
for opath in pairs(new_overlay) do
|
||||
if fs.getDir(opath) == sdir then
|
||||
table.insert(contents, fs.getName(opath))
|
||||
end
|
||||
end
|
||||
return contents
|
||||
end
|
||||
|
||||
add_to_table(new, map(lift_to_sandbox, copy_some_keys {"isDir", "getDrive", "getSize", "getFreeSpace", "makeDir", "move", "copy", "delete", "isDriveRoot"} (fs)))
|
||||
|
||||
function new.find(wildcard)
|
||||
local function recurse_spec(results, path, spec) -- From here: https://github.com/Sorroko/cclite/blob/62677542ed63bd4db212f83da1357cb953e82ce3/src/emulator/native_api.lua
|
||||
local segment = spec:match('([^/]*)'):gsub('/', '')
|
||||
local pattern = '^' .. segment:gsub('[*]', '.+'):gsub('?', '.'):gsub("-", "%%-") .. '$'
|
||||
|
||||
if new.isDir(path) then
|
||||
for _, file in ipairs(new.list(path)) do
|
||||
if file:match(pattern) then
|
||||
local f = new.combine(path, file)
|
||||
|
||||
if new.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 new.dump(dir)
|
||||
local dir = dir or "/"
|
||||
local out = {}
|
||||
for _, f in pairs(new.list(dir)) do
|
||||
local path = fs.combine(dir, f)
|
||||
local to_add = {
|
||||
n = f,
|
||||
t = "f"
|
||||
}
|
||||
if new.isDir(path) then
|
||||
to_add.c = new.dump(path)
|
||||
to_add.t = "d"
|
||||
else
|
||||
local fh = new.open(path, "r")
|
||||
to_add.c = fh.readAll()
|
||||
fh.close()
|
||||
end
|
||||
table.insert(out, to_add)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function new.load(dump, root)
|
||||
local root = root or "/"
|
||||
for _, f in pairs(dump) do
|
||||
local path = fs.combine(root, f.n)
|
||||
if f.t == "d" then
|
||||
new.makeDir(path)
|
||||
new.load(f.c, path)
|
||||
else
|
||||
local fh = new.open(path, "w")
|
||||
fh.write(f.c)
|
||||
fh.close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local allowed_APIs = {
|
||||
"term",
|
||||
"http",
|
||||
"pairs",
|
||||
"ipairs",
|
||||
-- getfenv, getfenv are modified to prevent sandbox escapes and defined in make_environment
|
||||
"peripheral",
|
||||
"table",
|
||||
"string",
|
||||
"type",
|
||||
"setmetatable",
|
||||
"getmetatable",
|
||||
"os",
|
||||
"sleep",
|
||||
"pcall",
|
||||
"xpcall",
|
||||
"select",
|
||||
"tostring",
|
||||
"tonumber",
|
||||
"coroutine",
|
||||
"next",
|
||||
"error",
|
||||
"math",
|
||||
"redstone",
|
||||
"rs",
|
||||
"assert",
|
||||
"unpack",
|
||||
"bit",
|
||||
"bit32",
|
||||
"turtle",
|
||||
"pocket",
|
||||
"ccemux",
|
||||
"config",
|
||||
"commands",
|
||||
"rawget",
|
||||
"rawset",
|
||||
"rawequal",
|
||||
"~expect",
|
||||
"__inext",
|
||||
"periphemu",
|
||||
}
|
||||
|
||||
local gf, sf = getfenv, setfenv
|
||||
|
||||
-- Takes the root directory to allow access to,
|
||||
-- a map of paths to either strings containing their contents or functions returning them
|
||||
-- and a table of extra APIs and partial overrides for existing APIs
|
||||
local function make_environment(root_directory, overlay, API_overrides)
|
||||
local environment = copy_some_keys(allowed_APIs)(_G)
|
||||
|
||||
environment.fs = create_FS(root_directory, overlay)
|
||||
|
||||
-- if function is not from within the VM, return env from within sandbox
|
||||
function environment.getfenv(arg)
|
||||
local env
|
||||
if type(arg) == "number" then return gf() end
|
||||
if not env or type(env._HOST) ~= "string" or not string.match(env._HOST, "YAFSS") then
|
||||
return gf()
|
||||
else
|
||||
return env
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Fix PS#AD2A532C
|
||||
Allowing `setfenv` to operate on any function meant that privileged code could in some cases be manipulated to leak information or operate undesirably. Due to this, we restrict it, similarly to getfenv.
|
||||
]]
|
||||
function environment.setfenv(fn, env)
|
||||
local nenv = gf(fn)
|
||||
if not nenv or type(nenv._HOST) ~= "string" or not string.match(nenv._HOST, "YAFSS") then
|
||||
return false
|
||||
end
|
||||
return sf(fn, env)
|
||||
end
|
||||
|
||||
function environment.load(code, file, mode, env)
|
||||
return load(code, file or "@<input>", mode or "t", env or environment)
|
||||
end
|
||||
|
||||
if debug then
|
||||
environment.debug = copy_some_keys {
|
||||
"getmetatable",
|
||||
"setmetatable",
|
||||
"traceback",
|
||||
"getinfo",
|
||||
"getregistry"
|
||||
}(debug)
|
||||
end
|
||||
|
||||
environment._G = environment
|
||||
environment._ENV = environment
|
||||
environment._HOST = string.format("YAFSS on %s", _HOST)
|
||||
|
||||
local upper = _G.hypercalls
|
||||
environment.hypercalls = {}
|
||||
function environment.hypercalls.upper()
|
||||
return upper
|
||||
end
|
||||
function environment.hypercalls.layers()
|
||||
if upper then return upper.layers() + 1
|
||||
else return 1 end
|
||||
end
|
||||
|
||||
function environment.os.shutdown()
|
||||
os.queueEvent("power_state", "shutdown")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
function environment.os.reboot()
|
||||
os.queueEvent("power_state", "reboot")
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
add_to_table(environment, copy(API_overrides))
|
||||
|
||||
return environment
|
||||
end
|
||||
|
||||
local function run(root_directory, overlay, API_overrides, init)
|
||||
if type(init) == "table" and init.URL then init = fetch(init.URL) end
|
||||
init = init or fetch "https://pastebin.com/raw/wKdMTPwQ"
|
||||
|
||||
local running = true
|
||||
while running do
|
||||
parallel.waitForAny(function()
|
||||
local env = make_environment(root_directory, overlay, API_overrides)
|
||||
env.init_code = init
|
||||
|
||||
local out, err = load(init, "@[init]", "t", env)
|
||||
env.hypercalls.run = out
|
||||
if not out then error(err) end
|
||||
local ok, err = pcall(out)
|
||||
if not ok then printError(err) end
|
||||
end,
|
||||
function()
|
||||
while true do
|
||||
local event, state = coroutine.yield "power_state"
|
||||
if event == "power_state" then -- coroutine.yield behaves weirdly with terminate
|
||||
if process then
|
||||
local this_process = process.running.ID
|
||||
for _, p in pairs(process.list()) do
|
||||
if p.parent and p.parent.ID == this_process then
|
||||
process.signal(p.ID, process.signals.KILL)
|
||||
end
|
||||
end
|
||||
end
|
||||
if state == "shutdown" then running = false return
|
||||
elseif state == "reboot" then return end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return run
|
Reference in New Issue
Block a user