Reformat + add prefix selection for config

This commit is contained in:
Soph :3 2025-11-14 22:43:49 +02:00
parent 88f4173c8e
commit 262c0b5408
13 changed files with 1191 additions and 1141 deletions

View file

@ -21,6 +21,52 @@
- Chatbox support for withdrawing and depositing items - Chatbox support for withdrawing and depositing items
- Remote access via enderstorages - Remote access via enderstorages
## How to use
1. First, in your /config.lua file, add the following:
```lua
return {
["inventories"] = { -- REQUIRED!! Please set at least one inventory pattern.
".*barrel.*" -- Lua patterns to match all barrels
},
["import"] = { -- Not required, just don't set!
"ender_storage_156" -- A inventory I want to move all items from into our storage
},
["chatbox"] = { -- Not required, just don't set!
["prefix"] = "home",
["players"] = {
["hartbreix"] = "manipulator_42" -- Chatbox support
}
},
["remote"] = { -- Access your items via modem, not required, just don't set!
["ender_storage"] = "ender_storage_493", -- Enderstorage that will be changed (set this to owner-only and computer-chanagable!)
["modem"] = "modem_1277", -- Modem to recieve messages
["remotes"] = { -- Remote access
["red/green/green"] = {
["port"] = 42420,
["password"] = "test123" -- Required!
}
}
}
}
```
2. Run the following command:
```
wget run https://git.sad.ovh/sophie/storage-solution/raw/branch/main/src/.main-installer.lua
```
This will install storage-solution to /storage-solution.
To make it start every time you turn on the computer, add
```
shell.run("wget run https://git.sad.ovh/sophie/storage-solution/raw/branch/main/src/.main-installer.lua")
```
to startup.lua
### [EXTRA!]
You can also run `wget run https://files.sad.ovh/public/storage-solution/.beta-installer.lua`, which is quite unstable. You may experience issues while running the beta version, but also new juicy features :)
## RA ## RA
SiSS has a remote access system that allows you to access your items via modem. SiSS has a remote access system that allows you to access your items via modem.
To use it, first set up your config.lua correctly. After that you need to implement the tiny_ra_library into your program. To use it, first set up your config.lua correctly. After that you need to implement the tiny_ra_library into your program.
@ -57,44 +103,3 @@ sleep(2)
print("deposits 16 items from the first slot") print("deposits 16 items from the first slot")
ra.depositBySlots({1}, 16) ra.depositBySlots({1}, 16)
``` ```
## How to use
1. First, in your /config.lua file, add the following:
```lua
return {
["inventories"] = { -- REQUIRED!! Please set at least one inventory pattern.
".*barrel.*" -- Lua patterns to match all barrels
},
["import"] = { -- Not required, just don't set!
"ender_storage_156" -- A inventory I want to move all items from into our storage
},
["chatbox"] = { -- Not required, just don't set!
["hartbreix"] = "manipulator_42" -- Chatbox support
},
["remote"] = { -- Access your items via modem, not required, just don't set!
["ender_storage"] = "ender_storage_493", -- Enderstorage that will be changed (set this to owner-only and computer-chanagable!)
["modem"] = "modem_1277", -- Modem to recieve messages
["remotes"] = { -- Remote access
["red/green/green"] = {
["port"] = 42420,
["password"] = "test123" -- Required!
}
}
}
}
```
2. Run the following command:
```
wget run https://git.sad.ovh/sophie/storage-solution/raw/branch/main/src/.main-installer.lua
```
This will install storage-solution to /storage-solution.
To make it start every time you turn on the computer, add
```
shell.run("wget run https://git.sad.ovh/sophie/storage-solution/raw/branch/main/src/.main-installer.lua")
```
to startup.lua
### [EXTRA!]
You can also run `wget run https://files.sad.ovh/public/storage-solution/.beta-installer.lua`, which is quite unstable. You may experience issues while running the beta version, but also new juicy features :)

View file

@ -3,75 +3,77 @@ local api_url = base_url .. "?ls"
local download_root = "storage-solution" local download_root = "storage-solution"
local function fetch_folder_list() local function fetch_folder_list()
local response = http.get(api_url) local response = http.get(api_url)
if not response then if not response then
error("Failed to get folder list from Copyparty API") error("Failed to get folder list from Copyparty API")
end end
local body = response.readAll() local body = response.readAll()
response.close() response.close()
local data = textutils.unserializeJSON(body) local data = textutils.unserializeJSON(body)
return data return data
end end
local function download_file(path) local function download_file(path)
local file_url = base_url .. "/" .. path local file_url = base_url .. "/" .. path
local local_path = download_root .. "/" .. path local local_path = download_root .. "/" .. path
local response = http.get(file_url) local response = http.get(file_url)
if response then if response then
local file = fs.open(local_path, "wb") local file = fs.open(local_path, "wb")
if file then if file then
file.write(response.readAll()) file.write(response.readAll())
file.close() file.close()
end
response.close()
else
print("Failed to download: " .. file_url)
end end
response.close()
else
print("Failed to download: " .. file_url)
end
end end
local function traverse_and_download(folder_data, prefix) local function traverse_and_download(folder_data, prefix)
prefix = prefix or "" prefix = prefix or ""
for _, file in ipairs(folder_data.files or {}) do for _, file in ipairs(folder_data.files or {}) do
print("Downloading: " .. prefix .. file.href) print("Downloading: " .. prefix .. file.href)
download_file(prefix .. file.href) download_file(prefix .. file.href)
end end
for _, dir in ipairs(folder_data.dirs or {}) do for _, dir in ipairs(folder_data.dirs or {}) do
fs.makeDir(download_root .. "/" .. prefix .. dir.href) fs.makeDir(download_root .. "/" .. prefix .. dir.href)
local new_prefix = (prefix ~= "" and (prefix .. "/") or "") .. dir.href local new_prefix = (prefix ~= "" and (prefix .. "/") or "") .. dir.href
local subdir_url = base_url .. "/" .. new_prefix .. "?ls" local response = http.get(subdir_url) local subdir_url = base_url .. "/" .. new_prefix .. "?ls"
if response then local response = http.get(subdir_url)
local body = response.readAll() if response then
if not body then return end local body = response.readAll()
response.close() if not body then return end
local subdir_data = textutils.unserializeJSON(body) response.close()
traverse_and_download(subdir_data, new_prefix) local subdir_data = textutils.unserializeJSON(body)
else traverse_and_download(subdir_data, new_prefix)
print("Failed to get subdirectory: " .. subdir_url) else
end print("Failed to get subdirectory: " .. subdir_url)
end end
end
end end
if fs.exists(download_root) then if fs.exists(download_root) then
if fs.exists(download_root .. "/version") then if fs.exists(download_root .. "/version") then
local previousVersion = fs.open(download_root .. "/version", "r").readAll() local previousVersion = fs.open(download_root .. "/version", "r").readAll()
local currentVersion = http.get(base_url .. "/version").readAll(); local currentVersion = http.get(base_url .. "/version").readAll();
if previousVersion == currentVersion then if previousVersion == currentVersion then
print("Previous version " .. previousVersion .. " is already installed, we're on " .. currentVersion .. " aswell, so skipping installation.") print("Previous version " ..
shell.run("storage-solution/main.lua") previousVersion .. " is already installed, we're on " .. currentVersion .. " aswell, so skipping installation.")
return shell.run("storage-solution/main.lua")
else return
print("Version " .. previousVersion .. " was already installed. Uninstalling.")
fs.delete(download_root)
end
else else
print("Version marker does not exist. Cannot install.") print("Version " .. previousVersion .. " was already installed. Uninstalling.")
shell.run("storage-solution/main.lua") fs.delete(download_root)
return
end end
else
print("Version marker does not exist. Cannot install.")
shell.run("storage-solution/main.lua")
return
end
end end
fs.makeDir(download_root) fs.makeDir(download_root)

View file

@ -5,79 +5,79 @@ local download_root = "storage-solution"
local remote_folder = "src" local remote_folder = "src"
local function fetch_commit_hash() local function fetch_commit_hash()
local url = repo_api .. "/branches/" .. branch local url = repo_api .. "/branches/" .. branch
local response = http.get(url) local response = http.get(url)
if not response then if not response then
error("Failed to fetch branch info: " .. url) error("Failed to fetch branch info: " .. url)
end end
local body = response.readAll() local body = response.readAll()
response.close() response.close()
local data = textutils.unserializeJSON(body) local data = textutils.unserializeJSON(body)
return data.commit.id return data.commit.id
end end
local function fetch_repo_tree(path) local function fetch_repo_tree(path)
local url = repo_api .. "/contents" .. (path and ("/" .. path) or "") local url = repo_api .. "/contents" .. (path and ("/" .. path) or "")
local response = http.get(url) local response = http.get(url)
if not response then if not response then
error("Failed to fetch repo tree: " .. url) error("Failed to fetch repo tree: " .. url)
end end
local body = response.readAll() local body = response.readAll()
response.close() response.close()
return textutils.unserializeJSON(body) return textutils.unserializeJSON(body)
end end
local function download_file(remote_path, local_path) local function download_file(remote_path, local_path)
local url = raw_base .. "/" .. remote_path local url = raw_base .. "/" .. remote_path
fs.makeDir(fs.getDir(local_path)) fs.makeDir(fs.getDir(local_path))
local response = http.get(url) local response = http.get(url)
if response then if response then
local file = fs.open(local_path, "wb") local file = fs.open(local_path, "wb")
if file then if file then
file.write(response.readAll()) file.write(response.readAll())
file.close() file.close()
end
response.close()
print("Downloaded: " .. local_path)
else
print("Failed to download: " .. url)
end end
response.close()
print("Downloaded: " .. local_path)
else
print("Failed to download: " .. url)
end
end end
local function traverse_and_download(remote_path, local_prefix) local function traverse_and_download(remote_path, local_prefix)
local tree = fetch_repo_tree(remote_path) local tree = fetch_repo_tree(remote_path)
for _, entry in ipairs(tree) do for _, entry in ipairs(tree) do
if entry.type == "file" then if entry.type == "file" then
local local_path = local_prefix .. "/" .. fs.getName(entry.path) local local_path = local_prefix .. "/" .. fs.getName(entry.path)
download_file(entry.path, local_path) download_file(entry.path, local_path)
elseif entry.type == "dir" then elseif entry.type == "dir" then
local new_remote = entry.path local new_remote = entry.path
local new_local = local_prefix .. "/" .. fs.getName(entry.path) local new_local = local_prefix .. "/" .. fs.getName(entry.path)
traverse_and_download(new_remote, new_local) traverse_and_download(new_remote, new_local)
end
end end
end
end end
local remote_hash = fetch_commit_hash() local remote_hash = fetch_commit_hash()
print("Latest commit: " .. remote_hash) print("Latest commit: " .. remote_hash)
if fs.exists(download_root) then if fs.exists(download_root) then
if fs.exists(download_root .. "/version") then if fs.exists(download_root .. "/version") then
local f = fs.open(download_root .. "/version", "r") local f = fs.open(download_root .. "/version", "r")
local local_hash = f.readAll() local local_hash = f.readAll()
f.close() f.close()
if local_hash == remote_hash then if local_hash == remote_hash then
print("Already up to date (commit " .. remote_hash:sub(1,7) .. ").") print("Already up to date (commit " .. remote_hash:sub(1, 7) .. ").")
shell.run(download_root .. "/main.lua") shell.run(download_root .. "/main.lua")
return return
else
print("Outdated (" .. local_hash:sub(1,7) .. " -> " .. remote_hash:sub(1,7) .. "), reinstalling...")
fs.delete(download_root)
end
else else
print("Version marker missing, reinstalling.") print("Outdated (" .. local_hash:sub(1, 7) .. " -> " .. remote_hash:sub(1, 7) .. "), reinstalling...")
fs.delete(download_root) fs.delete(download_root)
end end
else
print("Version marker missing, reinstalling.")
fs.delete(download_root)
end
end end
fs.makeDir(download_root) fs.makeDir(download_root)
@ -89,5 +89,5 @@ local f = fs.open(download_root .. "/version", "w")
f.write(remote_hash) f.write(remote_hash)
f.close() f.close()
print("Installed Sophie's Storage Solution (commit " .. remote_hash:sub(1,7) .. ")") print("Installed Sophie's Storage Solution (commit " .. remote_hash:sub(1, 7) .. ")")
shell.run(download_root .. "/main.lua") shell.run(download_root .. "/main.lua")

View file

@ -1,81 +1,94 @@
local g = string.gsub local g = string.gsub
sha256 = loadstring(g(g(g(g(g(g(g(g('Sa=XbandSb=XbxWSc=XlshiftSd=unpackSe=2^32SYf(g,h)Si=g/2^hSj=i%1Ui-j+j*eVSYk(l,m)Sn=l/2^mUn-n%1VSo={0x6a09e667Tbb67ae85T3c6ef372Ta54ff53aT510e527fT9b05688cT1f83d9abT5be0cd19}Sp={0x428a2f98T71374491Tb5c0fbcfTe9b5dba5T3956c25bT59f111f1T923f82a4Tab1c5ed5Td807aa98T12835b01T243185beT550c7dc3T72be5d74T80deb1feT9bdc06a7Tc19bf174Te49b69c1Tefbe4786T0fc19dc6T240ca1ccT2de92c6fT4a7484aaT5cb0a9dcT76f988daT983e5152Ta831c66dTb00327c8Tbf597fc7Tc6e00bf3Td5a79147T06ca6351T14292967T27b70a85T2e1b2138T4d2c6dfcT53380d13T650a7354T766a0abbT81c2c92eT92722c85Ta2bfe8a1Ta81a664bTc24b8b70Tc76c51a3Td192e819Td6990624Tf40e3585T106aa070T19a4c116T1e376c08T2748774cT34b0bcb5T391c0cb3T4ed8aa4aT5b9cca4fT682e6ff3T748f82eeT78a5636fT84c87814T8cc70208T90befffaTa4506cebTbef9a3f7Tc67178f2}SYq(r,q)if e-1-r[1]<q then r[2]=r[2]+1;r[1]=q-(e-1-r[1])-1 else r[1]=r[1]+qVUrVSYs(t)Su=#t;t[#t+1]=0x80;while#t%64~=56Zt[#t+1]=0VSv=q({0,0},u*8)fWw=2,1,-1Zt[#t+1]=a(k(a(v[w]TFF000000),24)TFF)t[#t+1]=a(k(a(v[w]TFF0000),16)TFF)t[#t+1]=a(k(a(v[w]TFF00),8)TFF)t[#t+1]=a(v[w]TFF)VUtVSYx(y,w)Uc(y[w]W0,24)+c(y[w+1]W0,16)+c(y[w+2]W0,8)+(y[w+3]W0)VSYz(t,w,A)SB={}fWC=1,16ZB[C]=x(t,w+(C-1)*4)VfWC=17,64ZSD=B[C-15]SE=b(b(f(B[C-15],7),f(B[C-15],18)),k(B[C-15],3))SF=b(b(f(B[C-2],17),f(B[C-2],19)),k(B[C-2],10))B[C]=(B[C-16]+E+B[C-7]+F)%eVSG,h,H,I,J,j,K,L=d(A)fWC=1,64ZSM=b(b(f(J,6),f(J,11)),f(J,25))SN=b(a(J,j),a(Xbnot(J),K))SO=(L+M+N+p[C]+B[C])%eSP=b(b(f(G,2),f(G,13)),f(G,22))SQ=b(b(a(G,h),a(G,H)),a(h,H))SR=(P+Q)%e;L,K,j,J,I,H,h,G=K,j,J,(I+O)%e,H,h,G,(O+R)%eVA[1]=(A[1]+G)%e;A[2]=(A[2]+h)%e;A[3]=(A[3]+H)%e;A[4]=(A[4]+I)%e;A[5]=(A[5]+J)%e;A[6]=(A[6]+j)%e;A[7]=(A[7]+K)%e;A[8]=(A[8]+L)%eUAVUY(t)t=t W""t=type(t)=="string"and{t:byte(1,-1)}Wt;t=s(t)SA={d(o)}fWw=1,#t,64ZA=z(t,w,A)VU("%08x"):rep(8):format(d(A))V',"S"," local "),"T",",0x"),"U"," return "),"V"," end "),"W","or "),"X","bit32."),"Y","function "),"Z"," do "))() sha256 = loadstring(g(
g(
g(
g(
g(
g(
g(
g(
'Sa=XbandSb=XbxWSc=XlshiftSd=unpackSe=2^32SYf(g,h)Si=g/2^hSj=i%1Ui-j+j*eVSYk(l,m)Sn=l/2^mUn-n%1VSo={0x6a09e667Tbb67ae85T3c6ef372Ta54ff53aT510e527fT9b05688cT1f83d9abT5be0cd19}Sp={0x428a2f98T71374491Tb5c0fbcfTe9b5dba5T3956c25bT59f111f1T923f82a4Tab1c5ed5Td807aa98T12835b01T243185beT550c7dc3T72be5d74T80deb1feT9bdc06a7Tc19bf174Te49b69c1Tefbe4786T0fc19dc6T240ca1ccT2de92c6fT4a7484aaT5cb0a9dcT76f988daT983e5152Ta831c66dTb00327c8Tbf597fc7Tc6e00bf3Td5a79147T06ca6351T14292967T27b70a85T2e1b2138T4d2c6dfcT53380d13T650a7354T766a0abbT81c2c92eT92722c85Ta2bfe8a1Ta81a664bTc24b8b70Tc76c51a3Td192e819Td6990624Tf40e3585T106aa070T19a4c116T1e376c08T2748774cT34b0bcb5T391c0cb3T4ed8aa4aT5b9cca4fT682e6ff3T748f82eeT78a5636fT84c87814T8cc70208T90befffaTa4506cebTbef9a3f7Tc67178f2}SYq(r,q)if e-1-r[1]<q then r[2]=r[2]+1;r[1]=q-(e-1-r[1])-1 else r[1]=r[1]+qVUrVSYs(t)Su=#t;t[#t+1]=0x80;while#t%64~=56Zt[#t+1]=0VSv=q({0,0},u*8)fWw=2,1,-1Zt[#t+1]=a(k(a(v[w]TFF000000),24)TFF)t[#t+1]=a(k(a(v[w]TFF0000),16)TFF)t[#t+1]=a(k(a(v[w]TFF00),8)TFF)t[#t+1]=a(v[w]TFF)VUtVSYx(y,w)Uc(y[w]W0,24)+c(y[w+1]W0,16)+c(y[w+2]W0,8)+(y[w+3]W0)VSYz(t,w,A)SB={}fWC=1,16ZB[C]=x(t,w+(C-1)*4)VfWC=17,64ZSD=B[C-15]SE=b(b(f(B[C-15],7),f(B[C-15],18)),k(B[C-15],3))SF=b(b(f(B[C-2],17),f(B[C-2],19)),k(B[C-2],10))B[C]=(B[C-16]+E+B[C-7]+F)%eVSG,h,H,I,J,j,K,L=d(A)fWC=1,64ZSM=b(b(f(J,6),f(J,11)),f(J,25))SN=b(a(J,j),a(Xbnot(J),K))SO=(L+M+N+p[C]+B[C])%eSP=b(b(f(G,2),f(G,13)),f(G,22))SQ=b(b(a(G,h),a(G,H)),a(h,H))SR=(P+Q)%e;L,K,j,J,I,H,h,G=K,j,J,(I+O)%e,H,h,G,(O+R)%eVA[1]=(A[1]+G)%e;A[2]=(A[2]+h)%e;A[3]=(A[3]+H)%e;A[4]=(A[4]+I)%e;A[5]=(A[5]+J)%e;A[6]=(A[6]+j)%e;A[7]=(A[7]+K)%e;A[8]=(A[8]+L)%eUAVUY(t)t=t W""t=type(t)=="string"and{t:byte(1,-1)}Wt;t=s(t)SA={d(o)}fWw=1,#t,64ZA=z(t,w,A)VU("%08x"):rep(8):format(d(A))V',
"S", " local "), "T", ",0x"), "U", " return "), "V", " end "), "W", "or "), "X", "bit32."), "Y", "function "), "Z",
" do "))()
local aead = require("lib.plc.aead_chacha_poly") local aead = require("lib.plc.aead_chacha_poly")
local function tobytes(s) return {s:byte(1, #s)} end local function tobytes(s) return { s:byte(1, #s) } end
local function frombytes(t) return string.char(table.unpack(t)) end local function frombytes(t) return string.char(table.unpack(t)) end
local b64abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' local b64abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
local function b64u_encode(bin) local function b64u_encode(bin)
local t, n, out = tobytes(bin), #bin, {} local t, n, out = tobytes(bin), #bin, {}
local i=1 local i = 1
while i<=n do while i <= n do
local a=t[i] or 0; local b=t[i+1] or 0; local c=t[i+2] or 0 local a = t[i] or 0; local b = t[i + 1] or 0; local c = t[i + 2] or 0
local triple = a*65536 + b*256 + c local triple = a * 65536 + b * 256 + c
out[#out+1]=b64abc:sub(bit32.band(bit32.rshift(triple,18), 63)+1,bit32.band(bit32.rshift(triple,18), 63)+1) out[#out + 1] = b64abc:sub(bit32.band(bit32.rshift(triple, 18), 63) + 1, bit32.band(bit32.rshift(triple, 18), 63) + 1)
out[#out+1]=b64abc:sub(bit32.band(bit32.rshift(triple,12), 63)+1,bit32.band(bit32.rshift(triple,12), 63)+1) out[#out + 1] = b64abc:sub(bit32.band(bit32.rshift(triple, 12), 63) + 1, bit32.band(bit32.rshift(triple, 12), 63) + 1)
out[#out+1]= i+1<=n and b64abc:sub(bit32.band(bit32.rshift(triple,6), 63)+1,bit32.band(bit32.rshift(triple,6), 63)+1) or '' out[#out + 1] = i + 1 <= n and
out[#out+1]= i+2<=n and b64abc:sub(bit32.band(bit32.rshift(triple,0), 63)+1,bit32.band(bit32.rshift(triple,0), 63)+1) or '' b64abc:sub(bit32.band(bit32.rshift(triple, 6), 63) + 1, bit32.band(bit32.rshift(triple, 6), 63) + 1) or ''
i=i+3 out[#out + 1] = i + 2 <= n and
b64abc:sub(bit32.band(bit32.rshift(triple, 0), 63) + 1, bit32.band(bit32.rshift(triple, 0), 63) + 1) or ''
i = i + 3
end end
return table.concat(out) return table.concat(out)
end end
local function b64u_decode(txt) local function b64u_decode(txt)
local map = {} local map = {}
for i=1,#b64abc do map[b64abc:sub(i,i)] = i-1 end for i = 1, #b64abc do map[b64abc:sub(i, i)] = i - 1 end
local out = {} local out = {}
local i=1 local i = 1
while i<=#txt do while i <= #txt do
local a = map[txt:sub(i,i)]; i=i+1 local a = map[txt:sub(i, i)]; i = i + 1
local b = map[txt:sub(i,i)]; i=i+1 local b = map[txt:sub(i, i)]; i = i + 1
local c = map[txt:sub(i,i)]; i=i+1 local c = map[txt:sub(i, i)]; i = i + 1
local d = map[txt:sub(i,i)]; i=i+1 local d = map[txt:sub(i, i)]; i = i + 1
if a==nil or b==nil then break end if a == nil or b == nil then break end
local triple = bit32.bor(bit32.lshift(a,18) , bit32.lshift(b,12) , bit32.lshift(c or 0,6) , (d or 0)) local triple = bit32.bor(bit32.lshift(a, 18), bit32.lshift(b, 12), bit32.lshift(c or 0, 6), (d or 0))
out[#out+1] = string.char(bit32.band(bit32.rshift(triple,16) , 255)) out[#out + 1] = string.char(bit32.band(bit32.rshift(triple, 16), 255))
if c ~= nil then out[#out+1] = string.char(bit32.band(bit32.rshift(triple,8) , 255)) end if c ~= nil then out[#out + 1] = string.char(bit32.band(bit32.rshift(triple, 8), 255)) end
if d ~= nil then out[#out+1] = string.char(bit32.band(triple , 255)) end if d ~= nil then out[#out + 1] = string.char(bit32.band(triple, 255)) end
end end
return table.concat(out) return table.concat(out)
end end
local function hmac_sha256(key, msg) local function hmac_sha256(key, msg)
local block = 64 local block = 64
if #key > block then key = (sha256(key):gsub('..', function(h) return string.char(tonumber(h,16)) end)) end if #key > block then key = (sha256(key):gsub('..', function(h) return string.char(tonumber(h, 16)) end)) end
if #key < block then key = key .. string.rep("\0", block - #key) end if #key < block then key = key .. string.rep("\0", block - #key) end
local o_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x5c)) end) local o_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x5c)) end)
local i_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c) , 0x36)) end) local i_key_pad = key:gsub('.', function(c) return string.char(bit32.bxor(string.byte(c), 0x36)) end)
local inner = sha256(i_key_pad .. msg) local inner = sha256(i_key_pad .. msg)
inner = inner:gsub('..', function(h) return string.char(tonumber(h,16)) end) inner = inner:gsub('..', function(h) return string.char(tonumber(h, 16)) end)
local mac = sha256(o_key_pad .. inner) local mac = sha256(o_key_pad .. inner)
return mac:gsub('..', function(h) return string.char(tonumber(h,16)) end) return mac:gsub('..', function(h) return string.char(tonumber(h, 16)) end)
end end
local function int_be(n) -- 32-bit BE local function int_be(n) -- 32-bit BE
return string.char(bit32.band(bit32.rshift(n,24), 255), bit32.band(bit32.rshift(n,16), 255), bit32.band(bit32.rshift(n,8), 255), bit32.band(n, 255)) return string.char(bit32.band(bit32.rshift(n, 24), 255), bit32.band(bit32.rshift(n, 16), 255),
bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255))
end end
local function pbkdf2_sha256(password, salt, iterations, dkLen) local function pbkdf2_sha256(password, salt, iterations, dkLen)
local hLen = 32 local hLen = 32
local l = math.ceil(dkLen / hLen) local l = math.ceil(dkLen / hLen)
local r = dkLen - (l-1)*hLen local r = dkLen - (l - 1) * hLen
local dk = {} local dk = {}
for i=1,l do for i = 1, l do
local U = hmac_sha256(password, salt .. int_be(i)) local U = hmac_sha256(password, salt .. int_be(i))
local T = {U:byte(1,hLen)} local T = { U:byte(1, hLen) }
for itr=2,iterations do for itr = 2, iterations do
U = hmac_sha256(password, U) U = hmac_sha256(password, U)
local Ub = {U:byte(1,hLen)} local Ub = { U:byte(1, hLen) }
for j=1,hLen do T[j] = bit32.bxor(T[j], Ub[j]) end for j = 1, hLen do T[j] = bit32.bxor(T[j], Ub[j]) end
if itr % 500 == 0 then sleep(0) end if itr % 500 == 0 then sleep(0) end
end end
if i<l then if i < l then
dk[#dk+1] = string.char(table.unpack(T)) dk[#dk + 1] = string.char(table.unpack(T))
else else
dk[#dk+1] = string.char(table.unpack(T, 1, r)) dk[#dk + 1] = string.char(table.unpack(T, 1, r))
end end
end end
return table.concat(dk) return table.concat(dk)
@ -83,20 +96,20 @@ end
-- HMAC-DRBG -- HMAC-DRBG
local SEED_PATH = "siss_drg_seed.bin" local SEED_PATH = "siss_drg_seed.bin"
local DRBG = {K = string.rep("\0",32), V = string.rep("\1",32), inited=false} local DRBG = { K = string.rep("\0", 32), V = string.rep("\1", 32), inited = false }
local function drbg_update(provided_data) local function drbg_update(provided_data)
DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\0" .. (provided_data or "")) DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\0" .. (provided_data or ""))
DRBG.V = hmac_sha256(DRBG.K, DRBG.V) DRBG.V = hmac_sha256(DRBG.K, DRBG.V)
if provided_data and #provided_data>0 then if provided_data and #provided_data > 0 then
DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\1" .. provided_data) DRBG.K = hmac_sha256(DRBG.K, DRBG.V .. "\1" .. provided_data)
DRBG.V = hmac_sha256(DRBG.K, DRBG.V) DRBG.V = hmac_sha256(DRBG.K, DRBG.V)
end end
end end
local function drbg_init(seed) local function drbg_init(seed)
DRBG.K = string.rep("\0",32) DRBG.K = string.rep("\0", 32)
DRBG.V = string.rep("\1",32) DRBG.V = string.rep("\1", 32)
drbg_update(seed) drbg_update(seed)
DRBG.inited = true DRBG.inited = true
end end
@ -106,27 +119,27 @@ local function drbg_bytes(n)
local out = {} local out = {}
while #table.concat(out) < n do while #table.concat(out) < n do
DRBG.V = hmac_sha256(DRBG.K, DRBG.V) DRBG.V = hmac_sha256(DRBG.K, DRBG.V)
out[#out+1] = DRBG.V out[#out + 1] = DRBG.V
end end
drbg_update("") drbg_update("")
local buf = table.concat(out) local buf = table.concat(out)
return buf:sub(1,n) return buf:sub(1, n)
end end
local function seed_entropy(extra_bytes) local function seed_entropy(extra_bytes)
local accum = {} local accum = {}
if fs.exists(SEED_PATH) then if fs.exists(SEED_PATH) then
local f = fs.open(SEED_PATH, "rb"); accum[#accum+1]=f.readAll(); f.close() local f = fs.open(SEED_PATH, "rb"); accum[#accum + 1] = f.readAll(); f.close()
local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h,16)) end) local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h, 16)) end)
drbg_init(seed) drbg_init(seed)
else else
term.write("Move around / mash keys then press Enter 4 times to seed…\n") term.write("Move around / mash keys then press Enter 4 times to seed…\n")
for i=1,4 do for i = 1, 4 do
local t1 = os.clock(); read(); local t2 = os.clock() local t1 = os.clock(); read(); local t2 = os.clock()
accum[#accum+1] = int_be(math.floor((t2 - t1)*1e9)) .. tostring({}):sub(8) accum[#accum + 1] = int_be(math.floor((t2 - t1) * 1e9)) .. tostring({}):sub(8)
end end
if extra_bytes and #extra_bytes>0 then accum[#accum+1]=extra_bytes end if extra_bytes and #extra_bytes > 0 then accum[#accum + 1] = extra_bytes end
local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h,16)) end) local seed = sha256(table.concat(accum)):gsub('..', function(h) return string.char(tonumber(h, 16)) end)
drbg_init(seed) drbg_init(seed)
local f = fs.open(SEED_PATH, "wb") local f = fs.open(SEED_PATH, "wb")
f.write(drbg_bytes(48)) f.write(drbg_bytes(48))
@ -138,15 +151,16 @@ local VERSION = "\1"
local KDF_ID = "\1" local KDF_ID = "\1"
local AAD = "SiSS RA rev1|chacha20poly1305" local AAD = "SiSS RA rev1|chacha20poly1305"
local function u32be(n) return string.char(bit32.band(bit32.rshift(n,24),255), bit32.band(bit32.rshift(n,16),255), bit32.band(bit32.rshift(n,8),255), bit32.band(n,255)) end local function u32be(n) return string.char(bit32.band(bit32.rshift(n, 24), 255), bit32.band(bit32.rshift(n, 16), 255),
local function u16be(n) return string.char(bit32.band(bit32.rshift(n,8),255), bit32.band(n,255)) end bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) end
local function u16be(n) return string.char(bit32.band(bit32.rshift(n, 8), 255), bit32.band(n, 255)) end
local function pack_params(iter) local function pack_params(iter)
return u32be(iter) .. u16be(32) return u32be(iter) .. u16be(32)
end end
local function unpack_params(s) local function unpack_params(s)
local i1,i2,i3,i4, d1,d2 = s:byte(1,6) local i1, i2, i3, i4, d1, d2 = s:byte(1, 6)
local iters = (bit32.bor(bit32.lshift(i1,24),bit32.lshift(i2,16),bit32.lshift(i3,8),i4)) local iters = (bit32.bor(bit32.lshift(i1, 24), bit32.lshift(i2, 16), bit32.lshift(i3, 8), i4))
local dklen = (bit32.bor(bit32.lshift(d1,8),d2)) local dklen = (bit32.bor(bit32.lshift(d1, 8), d2))
return iters, dklen return iters, dklen
end end
@ -170,24 +184,24 @@ local function encrypt_with_password(password, plaintext)
local ciphertext, tag = aead.encrypt(AAD, key, iv, constant, plaintext) local ciphertext, tag = aead.encrypt(AAD, key, iv, constant, plaintext)
local params = pack_params(iters) local params = pack_params(iters)
local blob = table.concat{ VERSION, KDF_ID, params, salt, nonce12, ciphertext, tag } local blob = table.concat { VERSION, KDF_ID, params, salt, nonce12, ciphertext, tag }
return b64u_encode(blob) return b64u_encode(blob)
end end
local function decrypt_with_password(password, blob) local function decrypt_with_password(password, blob)
local bin = b64u_decode(blob) local bin = b64u_decode(blob)
if #bin < 1+1+6+16+12+16 then return nil, "ciphertext too short" end if #bin < 1 + 1 + 6 + 16 + 12 + 16 then return nil, "ciphertext too short" end
local pos = 1 local pos = 1
local ver = bin:sub(pos,pos); pos=pos+1 local ver = bin:sub(pos, pos); pos = pos + 1
if ver ~= VERSION then return nil, "version mismatch" end if ver ~= VERSION then return nil, "version mismatch" end
local kdfid = bin:sub(pos,pos); pos=pos+1 local kdfid = bin:sub(pos, pos); pos = pos + 1
if kdfid ~= KDF_ID then return nil, "kdf mismatch" end if kdfid ~= KDF_ID then return nil, "kdf mismatch" end
local params = bin:sub(pos,pos+5); pos=pos+6 local params = bin:sub(pos, pos + 5); pos = pos + 6
local iters, dklen = unpack_params(params); if dklen ~= 32 then return nil, "bad dkLen" end local iters, dklen = unpack_params(params); if dklen ~= 32 then return nil, "bad dkLen" end
local salt = bin:sub(pos,pos+15); pos=pos+16 local salt = bin:sub(pos, pos + 15); pos = pos + 16
-- read the same 12-byte nonce and split the same way -- read the same 12-byte nonce and split the same way
local nonce12 = bin:sub(pos,pos+11); pos=pos+12 local nonce12 = bin:sub(pos, pos + 11); pos = pos + 12
local iv, constant = split_nonce12(nonce12) local iv, constant = split_nonce12(nonce12)
local tag_len = 16 local tag_len = 16

View file

@ -23,67 +23,67 @@ local poly1305 = require "lib.plc.poly1305"
-- poly1305 key generation -- poly1305 key generation
local poly_keygen = function(key, nonce) local poly_keygen = function(key, nonce)
local counter = 0 local counter = 0
local m = string.rep('\0', 64) local m = string.rep('\0', 64)
local e = chacha20.encrypt(key, counter, nonce, m) local e = chacha20.encrypt(key, counter, nonce, m)
-- keep only first the 256 bits (32 bytes) -- keep only first the 256 bits (32 bytes)
return e:sub(1, 32) return e:sub(1, 32)
end end
local pad16 = function(s) local pad16 = function(s)
-- return null bytes to add to s so that #s is a multiple of 16 -- return null bytes to add to s so that #s is a multiple of 16
return (#s % 16 == 0) and "" or ('\0'):rep(16 - (#s % 16)) return (#s % 16 == 0) and "" or ('\0'):rep(16 - (#s % 16))
end end
local app = table.insert local app = table.insert
local encrypt = function(aad, key, iv, constant, plain) local encrypt = function(aad, key, iv, constant, plain)
-- aad: additional authenticated data - arbitrary length -- aad: additional authenticated data - arbitrary length
-- key: 32-byte string -- key: 32-byte string
-- iv, constant: concatenated to form the nonce (12 bytes) -- iv, constant: concatenated to form the nonce (12 bytes)
-- (why not one 12-byte param? --maybe because IPsec uses -- (why not one 12-byte param? --maybe because IPsec uses
-- an 8-byte nonce) -- an 8-byte nonce)
-- implementation: RFC 7539 sect 2.8.1 -- implementation: RFC 7539 sect 2.8.1
-- (memory inefficient - encr text is copied in mac_data) -- (memory inefficient - encr text is copied in mac_data)
local mt = {} -- mac_data table local mt = {} -- mac_data table
local nonce = constant .. iv local nonce = constant .. iv
local otk = poly_keygen(key, nonce) local otk = poly_keygen(key, nonce)
local encr = chacha20.encrypt(key, 1, nonce, plain) local encr = chacha20.encrypt(key, 1, nonce, plain)
app(mt, aad) app(mt, aad)
app(mt, pad16(aad)) app(mt, pad16(aad))
app(mt, encr) app(mt, encr)
app(mt, pad16(encr)) app(mt, pad16(encr))
-- aad and encrypted text length must be encoded as -- aad and encrypted text length must be encoded as
-- little endian _u64_ (and not u32) -- see errata at -- little endian _u64_ (and not u32) -- see errata at
-- https://www.rfc-editor.org/errata_search.php?rfc=7539 -- https://www.rfc-editor.org/errata_search.php?rfc=7539
app(mt, string.pack('<I8', #aad)) app(mt, string.pack('<I8', #aad))
app(mt, string.pack('<I8', #encr)) app(mt, string.pack('<I8', #encr))
local mac_data = table.concat(mt) local mac_data = table.concat(mt)
--~ p16('mac', mac_data) --~ p16('mac', mac_data)
local tag = poly1305.auth(mac_data, otk) local tag = poly1305.auth(mac_data, otk)
return encr, tag return encr, tag
end --chacha20_aead_encrypt() end --chacha20_aead_encrypt()
local function decrypt(aad, key, iv, constant, encr, tag) local function decrypt(aad, key, iv, constant, encr, tag)
-- (memory inefficient - encr text is copied in mac_data) -- (memory inefficient - encr text is copied in mac_data)
-- (structure similar to aead_encrypt => what could be factored?) -- (structure similar to aead_encrypt => what could be factored?)
local mt = {} -- mac_data table local mt = {} -- mac_data table
local nonce = constant .. iv local nonce = constant .. iv
local otk = poly_keygen(key, nonce) local otk = poly_keygen(key, nonce)
app(mt, aad) app(mt, aad)
app(mt, pad16(aad)) app(mt, pad16(aad))
app(mt, encr) app(mt, encr)
app(mt, pad16(encr)) app(mt, pad16(encr))
app(mt, string.pack('<I8', #aad)) app(mt, string.pack('<I8', #aad))
app(mt, string.pack('<I8', #encr)) app(mt, string.pack('<I8', #encr))
local mac_data = table.concat(mt) local mac_data = table.concat(mt)
local mac = poly1305.auth(mac_data, otk) local mac = poly1305.auth(mac_data, otk)
if mac == tag then if mac == tag then
local plain = chacha20.encrypt(key, 1, nonce, encr) local plain = chacha20.encrypt(key, 1, nonce, encr)
return plain return plain
else else
return nil, "auth failed" return nil, "auth failed"
end end
end --chacha20_aead_decrypt() end --chacha20_aead_decrypt()
@ -91,7 +91,7 @@ end --chacha20_aead_decrypt()
-- return aead_chacha_poly module -- return aead_chacha_poly module
return { return {
poly_keygen = poly_keygen, poly_keygen = poly_keygen,
encrypt = encrypt, encrypt = encrypt,
decrypt = decrypt, decrypt = decrypt,
} }

View file

@ -27,178 +27,178 @@ local app, concat = table.insert, table.concat
local function qround(st, x, y, z, w) local function qround(st, x, y, z, w)
-- st is a chacha state: an array of 16 u32 words -- st is a chacha state: an array of 16 u32 words
-- x,y,z,w are indices in st -- x,y,z,w are indices in st
local a, b, c, d = st[x], st[y], st[z], st[w] local a, b, c, d = st[x], st[y], st[z], st[w]
local t local t
a = (a + b) % 0x100000000 a = (a + b) % 0x100000000
t = bit32.bxor(d, a) t = bit32.bxor(d, a)
d = bit32.band(bit32.bor(bit32.lshift(t, 16), bit32.rshift(t, 16)), 0xffffffff) d = bit32.band(bit32.bor(bit32.lshift(t, 16), bit32.rshift(t, 16)), 0xffffffff)
c = (c + d) % 0x100000000 c = (c + d) % 0x100000000
t = bit32.bxor(b, c) t = bit32.bxor(b, c)
b = bit32.band(bit32.bor(bit32.lshift(t, 12), bit32.rshift(t, 20)), 0xffffffff) b = bit32.band(bit32.bor(bit32.lshift(t, 12), bit32.rshift(t, 20)), 0xffffffff)
a = (a + b) % 0x100000000 a = (a + b) % 0x100000000
t = bit32.bxor(d, a) t = bit32.bxor(d, a)
d = bit32.band(bit32.bor(bit32.lshift(t, 8), bit32.rshift(t, 24)), 0xffffffff) d = bit32.band(bit32.bor(bit32.lshift(t, 8), bit32.rshift(t, 24)), 0xffffffff)
c = (c + d) % 0x100000000 c = (c + d) % 0x100000000
t = bit32.bxor(b, c) t = bit32.bxor(b, c)
b = bit32.band(bit32.bor(bit32.lshift(t, 7), bit32.rshift(t, 25)), 0xffffffff) b = bit32.band(bit32.bor(bit32.lshift(t, 7), bit32.rshift(t, 25)), 0xffffffff)
st[x], st[y], st[z], st[w] = a, b, c, d st[x], st[y], st[z], st[w] = a, b, c, d
return st return st
end end
-- chacha20 state and working state are allocated once and reused -- chacha20 state and working state are allocated once and reused
-- by each invocation of chacha20_block() -- by each invocation of chacha20_block()
local chacha20_state = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} local chacha20_state = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
local chacha20_working_state = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} local chacha20_working_state = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
local chacha20_block = function(key, counter, nonce) local chacha20_block = function(key, counter, nonce)
-- key: u32[8] -- key: u32[8]
-- counter: u32 -- counter: u32
-- nonce: u32[3] -- nonce: u32[3]
local st = chacha20_state -- state local st = chacha20_state -- state
local wst = chacha20_working_state -- working state local wst = chacha20_working_state -- working state
-- initialize state -- initialize state
st[1], st[2], st[3], st[4] = st[1], st[2], st[3], st[4] =
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574
for i = 1, 8 do st[i+4] = key[i] end for i = 1, 8 do st[i + 4] = key[i] end
st[13] = counter st[13] = counter
for i = 1, 3 do st[i+13] = nonce[i] end for i = 1, 3 do st[i + 13] = nonce[i] end
-- copy state to working_state -- copy state to working_state
for i = 1, 16 do wst[i] = st[i] end for i = 1, 16 do wst[i] = st[i] end
-- run 20 rounds, ie. 10 iterations of 8 quarter rounds -- run 20 rounds, ie. 10 iterations of 8 quarter rounds
for _ = 1, 10 do --RFC reference: for _ = 1, 10 do --RFC reference:
qround(wst, 1,5,9,13) --1. QUARTERROUND ( 0, 4, 8,12) qround(wst, 1, 5, 9, 13) --1. QUARTERROUND ( 0, 4, 8,12)
qround(wst, 2,6,10,14) --2. QUARTERROUND ( 1, 5, 9,13) qround(wst, 2, 6, 10, 14) --2. QUARTERROUND ( 1, 5, 9,13)
qround(wst, 3,7,11,15) --3. QUARTERROUND ( 2, 6,10,14) qround(wst, 3, 7, 11, 15) --3. QUARTERROUND ( 2, 6,10,14)
qround(wst, 4,8,12,16) --4. QUARTERROUND ( 3, 7,11,15) qround(wst, 4, 8, 12, 16) --4. QUARTERROUND ( 3, 7,11,15)
qround(wst, 1,6,11,16) --5. QUARTERROUND ( 0, 5,10,15) qround(wst, 1, 6, 11, 16) --5. QUARTERROUND ( 0, 5,10,15)
qround(wst, 2,7,12,13) --6. QUARTERROUND ( 1, 6,11,12) qround(wst, 2, 7, 12, 13) --6. QUARTERROUND ( 1, 6,11,12)
qround(wst, 3,8,9,14) --7. QUARTERROUND ( 2, 7, 8,13) qround(wst, 3, 8, 9, 14) --7. QUARTERROUND ( 2, 7, 8,13)
qround(wst, 4,5,10,15) --8. QUARTERROUND ( 3, 4, 9,14) qround(wst, 4, 5, 10, 15) --8. QUARTERROUND ( 3, 4, 9,14)
end end
-- add working_state to state -- add working_state to state
for i = 1, 16 do st[i] = bit32.band((st[i] + wst[i]), 0xffffffff) end for i = 1, 16 do st[i] = bit32.band((st[i] + wst[i]), 0xffffffff) end
-- return st, an array of 16 u32 words used as a keystream -- return st, an array of 16 u32 words used as a keystream
return st return st
end --chacha20_block() end --chacha20_block()
-- pat16: used to unpack a 64-byte string as 16 uint32 -- pat16: used to unpack a 64-byte string as 16 uint32
local pat16 = "<I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4" local pat16 = "<I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4I4"
local function chacha20_encrypt_block(key, counter, nonce, pt, ptidx) local function chacha20_encrypt_block(key, counter, nonce, pt, ptidx)
-- encrypt a 64-byte block of plain text. -- encrypt a 64-byte block of plain text.
-- key: 32 bytes as an array of 8 uint32 -- key: 32 bytes as an array of 8 uint32
-- counter: an uint32 (must be incremented for each block) -- counter: an uint32 (must be incremented for each block)
-- nonce: 12 bytes as an array of 3 uint32 -- nonce: 12 bytes as an array of 3 uint32
-- pt: plain text string, -- pt: plain text string,
-- ptidx: index of beginning of block in plain text (origin=1) -- ptidx: index of beginning of block in plain text (origin=1)
-- if less than 64 bytes are left at position ptidx, it is padded -- if less than 64 bytes are left at position ptidx, it is padded
-- with null bytes before encryption and result is stripped -- with null bytes before encryption and result is stripped
-- accordingly. -- accordingly.
-- return encrypted block as a string (length <= 16) -- return encrypted block as a string (length <= 16)
local rbn = #pt - ptidx + 1 -- number of remaining bytes in pt local rbn = #pt - ptidx + 1 -- number of remaining bytes in pt
if rbn < 64 then if rbn < 64 then
local tmp = string.sub(pt, ptidx) local tmp = string.sub(pt, ptidx)
pt = tmp .. string.rep('\0', 64 - rbn) --pad last block pt = tmp .. string.rep('\0', 64 - rbn) --pad last block
ptidx = 1 ptidx = 1
end end
assert(#pt >= 64) assert(#pt >= 64)
local ba = table.pack(string.unpack(pat16, pt, ptidx)) local ba = table.pack(string.unpack(pat16, pt, ptidx))
local keystream = chacha20_block(key, counter, nonce) local keystream = chacha20_block(key, counter, nonce)
for i = 1, 16 do for i = 1, 16 do
ba[i] = bit32.bxor(ba[i], keystream[i]) ba[i] = bit32.bxor(ba[i], keystream[i])
end end
local es = string.pack(pat16, table.unpack(ba)) local es = string.pack(pat16, table.unpack(ba))
if rbn < 64 then if rbn < 64 then
es = string.sub(es, 1, rbn) es = string.sub(es, 1, rbn)
end end
return es return es
end --chacha20_encrypt_block end --chacha20_encrypt_block
local chacha20_encrypt = function(key, counter, nonce, pt) local chacha20_encrypt = function(key, counter, nonce, pt)
-- encrypt plain text 'pt', return encrypted text -- encrypt plain text 'pt', return encrypted text
-- key: 32 bytes as a string -- key: 32 bytes as a string
-- counter: an uint32 (must be incremented for each block) -- counter: an uint32 (must be incremented for each block)
-- nonce: 8 bytes as a string -- nonce: 8 bytes as a string
-- pt: plain text string, -- pt: plain text string,
-- ensure counter can fit an uint32 --although it's unlikely -- ensure counter can fit an uint32 --although it's unlikely
-- that we hit this wall with pure Lua encryption :-) -- that we hit this wall with pure Lua encryption :-)
assert((counter + math.floor(#pt / 64) + 1) < 0xffffffff, assert((counter + math.floor(#pt / 64) + 1) < 0xffffffff,
"block counter must fit an uint32") "block counter must fit an uint32")
assert(#key == 32, "#key must be 32") assert(#key == 32, "#key must be 32")
assert(#nonce == 12, "#nonce must be 12") assert(#nonce == 12, "#nonce must be 12")
local keya = table.pack(string.unpack("<I4I4I4I4I4I4I4I4", key)) local keya = table.pack(string.unpack("<I4I4I4I4I4I4I4I4", key))
local noncea = table.pack(string.unpack("<I4I4I4", nonce)) local noncea = table.pack(string.unpack("<I4I4I4", nonce))
local t = {} -- used to collect all encrypted blocks local t = {} -- used to collect all encrypted blocks
local ptidx = 1 local ptidx = 1
while ptidx <= #pt do while ptidx <= #pt do
app(t, chacha20_encrypt_block(keya, counter, noncea, pt, ptidx)) app(t, chacha20_encrypt_block(keya, counter, noncea, pt, ptidx))
ptidx = ptidx + 64 ptidx = ptidx + 64
counter = counter + 1 counter = counter + 1
end end
local et = concat(t) local et = concat(t)
return et return et
end --chacha20_encrypt() end --chacha20_encrypt()
local function hchacha20(key, nonce16) local function hchacha20(key, nonce16)
-- key: string(32) -- key: string(32)
-- nonce16: string(16) -- nonce16: string(16)
local keya = table.pack(string.unpack("<I4I4I4I4I4I4I4I4", key)) local keya = table.pack(string.unpack("<I4I4I4I4I4I4I4I4", key))
local noncea = table.pack(string.unpack("<I4I4I4I4", nonce16)) local noncea = table.pack(string.unpack("<I4I4I4I4", nonce16))
local st = {} -- chacha working state local st = {} -- chacha working state
-- initialize state -- initialize state
st[1], st[2], st[3], st[4] = st[1], st[2], st[3], st[4] =
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574
for i = 1, 8 do st[i+4] = keya[i] end for i = 1, 8 do st[i + 4] = keya[i] end
for i = 1, 4 do st[i+12] = noncea[i] end for i = 1, 4 do st[i + 12] = noncea[i] end
-- run 20 rounds, ie. 10 iterations of 8 quarter rounds -- run 20 rounds, ie. 10 iterations of 8 quarter rounds
for _ = 1, 10 do --RFC reference: for _ = 1, 10 do --RFC reference:
qround(st, 1,5,9,13) --1. QUARTERROUND ( 0, 4, 8,12) qround(st, 1, 5, 9, 13) --1. QUARTERROUND ( 0, 4, 8,12)
qround(st, 2,6,10,14) --2. QUARTERROUND ( 1, 5, 9,13) qround(st, 2, 6, 10, 14) --2. QUARTERROUND ( 1, 5, 9,13)
qround(st, 3,7,11,15) --3. QUARTERROUND ( 2, 6,10,14) qround(st, 3, 7, 11, 15) --3. QUARTERROUND ( 2, 6,10,14)
qround(st, 4,8,12,16) --4. QUARTERROUND ( 3, 7,11,15) qround(st, 4, 8, 12, 16) --4. QUARTERROUND ( 3, 7,11,15)
qround(st, 1,6,11,16) --5. QUARTERROUND ( 0, 5,10,15) qround(st, 1, 6, 11, 16) --5. QUARTERROUND ( 0, 5,10,15)
qround(st, 2,7,12,13) --6. QUARTERROUND ( 1, 6,11,12) qround(st, 2, 7, 12, 13) --6. QUARTERROUND ( 1, 6,11,12)
qround(st, 3,8,9,14) --7. QUARTERROUND ( 2, 7, 8,13) qround(st, 3, 8, 9, 14) --7. QUARTERROUND ( 2, 7, 8,13)
qround(st, 4,5,10,15) --8. QUARTERROUND ( 3, 4, 9,14) qround(st, 4, 5, 10, 15) --8. QUARTERROUND ( 3, 4, 9,14)
end end
local subkey = string.pack("<I4I4I4I4I4I4I4I4", local subkey = string.pack("<I4I4I4I4I4I4I4I4",
st[1], st[2], st[3], st[4], st[1], st[2], st[3], st[4],
st[13], st[14], st[15], st[16] ) st[13], st[14], st[15], st[16])
return subkey return subkey
end --hchacha20() end --hchacha20()
local function xchacha20_encrypt(key, counter, nonce, pt) local function xchacha20_encrypt(key, counter, nonce, pt)
assert(#key == 32, "#key must be 32") assert(#key == 32, "#key must be 32")
assert(#nonce == 24, "#nonce must be 24") assert(#nonce == 24, "#nonce must be 24")
local subkey = hchacha20(key, nonce:sub(1, 16)) local subkey = hchacha20(key, nonce:sub(1, 16))
local nonce12 = '\0\0\0\0'..nonce:sub(17) local nonce12 = '\0\0\0\0' .. nonce:sub(17)
return chacha20_encrypt(subkey, counter, nonce12, pt) return chacha20_encrypt(subkey, counter, nonce12, pt)
end --xchacha20_encrypt() end --xchacha20_encrypt()
------------------------------------------------------------ ------------------------------------------------------------
return { return {
chacha20_encrypt = chacha20_encrypt, chacha20_encrypt = chacha20_encrypt,
chacha20_decrypt = chacha20_encrypt, -- xor encryption is symmetric chacha20_decrypt = chacha20_encrypt, -- xor encryption is symmetric
encrypt = chacha20_encrypt, --alias encrypt = chacha20_encrypt, --alias
decrypt = chacha20_encrypt, --alias decrypt = chacha20_encrypt, --alias
hchacha20 = hchacha20, hchacha20 = hchacha20,
xchacha20_encrypt = xchacha20_encrypt, xchacha20_encrypt = xchacha20_encrypt,
xchacha20_decrypt = xchacha20_encrypt, xchacha20_decrypt = xchacha20_encrypt,
-- --
key_size = 32, key_size = 32,
nonce_size = 12, -- nonce size for chacha20_encrypt nonce_size = 12, -- nonce size for chacha20_encrypt
xnonce_size = 24, -- nonce size for xchacha20_encrypt xnonce_size = 24, -- nonce size for xchacha20_encrypt
} }
--end of chacha20 --end of chacha20

View file

@ -13,192 +13,192 @@ local sunp = string.unpack
local bit32 = bit32 -- alias local bit32 = bit32 -- alias
local function poly_init(k) local function poly_init(k)
-- k: 32-byte key as a string -- k: 32-byte key as a string
-- initialize internal state -- initialize internal state
local st = { local st = {
r = { r = {
bit32.band(sunp('<I4', k, 1) , 0x3ffffff), -- r0 bit32.band(sunp('<I4', k, 1), 0x3ffffff), -- r0
bit32.band(bit32.rshift(sunp('<I4', k, 4), 2), 0x3ffff03), -- r1 bit32.band(bit32.rshift(sunp('<I4', k, 4), 2), 0x3ffff03), -- r1
bit32.band(bit32.rshift(sunp('<I4', k, 7), 4), 0x3ffc0ff), -- r2 bit32.band(bit32.rshift(sunp('<I4', k, 7), 4), 0x3ffc0ff), -- r2
bit32.band(bit32.rshift(sunp('<I4', k, 10), 6), 0x3f03fff), -- r3 bit32.band(bit32.rshift(sunp('<I4', k, 10), 6), 0x3f03fff), -- r3
bit32.band(bit32.rshift(sunp('<I4', k, 13), 8), 0x00fffff), -- r4 bit32.band(bit32.rshift(sunp('<I4', k, 13), 8), 0x00fffff), -- r4
}, },
h = { 0,0,0,0,0 }, h = { 0, 0, 0, 0, 0 },
pad = { sunp('<I4', k, 17), -- 's' in rfc pad = { sunp('<I4', k, 17), -- 's' in rfc
sunp('<I4', k, 21), sunp('<I4', k, 21),
sunp('<I4', k, 25), sunp('<I4', k, 25),
sunp('<I4', k, 29), sunp('<I4', k, 29),
}, },
buffer = "", -- buffer = "", --
leftover = 0, leftover = 0,
final = false, final = false,
}--st } --st
return st return st
end --poly_init() end --poly_init()
local function poly_blocks(st, m) local function poly_blocks(st, m)
-- st: internal state -- st: internal state
-- m: message:string -- m: message:string
local bytes = #m local bytes = #m
local midx = 1 local midx = 1
local hibit = st.final and 0 or 0x01000000 -- 1 << 24 local hibit = st.final and 0 or 0x01000000 -- 1 << 24
local r0 = st.r[1] local r0 = st.r[1]
local r1 = st.r[2] local r1 = st.r[2]
local r2 = st.r[3] local r2 = st.r[3]
local r3 = st.r[4] local r3 = st.r[4]
local r4 = st.r[5] local r4 = st.r[5]
local s1 = r1 * 5 local s1 = r1 * 5
local s2 = r2 * 5 local s2 = r2 * 5
local s3 = r3 * 5 local s3 = r3 * 5
local s4 = r4 * 5 local s4 = r4 * 5
local h0 = st.h[1] local h0 = st.h[1]
local h1 = st.h[2] local h1 = st.h[2]
local h2 = st.h[3] local h2 = st.h[3]
local h3 = st.h[4] local h3 = st.h[4]
local h4 = st.h[5] local h4 = st.h[5]
local d0, d1, d2, d3, d4, c local d0, d1, d2, d3, d4, c
-- --
while bytes >= 16 do -- 16 = poly1305_block_size while bytes >= 16 do -- 16 = poly1305_block_size
-- h += m[i] (in rfc: a += n with 0x01 byte) -- h += m[i] (in rfc: a += n with 0x01 byte)
h0 = h0 + bit32.band(sunp('<I4', m, midx ), 0x3ffffff) h0 = h0 + bit32.band(sunp('<I4', m, midx), 0x3ffffff)
h1 = h1 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 3), 2), 0x3ffffff) h1 = h1 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 3), 2), 0x3ffffff)
h2 = h2 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 6), 4), 0x3ffffff) h2 = h2 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 6), 4), 0x3ffffff)
h3 = h3 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 9), 6), 0x3ffffff) h3 = h3 + bit32.band(bit32.rshift(sunp('<I4', m, midx + 9), 6), 0x3ffffff)
h4 = h4 + bit32.bor(bit32.rshift(sunp('<I4', m, midx + 12), 8), hibit) -- 0x01 byte h4 = h4 + bit32.bor(bit32.rshift(sunp('<I4', m, midx + 12), 8), hibit) -- 0x01 byte
-- --
-- h *= r % p (partial) -- h *= r % p (partial)
d0 = h0*r0 + h1*s4 + h2*s3 + h3*s2 + h4*s1 d0 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1
d1 = h0*r1 + h1*r0 + h2*s4 + h3*s3 + h4*s2 d1 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2
d2 = h0*r2 + h1*r1 + h2*r0 + h3*s4 + h4*s3 d2 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3
d3 = h0*r3 + h1*r2 + h2*r1 + h3*r0 + h4*s4 d3 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4
d4 = h0*r4 + h1*r3 + h2*r2 + h3*r1 + h4*r0 d4 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0
-- --
c = bit32.band(bit32.rshift(d0,26), 0xffffffff) ; h0 = bit32.band(d0, 0x3ffffff) c = bit32.band(bit32.rshift(d0, 26), 0xffffffff); h0 = bit32.band(d0, 0x3ffffff)
d1 = d1 + c ; c = bit32.band(bit32.rshift(d1,26), 0xffffffff) ; h1 = bit32.band(d1, 0x3ffffff) d1 = d1 + c; c = bit32.band(bit32.rshift(d1, 26), 0xffffffff); h1 = bit32.band(d1, 0x3ffffff)
d2 = d2 + c ; c = bit32.band(bit32.rshift(d2,26), 0xffffffff) ; h2 = bit32.band(d2, 0x3ffffff) d2 = d2 + c; c = bit32.band(bit32.rshift(d2, 26), 0xffffffff); h2 = bit32.band(d2, 0x3ffffff)
d3 = d3 + c ; c = bit32.band(bit32.rshift(d3,26), 0xffffffff) ; h3 = bit32.band(d3, 0x3ffffff) d3 = d3 + c; c = bit32.band(bit32.rshift(d3, 26), 0xffffffff); h3 = bit32.band(d3, 0x3ffffff)
d4 = d4 + c ; c = bit32.band(bit32.rshift(d4,26), 0xffffffff) ; h4 = bit32.band(d4, 0x3ffffff) d4 = d4 + c; c = bit32.band(bit32.rshift(d4, 26), 0xffffffff); h4 = bit32.band(d4, 0x3ffffff)
h0 = h0 + (c*5) ; c = bit32.rshift(h0,26) ; h0 = bit32.band(h0, 0x3ffffff) h0 = h0 + (c * 5); c = bit32.rshift(h0, 26); h0 = bit32.band(h0, 0x3ffffff)
h1 = h1 + c h1 = h1 + c
-- --
midx = midx + 16 -- 16 = poly1305_block_size midx = midx + 16 -- 16 = poly1305_block_size
bytes = bytes - 16 bytes = bytes - 16
end --while end --while
st.h[1] = h0 st.h[1] = h0
st.h[2] = h1 st.h[2] = h1
st.h[3] = h2 st.h[3] = h2
st.h[4] = h3 st.h[4] = h3
st.h[5] = h4 st.h[5] = h4
st.bytes = bytes -- remaining bytes. must be < 16 here st.bytes = bytes -- remaining bytes. must be < 16 here
st.midx = midx -- index of first remaining bytes st.midx = midx -- index of first remaining bytes
return st return st
end --poly_blocks() end --poly_blocks()
local function poly_update(st, m) local function poly_update(st, m)
-- st: internal state -- st: internal state
-- m: message:string -- m: message:string
st.bytes, st.midx = #m, 1 st.bytes, st.midx = #m, 1
-- process full blocks if any -- process full blocks if any
if st.bytes >= 16 then if st.bytes >= 16 then
poly_blocks(st, m) poly_blocks(st, m)
end end
--handle remaining bytes --handle remaining bytes
if st.bytes == 0 then -- no bytes left if st.bytes == 0 then -- no bytes left
-- nothing to do? no add 0x01? - apparently not. -- nothing to do? no add 0x01? - apparently not.
else else
local buffer = string.sub(m, st.midx) local buffer = string.sub(m, st.midx)
.. '\x01' .. string.rep('\0', 16 - st.bytes -1) .. '\x01' .. string.rep('\0', 16 - st.bytes - 1)
assert(#buffer == 16) assert(#buffer == 16)
st.final = true -- this is the last block st.final = true -- this is the last block
--~ p16(buffer) --~ p16(buffer)
poly_blocks(st, buffer) poly_blocks(st, buffer)
end end
-- --
return st return st
end --poly_update end --poly_update
local function poly_finish(st) local function poly_finish(st)
-- --
local c, mask --u32 local c, mask --u32
local f --u64 local f --u64
-- fully carry h -- fully carry h
local h0 = st.h[1] local h0 = st.h[1]
local h1 = st.h[2] local h1 = st.h[2]
local h2 = st.h[3] local h2 = st.h[3]
local h3 = st.h[4] local h3 = st.h[4]
local h4 = st.h[5] local h4 = st.h[5]
-- --
c = bit32.rshift(h1,26); h1 = bit32.band(h1, 0x3ffffff) c = bit32.rshift(h1, 26); h1 = bit32.band(h1, 0x3ffffff)
h2 = h2 + c; c = bit32.rshift(h2, 26); h2 = bit32.band(h2, 0x3ffffff) h2 = h2 + c; c = bit32.rshift(h2, 26); h2 = bit32.band(h2, 0x3ffffff)
h3 = h3 + c; c = bit32.rshift(h3, 26); h3 = bit32.band(h3, 0x3ffffff) h3 = h3 + c; c = bit32.rshift(h3, 26); h3 = bit32.band(h3, 0x3ffffff)
h4 = h4 + c; c = bit32.rshift(h4, 26); h4 = bit32.band(h4, 0x3ffffff) h4 = h4 + c; c = bit32.rshift(h4, 26); h4 = bit32.band(h4, 0x3ffffff)
h0 = h0 + (c*5); c = bit32.rshift(h0, 26); h0 = bit32.band(h0, 0x3ffffff) h0 = h0 + (c * 5); c = bit32.rshift(h0, 26); h0 = bit32.band(h0, 0x3ffffff)
h1 = h1 + c h1 = h1 + c
-- --
--compute h + -p --compute h + -p
local g0 = (h0 + 5) ; c = bit32.rshift(g0, 26); g0 = bit32.band(g0, 0x3ffffff) local g0 = (h0 + 5); c = bit32.rshift(g0, 26); g0 = bit32.band(g0, 0x3ffffff)
local g1 = (h1 + c) ; c = bit32.rshift(g1, 26); g1 = bit32.band(g1, 0x3ffffff) local g1 = (h1 + c); c = bit32.rshift(g1, 26); g1 = bit32.band(g1, 0x3ffffff)
local g2 = (h2 + c) ; c = bit32.rshift(g2, 26); g2 = bit32.band(g2, 0x3ffffff) local g2 = (h2 + c); c = bit32.rshift(g2, 26); g2 = bit32.band(g2, 0x3ffffff)
local g3 = (h3 + c) ; c = bit32.rshift(g3, 26); g3 = bit32.band(g3, 0x3ffffff) local g3 = (h3 + c); c = bit32.rshift(g3, 26); g3 = bit32.band(g3, 0x3ffffff)
local g4 = bit32.band(h4 + c - 0x4000000, 0xffffffff) -- (1 << 26) local g4 = bit32.band(h4 + c - 0x4000000, 0xffffffff) -- (1 << 26)
-- --
-- select h if h < p, or h + -p if h >= p -- select h if h < p, or h + -p if h >= p
mask = bit32.band(bit32.rshift(g4, 31) - 1, 0xffffffff) mask = bit32.band(bit32.rshift(g4, 31) - 1, 0xffffffff)
-- --
g0 = bit32.band(g0, mask) g0 = bit32.band(g0, mask)
g1 = bit32.band(g1, mask) g1 = bit32.band(g1, mask)
g2 = bit32.band(g2, mask) g2 = bit32.band(g2, mask)
g3 = bit32.band(g3, mask) g3 = bit32.band(g3, mask)
g4 = bit32.band(g4, mask) g4 = bit32.band(g4, mask)
-- --
mask = bit32.band(bit32.bnot(mask), 0xffffffff) mask = bit32.band(bit32.bnot(mask), 0xffffffff)
h0 = bit32.bor(bit32.band(h0, mask), g0) h0 = bit32.bor(bit32.band(h0, mask), g0)
h1 = bit32.bor(bit32.band(h1, mask), g1) h1 = bit32.bor(bit32.band(h1, mask), g1)
h2 = bit32.bor(bit32.band(h2, mask), g2) h2 = bit32.bor(bit32.band(h2, mask), g2)
h3 = bit32.bor(bit32.band(h3, mask), g3) h3 = bit32.bor(bit32.band(h3, mask), g3)
h4 = bit32.bor(bit32.band(h4, mask), g4) h4 = bit32.bor(bit32.band(h4, mask), g4)
-- --
--h = h % (2^128) --h = h % (2^128)
h0 = bit32.band(bit32.bor((h0), bit32.lshift(h1, 26)), 0xffffffff) h0 = bit32.band(bit32.bor((h0), bit32.lshift(h1, 26)), 0xffffffff)
h1 = bit32.band(bit32.bor(bit32.rshift(h1, 6), bit32.lshift(h2, 20)), 0xffffffff) h1 = bit32.band(bit32.bor(bit32.rshift(h1, 6), bit32.lshift(h2, 20)), 0xffffffff)
h2 = bit32.band(bit32.bor(bit32.rshift(h2, 12), bit32.lshift(h3, 14)), 0xffffffff) h2 = bit32.band(bit32.bor(bit32.rshift(h2, 12), bit32.lshift(h3, 14)), 0xffffffff)
h3 = bit32.band(bit32.bor(bit32.rshift(h3, 18), bit32.lshift(h4, 8)), 0xffffffff) h3 = bit32.band(bit32.bor(bit32.rshift(h3, 18), bit32.lshift(h4, 8)), 0xffffffff)
-- --
-- mac = (h + pad) % (2^128) -- mac = (h + pad) % (2^128)
f = h0 + st.pad[1] ; h0 = bit32.band(f, 0xffffffff) f = h0 + st.pad[1]; h0 = bit32.band(f, 0xffffffff)
f = h1 + st.pad[2] + bit32.rshift(f, 32) ; h1 = bit32.band(f, 0xffffffff) f = h1 + st.pad[2] + bit32.rshift(f, 32); h1 = bit32.band(f, 0xffffffff)
f = h2 + st.pad[3] + bit32.rshift(f, 32) ; h2 = bit32.band(f, 0xffffffff) f = h2 + st.pad[3] + bit32.rshift(f, 32); h2 = bit32.band(f, 0xffffffff)
f = h3 + st.pad[4] + bit32.rshift(f, 32) ; h3 = bit32.band(f, 0xffffffff) f = h3 + st.pad[4] + bit32.rshift(f, 32); h3 = bit32.band(f, 0xffffffff)
-- --
local mac = string.pack('<I4I4I4I4', h0, h1, h2, h3) local mac = string.pack('<I4I4I4I4', h0, h1, h2, h3)
-- (should zero out the state?) -- (should zero out the state?)
-- --
return mac return mac
end --poly_finish() end --poly_finish()
local function poly_auth(m, k) local function poly_auth(m, k)
-- m: msg string -- m: msg string
-- k: key string (must be 32 bytes) -- k: key string (must be 32 bytes)
-- return mac 16-byte string -- return mac 16-byte string
assert(#k == 32) assert(#k == 32)
local st = poly_init(k) local st = poly_init(k)
poly_update(st, m) poly_update(st, m)
local mac = poly_finish(st) local mac = poly_finish(st)
return mac return mac
end --poly_auth() end --poly_auth()
local function poly_verify(m, k, mac) local function poly_verify(m, k, mac)
local macm = poly_auth(m, k) local macm = poly_auth(m, k)
return macm == mac return macm == mac
end --poly_verify() end --poly_verify()
------------------------------------------------------------ ------------------------------------------------------------
-- return poly1305 module -- return poly1305 module
return { return {
init = poly_init, init = poly_init,
update = poly_update, update = poly_update,
finish = poly_finish, finish = poly_finish,
auth = poly_auth, auth = poly_auth,
verify = poly_verify, verify = poly_verify,
} }

View file

@ -9,84 +9,84 @@ local expect = require "cc.expect".expect
-- Initialization code -- Initialization code
local PrimeUI = {} local PrimeUI = {}
do do
local coros = {} local coros = {}
local restoreCursor local restoreCursor
--- Adds a task to run in the main loop. --- Adds a task to run in the main loop.
---@param func function The function to run, usually an `os.pullEvent` loop ---@param func function The function to run, usually an `os.pullEvent` loop
function PrimeUI.addTask(func) function PrimeUI.addTask(func)
expect(1, func, "function") expect(1, func, "function")
local t = {coro = coroutine.create(func)} local t = { coro = coroutine.create(func) }
coros[#coros+1] = t coros[#coros + 1] = t
_, t.filter = coroutine.resume(t.coro) _, t.filter = coroutine.resume(t.coro)
end
--- Sends the provided arguments to the run loop, where they will be returned.
---@param ... any The parameters to send
function PrimeUI.resolve(...)
coroutine.yield(coros, ...)
end
--- Clears the screen and resets all components. Do not use any previously
--- created components after calling this function.
function PrimeUI.clear()
-- Reset the screen.
term.setCursorPos(1, 1)
term.setCursorBlink(false)
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
term.clear()
-- Reset the task list and cursor restore function.
coros = {}
restoreCursor = nil
end
--- Sets or clears the window that holds where the cursor should be.
---@param win window|nil The window to set as the active window
function PrimeUI.setCursorWindow(win)
expect(1, win, "table", "nil")
restoreCursor = win and win.restoreCursor
end
--- Gets the absolute position of a coordinate relative to a window.
---@param win window The window to check
---@param x number The relative X position of the point
---@param y number The relative Y position of the point
---@return number x The absolute X position of the window
---@return number y The absolute Y position of the window
function PrimeUI.getWindowPos(win, x, y)
if win == term then return x, y end
while win ~= term.native() and win ~= term.current() do
if not win.getPosition then return x, y end
local wx, wy = win.getPosition()
x, y = x + wx - 1, y + wy - 1
_, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue
end end
return x, y
end
--- Sends the provided arguments to the run loop, where they will be returned. --- Runs the main loop, returning information on an action.
---@param ... any The parameters to send ---@return any ... The result of the coroutine that exited
function PrimeUI.resolve(...) function PrimeUI.run()
coroutine.yield(coros, ...) while true do
end -- Restore the cursor and wait for the next event.
if restoreCursor then restoreCursor() end
--- Clears the screen and resets all components. Do not use any previously local ev = table.pack(os.pullEvent())
--- created components after calling this function. -- Run all coroutines.
function PrimeUI.clear() for _, v in ipairs(coros) do
-- Reset the screen. if v.filter == nil or v.filter == ev[1] then
term.setCursorPos(1, 1) -- Resume the coroutine, passing the current event.
term.setCursorBlink(false) local res = table.pack(coroutine.resume(v.coro, table.unpack(ev, 1, ev.n)))
term.setBackgroundColor(colors.black) -- If the call failed, bail out. Coroutines should never exit.
term.setTextColor(colors.white) if not res[1] then error(res[2], 2) end
term.clear() -- If the coroutine resolved, return its values.
-- Reset the task list and cursor restore function. if res[2] == coros then return table.unpack(res, 3, res.n) end
coros = {} -- Set the next event filter.
restoreCursor = nil v.filter = res[2]
end
--- Sets or clears the window that holds where the cursor should be.
---@param win window|nil The window to set as the active window
function PrimeUI.setCursorWindow(win)
expect(1, win, "table", "nil")
restoreCursor = win and win.restoreCursor
end
--- Gets the absolute position of a coordinate relative to a window.
---@param win window The window to check
---@param x number The relative X position of the point
---@param y number The relative Y position of the point
---@return number x The absolute X position of the window
---@return number y The absolute Y position of the window
function PrimeUI.getWindowPos(win, x, y)
if win == term then return x, y end
while win ~= term.native() and win ~= term.current() do
if not win.getPosition then return x, y end
local wx, wy = win.getPosition()
x, y = x + wx - 1, y + wy - 1
_, win = debug.getupvalue(select(2, debug.getupvalue(win.isColor, 1)), 1) -- gets the parent window through an upvalue
end
return x, y
end
--- Runs the main loop, returning information on an action.
---@return any ... The result of the coroutine that exited
function PrimeUI.run()
while true do
-- Restore the cursor and wait for the next event.
if restoreCursor then restoreCursor() end
local ev = table.pack(os.pullEvent())
-- Run all coroutines.
for _, v in ipairs(coros) do
if v.filter == nil or v.filter == ev[1] then
-- Resume the coroutine, passing the current event.
local res = table.pack(coroutine.resume(v.coro, table.unpack(ev, 1, ev.n)))
-- If the call failed, bail out. Coroutines should never exit.
if not res[1] then error(res[2], 2) end
-- If the coroutine resolved, return its values.
if res[2] == coros then return table.unpack(res, 3, res.n) end
-- Set the next event filter.
v.filter = res[2]
end
end
end end
end
end end
end
end end
--- Creates a text input box. --- Creates a text input box.
@ -102,102 +102,103 @@ end
---@param replacement string|nil A character to replace typed characters with ---@param replacement string|nil A character to replace typed characters with
---@param history string[]|nil A list of previous entries to provide ---@param history string[]|nil A list of previous entries to provide
---@param completion function|nil A function to call to provide completion ---@param completion function|nil A function to call to provide completion
function PrimeUI.inputBox(win, x, y, width, action, placeholder, fgColor, bgColor, placeholderFg, replacement, history, completion, default) function PrimeUI.inputBox(win, x, y, width, action, placeholder, fgColor, bgColor, placeholderFg, replacement, history,
expect(1, win, "table") completion, default)
expect(2, x, "number") expect(1, win, "table")
expect(3, y, "number") expect(2, x, "number")
expect(4, width, "number") expect(3, y, "number")
expect(5, action, "function", "string") expect(4, width, "number")
expect(6, placeholder, "string", "nil") expect(5, action, "function", "string")
fgColor = expect(7, fgColor, "number", "nil") or colors.white expect(6, placeholder, "string", "nil")
bgColor = expect(8, bgColor, "number", "nil") or colors.black fgColor = expect(7, fgColor, "number", "nil") or colors.white
placeholderFg = expect(9, placeholderFg, "number", "nil") or colors.lightGray bgColor = expect(8, bgColor, "number", "nil") or colors.black
expect(10, replacement, "string", "nil") placeholderFg = expect(9, placeholderFg, "number", "nil") or colors.lightGray
expect(11, history, "table", "nil") expect(10, replacement, "string", "nil")
expect(12, completion, "function", "nil") expect(11, history, "table", "nil")
expect(13, default, "string", "nil") expect(12, completion, "function", "nil")
expect(13, default, "string", "nil")
local box = window.create(win, x, y, width, 1) local box = window.create(win, x, y, width, 1)
box.setTextColor(fgColor) box.setTextColor(fgColor)
box.setBackgroundColor(bgColor) box.setBackgroundColor(bgColor)
box.clear() box.clear()
PrimeUI.addTask(function() PrimeUI.addTask(function()
local text = default or "" local text = default or ""
local cursor = #text + 1 local cursor = #text + 1
local histIndex = nil local histIndex = nil
local function runAction() local function runAction()
if type(action) == "string" then if type(action) == "string" then
PrimeUI.resolve("inputBox", action, text) PrimeUI.resolve("inputBox", action, text)
else else
action(text) action(text)
end end
end
local function redraw()
box.clear()
box.setCursorPos(1, 1)
if replacement then
box.write(string.rep(replacement, #text))
else
if #text == 0 and placeholder then
box.setTextColor(placeholderFg)
box.write(placeholder)
box.setTextColor(fgColor)
else
box.write(text)
end end
end
box.setCursorPos(cursor, 1)
runAction()
end
local function redraw() redraw()
box.clear()
box.setCursorPos(1, 1)
if replacement then
box.write(string.rep(replacement, #text))
else
if #text == 0 and placeholder then
box.setTextColor(placeholderFg)
box.write(placeholder)
box.setTextColor(fgColor)
else
box.write(text)
end
end
box.setCursorPos(cursor, 1)
runAction()
end
while true do
local ev, p1 = os.pullEvent()
if ev == "char" then
text = text:sub(1, cursor - 1) .. p1 .. text:sub(cursor)
cursor = cursor + 1
redraw() redraw()
elseif ev == "key" then
while true do if p1 == keys.enter then
local ev, p1 = os.pullEvent() runAction()
if ev == "char" then elseif p1 == keys.left then
text = text:sub(1, cursor - 1) .. p1 .. text:sub(cursor) cursor = math.max(1, cursor - 1)
cursor = cursor + 1 box.setCursorPos(cursor, 1)
redraw() elseif p1 == keys.right then
elseif ev == "key" then cursor = math.min(#text + 1, cursor + 1)
if p1 == keys.enter then box.setCursorPos(cursor, 1)
runAction() elseif p1 == keys.backspace then
elseif p1 == keys.left then if cursor > 1 then
cursor = math.max(1, cursor - 1) text = text:sub(1, cursor - 2) .. text:sub(cursor)
box.setCursorPos(cursor, 1) cursor = cursor - 1
elseif p1 == keys.right then redraw()
cursor = math.min(#text + 1, cursor + 1) end
box.setCursorPos(cursor, 1) elseif p1 == keys.delete then
elseif p1 == keys.backspace then text = text:sub(1, cursor - 1) .. text:sub(cursor + 1)
if cursor > 1 then redraw()
text = text:sub(1, cursor - 2) .. text:sub(cursor) elseif p1 == keys.up and history then
cursor = cursor - 1 if not histIndex then histIndex = #history + 1 end
redraw() histIndex = math.max(1, histIndex - 1)
end text = history[histIndex] or ""
elseif p1 == keys.delete then cursor = #text + 1
text = text:sub(1, cursor - 1) .. text:sub(cursor + 1) redraw()
redraw() elseif p1 == keys.down and history then
elseif p1 == keys.up and history then if histIndex then
if not histIndex then histIndex = #history + 1 end histIndex = math.min(#history + 1, histIndex + 1)
histIndex = math.max(1, histIndex - 1) text = history[histIndex] or ""
text = history[histIndex] or "" cursor = #text + 1
cursor = #text + 1 redraw()
redraw() end
elseif p1 == keys.down and history then
if histIndex then
histIndex = math.min(#history + 1, histIndex + 1)
text = history[histIndex] or ""
cursor = #text + 1
redraw()
end
end
end
end end
end) end
end
end)
end end
--- Draws a line of text at a position. --- Draws a line of text at a position.
@ -208,16 +209,16 @@ end
---@param fgColor color|nil The color of the text (defaults to white) ---@param fgColor color|nil The color of the text (defaults to white)
---@param bgColor color|nil The color of the background (defaults to black) ---@param bgColor color|nil The color of the background (defaults to black)
function PrimeUI.label(win, x, y, text, fgColor, bgColor) function PrimeUI.label(win, x, y, text, fgColor, bgColor)
expect(1, win, "table") expect(1, win, "table")
expect(2, x, "number") expect(2, x, "number")
expect(3, y, "number") expect(3, y, "number")
expect(4, text, "string") expect(4, text, "string")
fgColor = expect(5, fgColor, "number", "nil") or colors.white fgColor = expect(5, fgColor, "number", "nil") or colors.white
bgColor = expect(6, bgColor, "number", "nil") or colors.black bgColor = expect(6, bgColor, "number", "nil") or colors.black
win.setCursorPos(x, y) win.setCursorPos(x, y)
win.setTextColor(fgColor) win.setTextColor(fgColor)
win.setBackgroundColor(bgColor) win.setBackgroundColor(bgColor)
win.write(text) win.write(text)
end end
--- Creates a scrollable window, which allows drawing large content in a small area. --- Creates a scrollable window, which allows drawing large content in a small area.
@ -234,88 +235,90 @@ end
---@return window inner The inner window to draw inside ---@return window inner The inner window to draw inside
---@return fun(pos:number) scroll A function to manually set the scroll position of the window ---@return fun(pos:number) scroll A function to manually set the scroll position of the window
function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor) function PrimeUI.scrollBox(win, x, y, width, height, innerHeight, allowArrowKeys, showScrollIndicators, fgColor, bgColor)
expect(1, win, "table") expect(1, win, "table")
expect(2, x, "number") expect(2, x, "number")
expect(3, y, "number") expect(3, y, "number")
expect(4, width, "number") expect(4, width, "number")
expect(5, height, "number") expect(5, height, "number")
expect(6, innerHeight, "number") expect(6, innerHeight, "number")
expect(7, allowArrowKeys, "boolean", "nil") expect(7, allowArrowKeys, "boolean", "nil")
expect(8, showScrollIndicators, "boolean", "nil") expect(8, showScrollIndicators, "boolean", "nil")
fgColor = expect(9, fgColor, "number", "nil") or colors.white fgColor = expect(9, fgColor, "number", "nil") or colors.white
bgColor = expect(10, bgColor, "number", "nil") or colors.black bgColor = expect(10, bgColor, "number", "nil") or colors.black
if allowArrowKeys == nil then allowArrowKeys = true end if allowArrowKeys == nil then allowArrowKeys = true end
-- Create the outer container box. -- Create the outer container box.
local outer = window.create(win == term and term.current() or win, x, y, width, height) local outer = window.create(win == term and term.current() or win, x, y, width, height)
outer.setBackgroundColor(bgColor)
outer.clear()
-- Create the inner scrolling box.
local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight)
inner.setBackgroundColor(bgColor)
inner.clear()
-- Draw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor) outer.setBackgroundColor(bgColor)
outer.clear() outer.setTextColor(fgColor)
-- Create the inner scrolling box. outer.setCursorPos(width, height)
local inner = window.create(outer, 1, 1, width - (showScrollIndicators and 1 or 0), innerHeight) outer.write(innerHeight > height and "\31" or " ")
inner.setBackgroundColor(bgColor) end
inner.clear() -- Get the absolute position of the window.
-- Draw scroll indicators if desired. x, y = PrimeUI.getWindowPos(win, x, y)
if showScrollIndicators then -- Add the scroll handler.
local scrollPos = 1
PrimeUI.addTask(function()
while true do
-- Wait for next event.
local ev = table.pack(os.pullEvent())
-- Update inner height in case it changed.
innerHeight = select(2, inner.getSize())
-- Check for scroll events and set direction.
local dir
if ev[1] == "key" and allowArrowKeys then
if ev[2] == keys.up then
dir = -1
elseif ev[2] == keys.down then
dir = 1
end
elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then
dir = ev[2]
end
-- If there's a scroll event, move the window vertically.
if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
scrollPos = scrollPos + dir
inner.reposition(1, 2 - scrollPos)
end
-- Redraw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor) outer.setBackgroundColor(bgColor)
outer.setTextColor(fgColor) outer.setTextColor(fgColor)
outer.setCursorPos(width, 1)
outer.write(scrollPos > 1 and "\30" or " ")
outer.setCursorPos(width, height) outer.setCursorPos(width, height)
outer.write(innerHeight > height and "\31" or " ") outer.write(scrollPos < innerHeight - height and "\31" or " ")
end
end end
-- Get the absolute position of the window. end)
x, y = PrimeUI.getWindowPos(win, x, y) -- Make a function to allow external scrolling.
-- Add the scroll handler. local function scroll(pos)
local scrollPos = 1 expect(1, pos, "number")
PrimeUI.addTask(function() pos = math.floor(pos)
while true do expect.range(pos, 1, innerHeight - height)
-- Wait for next event. -- Scroll the window.
local ev = table.pack(os.pullEvent()) scrollPos = pos
-- Update inner height in case it changed. inner.reposition(1, 2 - scrollPos)
innerHeight = select(2, inner.getSize()) -- Redraw scroll indicators if desired.
-- Check for scroll events and set direction. if showScrollIndicators then
local dir outer.setBackgroundColor(bgColor)
if ev[1] == "key" and allowArrowKeys then outer.setTextColor(fgColor)
if ev[2] == keys.up then dir = -1 outer.setCursorPos(width, 1)
elseif ev[2] == keys.down then dir = 1 end outer.write(scrollPos > 1 and "\30" or " ")
elseif ev[1] == "mouse_scroll" and ev[3] >= x and ev[3] < x + width and ev[4] >= y and ev[4] < y + height then outer.setCursorPos(width, height)
dir = ev[2] outer.write(scrollPos < innerHeight - height and "\31" or " ")
end
-- If there's a scroll event, move the window vertically.
if dir and (scrollPos + dir >= 1 and scrollPos + dir <= innerHeight - height) then
scrollPos = scrollPos + dir
inner.reposition(1, 2 - scrollPos)
end
-- Redraw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor)
outer.setTextColor(fgColor)
outer.setCursorPos(width, 1)
outer.write(scrollPos > 1 and "\30" or " ")
outer.setCursorPos(width, height)
outer.write(scrollPos < innerHeight - height and "\31" or " ")
end
end
end)
-- Make a function to allow external scrolling.
local function scroll(pos)
expect(1, pos, "number")
pos = math.floor(pos)
expect.range(pos, 1, innerHeight - height)
-- Scroll the window.
scrollPos = pos
inner.reposition(1, 2 - scrollPos)
-- Redraw scroll indicators if desired.
if showScrollIndicators then
outer.setBackgroundColor(bgColor)
outer.setTextColor(fgColor)
outer.setCursorPos(width, 1)
outer.write(scrollPos > 1 and "\30" or " ")
outer.setCursorPos(width, height)
outer.write(scrollPos < innerHeight - height and "\31" or " ")
end
end end
return inner, scroll end
return inner, scroll
end end
--- Creates a list of entries that can each be selected. --- Creates a list of entries that can each be selected.
---@param win window The window to draw on ---@param win window The window to draw on
---@param x number The X coordinate of the inside of the box ---@param x number The X coordinate of the inside of the box
@ -328,152 +331,178 @@ end
---@param fgColor color|nil The color of the text (defaults to white) ---@param fgColor color|nil The color of the text (defaults to white)
---@param bgColor color|nil The color of the background (defaults to black) ---@param bgColor color|nil The color of the background (defaults to black)
function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor) function PrimeUI.selectionBox(win, x, y, width, height, entries, action, selectChangeAction, fgColor, bgColor)
expect(1, win, "table") expect(1, win, "table")
expect(2, x, "number") expect(2, x, "number")
expect(3, y, "number") expect(3, y, "number")
expect(4, width, "number") expect(4, width, "number")
expect(5, height, "number") expect(5, height, "number")
expect(6, entries, "function") expect(6, entries, "function")
expect(7, action, "function", "string") expect(7, action, "function", "string")
expect(8, selectChangeAction, "function", "string", "nil") expect(8, selectChangeAction, "function", "string", "nil")
fgColor = expect(9, fgColor, "number", "nil") or colors.white fgColor = expect(9, fgColor, "number", "nil") or colors.white
bgColor = expect(10, bgColor, "number", "nil") or colors.black bgColor = expect(10, bgColor, "number", "nil") or colors.black
local entrywin = window.create(win, x, y, width, height) local entrywin = window.create(win, x, y, width, height)
local selection, scroll = 1, 1 local selection, scroll = 1, 1
-- Create a function to redraw the entries on screen. -- Create a function to redraw the entries on screen.
local function drawEntries() local function drawEntries()
-- Clear and set invisible for performance. -- Clear and set invisible for performance.
entrywin.setVisible(false) entrywin.setVisible(false)
entrywin.setBackgroundColor(bgColor) entrywin.setBackgroundColor(bgColor)
entrywin.clear() entrywin.clear()
-- Draw each entry in the scrolled region. -- Draw each entry in the scrolled region.
for i = scroll, scroll + height - 1 do for i = scroll, scroll + height - 1 do
-- Get the entry; stop if there's no more. -- Get the entry; stop if there's no more.
local e = entries()[i] local e = entries()[i]
if not e then break end if not e then break end
-- Set the colors: invert if selected. -- Set the colors: invert if selected.
entrywin.setCursorPos(2, i - scroll + 1) entrywin.setCursorPos(2, i - scroll + 1)
if i == selection then if i == selection then
entrywin.setBackgroundColor(fgColor) entrywin.setBackgroundColor(fgColor)
entrywin.setTextColor(bgColor) entrywin.setTextColor(bgColor)
else else
entrywin.setBackgroundColor(bgColor)
entrywin.setTextColor(fgColor)
end
-- Draw the selection.
entrywin.clearLine()
entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
end
-- Draw scroll arrows.
entrywin.setBackgroundColor(bgColor) entrywin.setBackgroundColor(bgColor)
entrywin.setTextColor(fgColor) entrywin.setTextColor(fgColor)
entrywin.setCursorPos(width, 1) end
entrywin.write("\30") -- Draw the selection.
entrywin.setCursorPos(width, height) entrywin.clearLine()
entrywin.write("\31") entrywin.write(#e > width - 1 and e:sub(1, width - 4) .. "..." or e)
-- Send updates to the screen.
entrywin.setVisible(true)
end end
-- Draw first screen. -- Draw scroll arrows.
drawEntries() entrywin.setBackgroundColor(bgColor)
-- Add a task for selection keys. entrywin.setTextColor(fgColor)
PrimeUI.addTask(function() entrywin.setCursorPos(width, 1)
while true do entrywin.write("\30")
local event, key, cx, cy = os.pullEvent() entrywin.setCursorPos(width, height)
if event == "key" then entrywin.write("\31")
if key == keys.down and selection < #entries() then -- Send updates to the screen.
-- Move selection down. entrywin.setVisible(true)
selection = selection + 1 end
if selection > scroll + height - 1 then scroll = scroll + 1 end -- Draw first screen.
-- Send action if necessary. drawEntries()
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection) -- Add a task for selection keys.
elseif selectChangeAction then selectChangeAction(selection) end PrimeUI.addTask(function()
-- Redraw screen. while true do
drawEntries() local event, key, cx, cy = os.pullEvent()
elseif key == keys.up and selection > 1 then if event == "key" then
-- Move selection up. if key == keys.down and selection < #entries() then
selection = selection - 1 -- Move selection down.
if selection < scroll then scroll = scroll - 1 end selection = selection + 1
-- Send action if necessary. if selection > scroll + height - 1 then scroll = scroll + 1 end
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection) -- Send action if necessary.
elseif selectChangeAction then selectChangeAction(selection) end if type(selectChangeAction) == "string" then
-- Redraw screen. PrimeUI.resolve("selectionBox", selectChangeAction, selection)
drawEntries() elseif selectChangeAction then
elseif key == keys.enter then selectChangeAction(selection)
-- Select the entry: send the action. end
if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries()[selection]) -- Redraw screen.
else action(entries()[selection]) end drawEntries()
end elseif key == keys.up and selection > 1 then
elseif event == "mouse_click" and key == 1 then -- Move selection up.
-- Handle clicking the scroll arrows. selection = selection - 1
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1) if selection < scroll then scroll = scroll - 1 end
if cx == wx + width - 1 then -- Send action if necessary.
if cy == wy and selection > 1 then if type(selectChangeAction) == "string" then
-- Move selection up. PrimeUI.resolve("selectionBox", selectChangeAction, selection)
selection = selection - 1 elseif selectChangeAction then
if selection < scroll then scroll = scroll - 1 end selectChangeAction(selection)
-- Send action if necessary. end
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection) -- Redraw screen.
elseif selectChangeAction then selectChangeAction(selection) end drawEntries()
-- Redraw screen. elseif key == keys.enter then
drawEntries() -- Select the entry: send the action.
elseif cy == wy + height - 1 and selection < #entries() then if type(action) == "string" then
-- Move selection down. PrimeUI.resolve("selectionBox", action, entries()[selection])
selection = selection + 1 else
if selection > scroll + height - 1 then scroll = scroll + 1 end action(entries()[selection])
-- Send action if necessary. end
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
end
elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
local sel = scroll + (cy - wy)
if sel == selection then
-- Select the entry: send the action.
if type(action) == "string" then PrimeUI.resolve("selectionBox", action, entries()[selection])
else action(entries()[selection]) end
else
selection = sel
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
end
end
elseif event == "mouse_scroll" then
-- Handle mouse scrolling.
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
if key < 0 and selection > 1 then
-- Move selection up.
selection = selection - 1
if selection < scroll then scroll = scroll - 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
elseif key > 0 and selection < #entries() then
-- Move selection down.
selection = selection + 1
if selection > scroll + height - 1 then scroll = scroll + 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then selectChangeAction(selection) end
-- Redraw screen.
drawEntries()
end
end
end
end end
end) elseif event == "mouse_click" and key == 1 then
return drawEntries -- Handle clicking the scroll arrows.
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
if cx == wx + width - 1 then
if cy == wy and selection > 1 then
-- Move selection up.
selection = selection - 1
if selection < scroll then scroll = scroll - 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then
PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then
selectChangeAction(selection)
end
-- Redraw screen.
drawEntries()
elseif cy == wy + height - 1 and selection < #entries() then
-- Move selection down.
selection = selection + 1
if selection > scroll + height - 1 then scroll = scroll + 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then
PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then
selectChangeAction(selection)
end
-- Redraw screen.
drawEntries()
end
elseif cx >= wx and cx < wx + width - 1 and cy >= wy and cy < wy + height then
local sel = scroll + (cy - wy)
if sel == selection then
-- Select the entry: send the action.
if type(action) == "string" then
PrimeUI.resolve("selectionBox", action, entries()[selection])
else
action(entries()[selection])
end
else
selection = sel
-- Send action if necessary.
if type(selectChangeAction) == "string" then
PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then
selectChangeAction(selection)
end
-- Redraw screen.
drawEntries()
end
end
elseif event == "mouse_scroll" then
-- Handle mouse scrolling.
local wx, wy = PrimeUI.getWindowPos(entrywin, 1, 1)
if cx >= wx and cx < wx + width and cy >= wy and cy < wy + height then
if key < 0 and selection > 1 then
-- Move selection up.
selection = selection - 1
if selection < scroll then scroll = scroll - 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then
PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then
selectChangeAction(selection)
end
-- Redraw screen.
drawEntries()
elseif key > 0 and selection < #entries() then
-- Move selection down.
selection = selection + 1
if selection > scroll + height - 1 then scroll = scroll + 1 end
-- Send action if necessary.
if type(selectChangeAction) == "string" then
PrimeUI.resolve("selectionBox", selectChangeAction, selection)
elseif selectChangeAction then
selectChangeAction(selection)
end
-- Redraw screen.
drawEntries()
end
end
end
end
end)
return drawEntries
end end
return { return {
PrimeUI = PrimeUI PrimeUI = PrimeUI
} }

View file

@ -1,4 +1,3 @@
---@class InnerRemote ---@class InnerRemote
---@field port number ---@field port number
---@field password string ---@field password string
@ -8,11 +7,15 @@
---@field ender_storage string ---@field ender_storage string
---@field modem string ---@field modem string
---@field remotes table<string, InnerRemote> ---@field remotes table<string, InnerRemote>
---
---@class Chatbox
---@field players table<string, string>
---@field prefix string|nil
---@class Config ---@class Config
---@field inventories string[] ---@field inventories string[]
---@field import string[]|nil ---@field import string[]|nil
---@field chatbox table<string, string>|nil ---@field chatbox Chatbox|nil
---@field remote Remote ---@field remote Remote
local inv = require("modules.inv") local inv = require("modules.inv")
@ -23,32 +26,32 @@ local ra = require("modules.ra")
local config = require("../config") ---@type Config local config = require("../config") ---@type Config
local function importMechanism() local function importMechanism()
if config.import == nil then if config.import == nil then
return return
end end
if #config.import == 0 then if #config.import == 0 then
return return
end end
while true do while true do
for _, import_from_inv in ipairs(config.import) do for _, import_from_inv in ipairs(config.import) do
---@type ccTweaked.peripheral.Inventory ---@type ccTweaked.peripheral.Inventory
local perip = peripheral.wrap(import_from_inv) local perip = peripheral.wrap(import_from_inv)
if perip and perip.size then if perip and perip.size then
local slotsToSend = {} local slotsToSend = {}
for slot = 1, perip.size() do for slot = 1, perip.size() do
local item = perip.getItemDetail(slot) local item = perip.getItemDetail(slot)
if item then if item then
table.insert(slotsToSend, slot) table.insert(slotsToSend, slot)
end end
end
if #slotsToSend > 0 then
inv.sendItemAwayMultiple(slotsToSend, perip, import_from_inv)
end
end
end end
sleep(0.1) if #slotsToSend > 0 then
inv.sendItemAwayMultiple(slotsToSend, perip, import_from_inv)
end
end
end end
sleep(0.1)
end
end end
inv.sync() inv.sync()

View file

@ -2,91 +2,90 @@ local config = require("../../config") ---@type Config
local inv = require("modules.inv") local inv = require("modules.inv")
local function levDist(s, t) local function levDist(s, t)
local n = #s local n = #s
local m = #t local m = #t
if n == 0 then return m end if n == 0 then return m end
if m == 0 then return n end if m == 0 then return n end
-- create matrix -- create matrix
local d = {} local d = {}
for i = 0, n do for i = 0, n do
d[i] = {} d[i] = {}
end
-- initialize
for i = 0, n do d[i][0] = i end
for j = 0, m do d[0][j] = j end
-- main loop
for i = 1, n do
local s_i = s:sub(i, i)
for j = 1, m do
-- safeguard shortcut
if i == j and d[i][j] and d[i][j] > 4 then
return n
end
local t_j = t:sub(j, j)
local cost = (s_i == t_j) and 0 or 1
local mi = math.min(
d[i - 1][j] + 1,
d[i][j - 1] + 1,
d[i - 1][j - 1] + cost
)
d[i][j] = mi
-- Damerau transposition
if i > 1 and j > 1 and s_i == t:sub(j - 1, j - 1) and s:sub(i - 1, i - 1) == t_j then
d[i][j] = math.min(d[i][j], d[i - 2][j - 2] + cost)
end
end end
end
-- initialize return d[n][m]
for i = 0, n do d[i][0] = i end
for j = 0, m do d[0][j] = j end
-- main loop
for i = 1, n do
local s_i = s:sub(i, i)
for j = 1, m do
-- safeguard shortcut
if i == j and d[i][j] and d[i][j] > 4 then
return n
end
local t_j = t:sub(j, j)
local cost = (s_i == t_j) and 0 or 1
local mi = math.min(
d[i - 1][j] + 1,
d[i][j - 1] + 1,
d[i - 1][j - 1] + cost
)
d[i][j] = mi
-- Damerau transposition
if i > 1 and j > 1 and s_i == t:sub(j - 1, j - 1) and s:sub(i - 1, i - 1) == t_j then
d[i][j] = math.min(d[i][j], d[i - 2][j - 2] + cost)
end
end
end
return d[n][m]
end end
local function findBest(data, item) local function findBest(data, item)
local sorted_data = {}
local sorted_data = {} for _, z in ipairs(data) do
local parts = {}
for _, z in ipairs(data) do for part in string.gmatch(z, "([^:]+)") do
local parts = {} table.insert(parts, part)
for part in string.gmatch(z, "([^:]+)") do
table.insert(parts, part)
end
local key = parts[1] .. ":" .. (parts[2] or "")
local dist = levDist(item, parts[2] or "")
table.insert(sorted_data, { key, dist })
end end
table.sort(sorted_data, function(a, b) local key = parts[1] .. ":" .. (parts[2] or "")
return a[2] < b[2] local dist = levDist(item, parts[2] or "")
end) table.insert(sorted_data, { key, dist })
end
local best = {} table.sort(sorted_data, function(a, b)
local best_dist = sorted_data[1][2] return a[2] < b[2]
for _, z in ipairs(sorted_data) do end)
if z[2] == best_dist then
table.insert(best, z) local best = {}
else local best_dist = sorted_data[1][2]
break for _, z in ipairs(sorted_data) do
end if z[2] == best_dist then
table.insert(best, z)
else
break
end end
end
return best return best
end end
local BOT_NAME = "&cS &eI&an&3c &5S&cI&6S" local BOT_NAME = "&cS &eI&an&3c &5S&cI&6S"
function auth(user) function auth(user)
local manip = config.chatbox[user] local manip = config.chatbox.players[user]
if manip then if manip then
return true return true
else else
chatbox.tell(user, "You are not authorized to use this command.", BOT_NAME) chatbox.tell(user, "You are not authorized to use this command.", BOT_NAME)
return false return false
@ -99,11 +98,11 @@ function run()
while true do while true do
local _, user, command, args = os.pullEvent("command") local _, user, command, args = os.pullEvent("command")
if command == "sis" then if command == (config.chatbox.prefix or "sis") then
if args[1] == "whoami" then if args[1] == "whoami" then
local manip = config.chatbox[user] local manip = config.chatbox.players[user]
if manip then if manip then
chatbox.tell(user, "You are " .. user .. ", linked with `" .. manip .."`.", BOT_NAME) chatbox.tell(user, "You are " .. user .. ", linked with `" .. manip .. "`.", BOT_NAME)
else else
chatbox.tell(user, "You are not registered. Ask the creator of this SIS to be registered.", BOT_NAME) chatbox.tell(user, "You are not registered. Ask the creator of this SIS to be registered.", BOT_NAME)
end end
@ -116,7 +115,7 @@ function run()
end end
local perip = peripheral.wrap(config.chatbox[user]) local perip = peripheral.wrap(config.chatbox.players[user])
---@type ccTweaked.peripheral.Inventory ---@type ccTweaked.peripheral.Inventory
local peripInventory = perip.getInventory() local peripInventory = perip.getInventory()
@ -126,11 +125,11 @@ function run()
local seen = {} local seen = {}
for _, stack in pairs(invList) do for _, stack in pairs(invList) do
local name = stack and stack.name local name = stack and stack.name
if name and not seen[name] then if name and not seen[name] then
seen[name] = true seen[name] = true
allItems[#allItems + 1] = name allItems[#allItems + 1] = name
end end
end end
local best = findBest(allItems, args[2]) local best = findBest(allItems, args[2])
@ -138,27 +137,27 @@ function run()
local itemName = "" local itemName = ""
if #best > 1 then if #best > 1 then
itemName = best[1][1] itemName = best[1][1]
if string.find(":", args[2]) then if string.find(":", args[2]) then
itemName = args[2] itemName = args[2]
end end
chatbox.tell(user, "`WARNING`: Item most likely inaccurate. Assuming " .. itemName, BOT_NAME) chatbox.tell(user, "`WARNING`: Item most likely inaccurate. Assuming " .. itemName, BOT_NAME)
else else
itemName = best[1][1] itemName = best[1][1]
end end
local slots = {} local slots = {}
for slot, item in pairs(peripInventory.list()) do for slot, item in pairs(peripInventory.list()) do
if item.name == itemName then if item.name == itemName then
table.insert(slots, slot) table.insert(slots, slot)
end end
end end
local amount = nil local amount = nil
if args[3] then if args[3] then
amount = tonumber(args[3], 10) amount = tonumber(args[3], 10)
end end
local moved = inv.sendItemAwayMultiple(slots, peripInventory, peripInventory, amount) local moved = inv.sendItemAwayMultiple(slots, peripInventory, peripInventory, amount)
@ -168,11 +167,11 @@ function run()
if not auth(user) then goto continue end if not auth(user) then goto continue end
if not args[2] then if not args[2] then
chatbox.tell(user, "Supply a item (minecraft:`diamond` part) to withdraw it!", BOT_NAME) chatbox.tell(user, "Supply a item (minecraft:`diamond` part) to withdraw it!", BOT_NAME)
goto continue goto continue
end end
local perip = peripheral.wrap(config.chatbox[user]) local perip = peripheral.wrap(config.chatbox.players[user])
---@type ccTweaked.peripheral.Inventory ---@type ccTweaked.peripheral.Inventory
local peripInventory = perip.getInventory() local peripInventory = perip.getInventory()
@ -182,19 +181,19 @@ function run()
local itemName = "" local itemName = ""
if #best > 1 then if #best > 1 then
itemName = best[1][1] itemName = best[1][1]
if string.find(":", args[2]) then if string.find(":", args[2]) then
itemName = args[2] itemName = args[2]
end end
chatbox.tell(user, "`WARNING`: Item most likely inaccurate. Assuming " .. itemName, BOT_NAME) chatbox.tell(user, "`WARNING`: Item most likely inaccurate. Assuming " .. itemName, BOT_NAME)
else else
itemName = best[1][1] itemName = best[1][1]
end end
local amount = nil local amount = nil
if args[3] then if args[3] then
amount = tonumber(args[3], 10) amount = tonumber(args[3], 10)
end end
local moved = inv.sendItemToSelf(itemName, peripInventory, amount) local moved = inv.sendItemToSelf(itemName, peripInventory, amount)

View file

@ -89,7 +89,6 @@ local function sendItemToSelf(itemName, perip, maxAmount)
end end
end end
end end
-- sleep(0.1)
turtleMoveAllowed = true turtleMoveAllowed = true
return total return total

View file

@ -30,15 +30,13 @@ local function run()
for color, remote in pairs(config.remote.remotes) do for color, remote in pairs(config.remote.remotes) do
if remote.port == channel then if remote.port == channel then
local err, data = pcall(function()
local err, data = pcall(function ()
return hmac.decrypt_with_password(remote.password, message) return hmac.decrypt_with_password(remote.password, message)
end) end)
if not data then return end if not data then return end
if err == true then if err == true then
---@type RemoteAccess ---@type RemoteAccess
local json, errj = textutils.unserialiseJSON(data) local json, errj = textutils.unserialiseJSON(data)
if json or errj ~= nil then if json or errj ~= nil then
@ -57,12 +55,12 @@ local function run()
elseif json.type == "deposit" then elseif json.type == "deposit" then
if json.data.itemName then if json.data.itemName then
local move = {} local move = {}
for s, i in pairs(ender_storage.list()) do for s, i in pairs(ender_storage.list()) do
if i.name == json.data.itemName then if i.name == json.data.itemName then
table.insert(move, s) table.insert(move, s)
end end
end end
inv.sendItemAwayMultiple(move, ender_storage, config.remote.ender_storage, json.data.amount) inv.sendItemAwayMultiple(move, ender_storage, config.remote.ender_storage, json.data.amount)
elseif json.data.slots then elseif json.data.slots then
if type(json.data.slots) ~= 'table' then if type(json.data.slots) ~= 'table' then
return return

View file

@ -8,71 +8,72 @@ local function is_beta(ver)
end end
local function run() local function run()
local search = "" local search = ""
local win = term.current(); local win = term.current();
local w , h= term.getSize() local w, h = term.getSize()
local function getFiltered() local function getFiltered()
local filtered = {} local filtered = {}
for name, count in pairs(inv.getAIL().listItemAmounts()) do for name, count in pairs(inv.getAIL().listItemAmounts()) do
if search == "" or string.find(name:lower(), search:lower(), 1, true) then if search == "" or string.find(name:lower(), search:lower(), 1, true) then
table.insert(filtered, { name = name, count = count }) table.insert(filtered, { name = name, count = count })
end end
end
table.sort(filtered, function(a, b) return a.count > b.count end)
local refiltered = {}
for i, filter in ipairs(filtered) do
refiltered[i] = filter.name .. string.rep(" ", w-(2+#filter.name+#tostring(filter.count))) .. tostring(filter.count)
end
return refiltered
end end
table.sort(filtered, function(a, b) return a.count > b.count end)
local com = getFiltered() local refiltered = {}
PrimeUI.clear() for i, filter in ipairs(filtered) do
local updateEntries = PrimeUI.selectionBox( refiltered[i] = filter.name ..
win, 1, 4, w, h-7, string.rep(" ", w - (2 + #filter.name + #tostring(filter.count))) .. tostring(filter.count)
function () end
return com return refiltered
end, end
function(option)
local z = option:match("^(%S+)")
if inv.getAIL().getItem(z) then local com = getFiltered()
inv.sendItemToSelf(z)
end
end
)
PrimeUI.inputBox(win, 2, 2, w-2, function (data) PrimeUI.clear()
search = data local updateEntries = PrimeUI.selectionBox(
win, 1, 4, w, h - 7,
function()
return com
end,
function(option)
local z = option:match("^(%S+)")
if inv.getAIL().getItem(z) then
inv.sendItemToSelf(z)
end
end
)
PrimeUI.inputBox(win, 2, 2, w - 2, function(data)
search = data
com = getFiltered()
updateEntries()
end, "Input a item to search..")
local ver = fs.open("storage-solution/version", "r").readAll()
PrimeUI.label(win, 2, h - 1, is_beta(ver) and "you're running beta :3" or "welcome to SiSS", colors.lightGray)
PrimeUI.label(win, 2, h - 2, "rev. " .. ver, colors.lightGray)
local timer = os.startTimer(1)
PrimeUI.addTask(function()
while true do
local _, osTimer = os.pullEvent("timer")
if osTimer == timer then
com = getFiltered() com = getFiltered()
updateEntries() updateEntries()
end, "Input a item to search..") timer = os.startTimer(1)
end
end
end)
local ver = fs.open("storage-solution/version", "r").readAll() PrimeUI.run()
PrimeUI.label(win, 2, h-1, is_beta(ver) and "you're running beta :3" or "welcome to SiSS", colors.lightGray)
PrimeUI.label(win, 2, h-2, "rev. " .. ver, colors.lightGray)
local timer = os.startTimer(1)
PrimeUI.addTask(function()
while true do
local _, osTimer = os.pullEvent("timer")
if osTimer == timer then
com = getFiltered()
updateEntries()
timer = os.startTimer(1)
end
end
end)
PrimeUI.run()
end end
return { return {
run = run run = run
} }