Merge branch 'unfinished-ag2' into Pre-Alpha-3.0.0

This commit is contained in:
2026-06-18 16:38:51 +03:00
90 changed files with 2913 additions and 922 deletions
+23 -20
View File
@@ -32,6 +32,11 @@ function filesystem.canonical(path)
return "/" .. table.concat(segList, "/")
end
function filesystem.basename(path)
checkArg(1, path, "string")
return path:match("/([^/]+)/?$") or ""
end
function filesystem.concat(path1, path2)
checkArg(1, path1, "string")
checkArg(2, path2, "string")
@@ -67,6 +72,13 @@ function filesystem.absolutePath(path) -- returns the address and absolute path
return address, path
end
function filesystem.parent(path)
checkArg(1, path, "string")
local p = filesystem.canonical(path)
-- return "/" on "/"
return p == "/" and "/" or (p:match("^(.*)/[^/]+/?$") or "/")
end
function filesystem.exists(path) -- check if path exists
checkArg(1, path, "string")
local address, absPath = filesystem.absolutePath(path)
@@ -350,7 +362,7 @@ function filesystem.open(path, mode, buffered) -- opens a file and returns its h
end
end
function properHandle.seek(self, whence, offset)
checkArg(2, whence, "string", "number")
checkArg(2, whence, "string", "number", "nil")
checkArg(3, offset, "number", "nil")
if not offset then
offset = 0
@@ -475,14 +487,12 @@ local function copyContent(fromHandle, toHandle)
if not (fromHandle and toHandle) then
return
end
local memory = math.floor(computer.freeMemory() * 0.8)
local tmpdata
while true do
tmpdata = fromHandle:read(memory)
local tmpdata = fromHandle.read(2048)
if not tmpdata then
break
end
local status, reason = toHandle:write(tmpdata)
local status, reason = toHandle.write(tmpdata)
if status ~= true then
break
end
@@ -492,7 +502,6 @@ local function copyContent(fromHandle, toHandle)
end
local function copyRecursive(fromAddress, fromAbsPath, toAddress, toAbsPath)
-- TODO: make this use copyContent
if fromAbsPath:sub(-1) == "/" then
fromAbsPath = fromAbsPath:sub(1, -2)
end
@@ -506,31 +515,21 @@ local function copyRecursive(fromAddress, fromAbsPath, toAddress, toAbsPath)
end
for i = 1, #fileList do
local fromFile, toFile = fromAbsPath .. "/" .. fileList[i], toAbsPath .. "/" .. fileList[i]
--[[ local handle = component.invoke(fromAddress, "open", fromFile, "r")
local data, tmpdata = "", nil
repeat
tmpdata = component.invoke(fromAddress, "read", handle, math.huge or math.maxinteger)
data = data .. (tmpdata or "")
until not tmpdata
tmpdata = component.invoke(fromAddress, "close", handle)
local handle = component.invoke(toAddress, "open", toFile, "w")
component.invoke(toAddress, "write", handle, data)
component.invoke(toAddress, "close", handle) ]]
local fromHandle = component.invoke(fromAddress, "open", fromFile, "r")
local toHandle = component.invoke(toAddress, "open", toFile, "w")
copyContent({
["read"] = function(...)
return component.invoke(fromAddress, "read", handle, ...)
return component.invoke(fromAddress, "read", fromHandle, ...)
end,
["close"] = function(...)
return component.invoke(fromAddress, "close", handle, ...)
return component.invoke(fromAddress, "close", fromHandle, ...)
end,
}, {
["write"] = function(...)
return component.invoke(fromAddress, "write", handle, ...)
return component.invoke(toAddress, "write", toHandle, ...)
end,
["close"] = function(...)
return component.invoke(fromAddress, "close", handle, ...)
return component.invoke(toAddress, "close", toHandle, ...)
end,
})
end
@@ -545,6 +544,10 @@ function filesystem.isDirectory(path)
return component.invoke(address, "isDirectory", absPath)
end
function filesystem.isFile(path)
return not filesystem.isDirectory(path) and filesystem.exists(path)
end
function filesystem.rename(fromPath, toPath)
checkArg(1, fromPath, "string")
checkArg(2, toPath, "string")
+4 -7
View File
@@ -55,14 +55,11 @@ local logFileSizeLimit = 16384
local function writeToLog(path, text)
fs.makeDirectory("halyde/logs") -- Git likes to not clone empty directories
local handle
if fs.exists(path) then
handle = assert(fs.open(path, "a"))
else
handle = assert(fs.open(path, "w"))
local handle = fs.open(path, "a")
if handle then
handle:write(text .. "\n")
handle:close()
end
handle:write(text .. "\n")
handle:close()
-- Log trimming if it gets too long
if fs.size(path) > logFileSizeLimit then
+43 -122
View File
@@ -1,127 +1,48 @@
local serialize = {}
function serialize.string(str)
return '"'..str:gsub("[%z\1-\31\34\92\127-\159]",function(c)
local byte = c:byte()
if byte== 7 then return "\\a" end
if byte== 8 then return "\\b" end
if byte== 9 then return "\\t" end
if byte==10 then return "\\n" end
if byte==11 then return "\\v" end
if byte==12 then return "\\f" end
if byte==13 then return "\\r" end
if byte==34 then return "\\\"" end
if byte==92 then return "\\\\" end
return string.format("\\x%02x",byte)
end)..'"'
local function _serialize(value, indent, level, visited)
local currentIndent = indent and string.rep(indent, level) or ""
local nextIndent = indent and string.rep(indent, level + 1) or ""
local sep = indent and "\n" or " "
local t = type(value)
if t == "nil" then return "nil" end
if t == "string" then return string.format("%q", value) end
if t == "number" then
if value ~= value then return "0/0" end
if value == math.huge then return "math.huge" end
if value == -math.huge then return "-math.huge" end
return tostring(value)
end
if t == "boolean" then return tostring(value) end
if t == "table" then
if visited[value] then return "..." end
visited[value] = true
local items = {}
local arrayCount = 0
for i = 1, #value do
if value[i] ~= nil then arrayCount = i else break end
end
for i = 1, arrayCount do
table.insert(items, nextIndent .. _serialize(value[i], indent, level + 1, visited))
end
for k, v in pairs(value) do
if type(k) ~= "number" or k < 1 or k > arrayCount then
local keyStr
if type(k) == "string" and k:match("^[%a_][%w_]*$") then
keyStr = k
else
keyStr = "[" .. _serialize(k, indent, level + 1, visited) .. "]"
end
table.insert(items, nextIndent .. keyStr .. " = " .. _serialize(v, indent, level + 1, visited))
end
end
visited[value] = nil
if #items == 0 then return "{}" end
return "{" .. sep .. table.concat(items, "," .. sep) .. sep .. currentIndent .. "}"
end
return tostring(value)
end
function serialize.table(tbl,colors,stack)
stack = table.copy(stack or {})
table.insert(stack,tbl)
local keyAmount = 0
local keyNumber = true
local out = ""
local first = true
for key,val in pairs(tbl) do
if not first then out=out..",\n" end
first=false
out=out.." "
if type(key)=="string" then
if key:match("^[%a_][%w_]*$") then
out=out..key.."="
else
out=out..'['..serialize.string(key)..']='
end
else
out=out.."["..tostring(key).."]="
end
if type(key)~="number" then
keyNumber=false
end
local success,reason = pcall(function()
local valStr = ""
if type(val)=="table" then
if #stack>4 or table.find(stack,val) then
valStr="..."
else
valStr=serialize.table(val,colors,stack)
end
elseif type(val)=="string" then
local lines = {}
for line in (val.."\n"):gmatch("([^\n]*)\n") do table.insert(lines,line) end
if #lines[#lines]==0 then
lines[#lines]=nil
lines[#lines]=lines[#lines].."\n"
end
for i=1,#lines do
if i<#lines then
lines[i]=serialize.string(lines[i].."\n")
else
lines[i]=serialize.string(lines[i])
end
end
valStr=table.concat(lines," ..\n ")
else
valStr=tostring(val)
end
local lines = {}
for line in (valStr.."\n"):gmatch("([^\n]*)\n") do table.insert(lines,line) end
out=out..table.concat(lines,"\n ")
lines = nil
keyAmount=keyAmount+1
end)
if not success then
if colors then out=out.."\x1b[91m" end
out=out.."["..tostring(reason).."]"
if colors then out=out.."\x1b[39m" end
end
coroutine.yield()
end
local metatbl = getmetatable(tbl)
local metakeys = {}
local metastring = ""
if type(metatbl)=="table" then
for i,v in pairs(metatbl) do
keyNumber=false
table.insert(metakeys,i)
end
end
if #metakeys>0 then
out=out.."\n "
if colors then metastring=metastring.."\x1b[92m" end
if table.find(metakeys,"__tostring") then
metastring=metastring.."tostring: "..serialize.string(tostring(tbl)).."\n "
table.remove(metakeys,table.find(metakeys,"__tostring"))
end
metastring=metastring..table.concat(metakeys,", ")
if colors then metastring=metastring.."\x1b[39m" end
out=out..metastring
end
if keyAmount==0 then return "{"..metastring.."}" end
if keyNumber then
-- fix strings not being serialised
local vals = {}
for _,v in pairs(tbl) do
if #vals>=5 and #stack>1 then
table.insert(vals,"...")
break
end
if type(v)=="table" then
table.insert(vals,serialize.table(v,colors,stack))
elseif type(v)=="string" then
table.insert(vals,serialize.string(v))
else
table.insert(vals,tostring(v))
end
end
return "{"..table.concat(vals,", ")..(#metakeys>0 and "\n "..metastring or "").."}"
end
return "{\n"..out.."\n}"
function serialize(value, indent)
return _serialize(value, indent, 0, {})
end
return serialize
+501
View File
@@ -0,0 +1,501 @@
local defaultDBPath = "/ag2/testdb.json"
local computer = require("computer")
local fs = require("filesystem")
-- local db = require("solvitdb")
local db = {}
local json = require("json")
function db.create(dbpath)
local handle = fs.open(dbpath,"w")
handle:write("{}")
handle:close()
end
function db.readJSON(dbpath)
local handle = fs.open(dbpath,"r")
local content = ""
while true do
local s = handle:read(math.huge or math.maxinteger)
if not s then break end
content=content..s
end
handle:close()
return content
end
function db.get(dbpath,pack)
local dbc = json.decode(db.readJSON(dbpath))
return dbc[pack]
end
function db.set(dbpath,pack,info)
local dbc = json.decode(db.readJSON(dbpath))
dbc[pack]=info
local handle = fs.open(dbpath,"w")
handle:write(json.encode(dbc))
handle:close()
end
function db.remove(dbpath,pack)
local dbc = json.decode(db.readJSON(dbpath))
dbc[pack]=nil
local handle = fs.open(dbpath,"w")
handle:write(json.encode(dbc))
handle:close()
end
function db.list(dbpath,pack)
local dbc = json.decode(db.readJSON(dbpath))
local keys = {}
for i,_ in pairs(dbc) do
table.insert(keys,i)
end
return ipairs(keys)
end
local avs = {}
function avs.splitSingular(s)
local result = {}
for str in string.gmatch(s, "([^.]+)") do
table.insert(result,tonumber(str) or -1)
end
return result
end
function avs.parse(pack)
if not string.find(pack,"=") then
return {pack}
end
local idx=pack:find("=")
local name=pack:sub(1,idx-1)
local verstr=pack:sub(idx+1)
if string.find(verstr,"-") then
idx=verstr:find("-")
verstr={verstr:sub(1,idx-1),verstr:sub(idx+1)}
else
verstr={verstr}
end
for i=1,#verstr do
verstr[i]=avs.splitSingular(verstr[i])
end
if #verstr>1 then
for i=1,3 do
verstr[1][i]=math.max(verstr[1][i],0)
end
end
return {name,verstr}
end
function avs.serializeSingle(ver)
local ver2 = table.copy(ver)
for i=1,3 do
if ver2[i]==-1 then
ver2[i]="*"
else
ver2[i]=tostring(ver2[i])
end
end
return ver2[1].."."..ver2[2].."."..ver2[3]
end
function avs.serializeVersion(ver)
local singles = {}
for i=1,#ver do
table.insert(singles,avs.serializeSingle(ver[i]))
end
if singles[1]==singles[2] then
singles={singles[1]}
end
local out=""
for i=1,#singles do
out=out..singles[i]
if i~=#singles then
out=out.."-"
end
end
return out
end
function avs.serializePack(pack)
if #pack==1 then
return pack[1]
end
return pack[1].."="..avs.serializeVersion(pack[2])
end
function avs.singleGreater(ver1,ver2)
for i=1,3 do
if ver1[i]~=ver2[i] then
if ver1[i]==-1 or ver1[i]>ver2[i] then
return true
end
if ver2[i]==-1 or ver2[i]>ver1[i] then
return false
end
end
end
return false
end
function avs.singleLesser(ver1,ver2)
return avs.singleGreater(ver2,ver1)
end
function avs.singleMin(ver1,ver2)
return avs.singleLesser(ver1,ver2) and ver1 or ver2
end
function avs.singleMax(ver1,ver2)
return avs.singleGreater(ver1,ver2) and ver1 or ver2
end
function avs.compatibleRange(vers)
for i=1,#vers do
if type(vers[i])=="string" then vers[i]=avs.parse(vers[i])[2] end
if #vers[i]==1 then vers[i]={vers[i][1],vers[i][1]} end
end
local range = vers[1]
for i=2,#vers do
range[1]=avs.singleMax(range[1],vers[i][1])
range[2]=avs.singleMin(range[2],vers[i][2])
end
if avs.singleGreater(range[1],range[2]) then
return nil
end
return range
end
function avs.matching(pack1,pack2)
if pack1[1]~=pack2[1] then return false end
local ver1 = pack1[2] or {{-1,-1,-1}}
local ver2 = pack2[2] or {{-1,-1,-1}}
if #ver1==1 then ver1={ver1[1],ver1[1]} end
if #ver2==1 then ver2={ver2[1],ver2[1]} end
return avs.compatibleRange({ver1,ver2})~=nil
end
local function packageInArray(pack,arr)
for i=1,#arr do
if avs.matching(avs.parse(arr[i]),pack) then
return true,arr[i]
end
end
return false
end
local function packageNameInArray(pack,arr)
for i=1,#arr do
if arr[i][1]==pack[1] then
return true,arr[i]
end
end
return false
end
local function removeFromArray(el,arr)
for i=1,#arr do
if arr[i]==el then
table.remove(arr,i)
return i
end
end
end
local function startTransaction(dbpath)
dbpath = dbpath or defaultDBPath
if not fs.exists(dbpath) then
db.create(dbpath)
end
local yieldClock = computer.uptime()
local function yieldIfNecessary()
if computer.uptime()-yieldClock>=0.1 then
coroutine.yield()
yieldClock = computer.uptime()
end
end
local installIncomplete = false
local removeIncomplete = false
local packInfo = {}
local ins = {}
local rem = {}
local transaction = {}
function transaction.install(name)
table.insert(ins,avs.parse(name))
installIncomplete = true
end
function transaction.remove(name)
table.insert(rem,avs.parse(name))
removeIncomplete = true
end
function transaction.autoRemove()
end
function transaction.update(name)
end
function transaction.updateAll(name)
end
function transaction.addInfo(name,info)
if not info.type then info.type="package" end
packInfo[name]=info
-- print(require("serialize")(packInfo))
end
local function getPackInfo(pack)
return packInfo[avs.serializePack(pack)]
end
local function findReverseDependencies(pack)
local out={}
for i,info in pairs(packInfo) do
if info.dependencies and packageInArray(pack,info.dependencies) then
table.insert(out,avs.parse(i))
end
end
return out
end
local function finalizeInstall(settings)
-- find missing package information
local missing = {}
for i=1,#ins do
if getPackInfo(ins[i])==nil then
table.insert(missing,avs.serializePack(ins[i]))
end
end
if #missing>0 then
return false,missing
end
-- find dependencies
installIncomplete=false
local i=1
while i<=#ins do
local deps = getPackInfo(ins[i]).dependencies
if deps and #deps>=1 then
for j=1,#deps do
local dep = avs.parse(deps[j])
local inArr,arrPack = packageNameInArray(dep,ins)
if inArr then
if not avs.matching(dep,arrPack) then
local msg = ""
if settings.resolveConflict then
msg="Cannot resolve conflict: "..msg
end
local rev = findReverseDependencies(arrPack)
if #rev==0 then
msg=msg.."Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but one or more packages depend on "..avs.serializePack(arrPack)
return false, msg
elseif #rev==1 then
msg=msg.."Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but package "..avs.serializePack(rev[1]).." depends on "..avs.serializePack(arrPack)
return false, msg
else
msg=msg.."Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but packages "
for i=1,#rev do
if i>1 and i~=#rev then
msg=msg..", "
elseif i==#rev then
msg=msg.." and "
end
msg=msg..avs.serializePack(rev[i])
end
msg=msg.." depend on "..avs.serializePack(arrPack)
return false, msg
end
end
elseif type(db.get(dbpath,dep[1]))=="nil" then
installIncomplete=true
table.insert(ins,j,dep)
else
local dbinfo = db.get(dbpath,dep[1])
local dbpack = {dep[1]}
if dbinfo.version then
dbpack = avs.parse(dep[1].."="..dbinfo.version)
end
if not avs.matching(dep,dbpack) then
if settings.resolveConflict then
removeIncomplete=true
table.insert(rem,1,dbpack)
installIncomplete=true
table.insert(ins,j,dep)
else
local msg = "Package "..avs.serializePack(ins[i]).." depends on "..avs.serializePack(dep)..", but another version ("..avs.serializePack(dbpack)..") is installed"
return false, msg
end
end
end
end
i=i+#deps
end
i=i+1
end
end
local function finalizeRemove(settings)
removeIncomplete=false
-- filter to only have packages in the database
local i=1
while i<=#rem do
local dat = db.get(dbpath,rem[i][1])
if not dat then
table.remove(rem,i)
else
packInfo[avs.serializePack(rem[i])]=dat
i=i+1
end
end
-- get package info from database
for i=1,#rem do
if not getPackInfo(rem[i]) then
packInfo[avs.serializePack(rem[i])]=db.get(rem[i][1])
end
end
-- find if the main package is reverse dependant to another
i=1
while i<=#rem do
local dat = getPackInfo(rem[i])
if dat.reverseDependencies and #dat.reverseDependencies>0 then
for _,dep in ipairs(dat.reverseDependencies) do
if not packageNameInArray({dep},rem) then
if settings.cascade then
table.insert(rem,1,{dep})
i=i+1
else
return false, "Package "..rem[i][1].." is a dependency of "..dep
end
end
end
end
i=i+1
end
-- look for dependencies if settings.autoremove is on
if settings.autoremove then
i=1
while i<=#rem do
local deps = getPackInfo(rem[i]).dependencies
if deps and #deps>=1 then
for j=1,#deps do
local dep = avs.parse(deps[j])
local depdat = packInfo[deps[j]]
if not depdat then
depdat = db.get(dbpath,dep[1])
packInfo[deps[j]]=depdat
end
if (not packageNameInArray(dep,rem)) and type(db.get(dbpath,dep[1]))~="nil" and (#depdat.reverseDependencies==1 and depdat.reverseDependencies[1]==rem[i][1]) then
removeIncomplete=true
table.insert(rem,j,dep)
end
end
i=i+#deps
end
i=i+1
end
end
end
function transaction.finalize(settings)
settings = settings or {}
while installIncomplete or removeIncomplete do
while installIncomplete do
local out = {finalizeInstall(settings)}
if out[1]==false then return table.unpack(out) end
yieldIfNecessary()
end
while removeIncomplete do
local out = {finalizeRemove(settings)}
if out[1]==false then return table.unpack(out) end
yieldIfNecessary()
end
end
local install = {}
local remove = {}
for i=1,#ins do
table.insert(install,avs.serializePack(ins[i]))
end
for i=1,#rem do
table.insert(remove,avs.serializePack(rem[i]))
end
return true, {install=install,remove=remove}
-- return "true, {["install"] = {"dep1", "package1", "package2"}, ["remove"] = {"package3"}}" on success
-- return "false, {"dep1"}" when not enough data
-- return "false, "[verbose string]"" when conflict found
-- TODO: handle same range AVS
-- TODO: handle different intercompatible 1.*.* range AVS
-- TODO: handle different incompatible 1.*.* range AVS
-- TODO: handle different intercompatible 1.*.*-2.*.* range AVS
-- TODO: handle different incompatible 1.*.*-2.*.* range AVS
-- TODO: handle conflicts from package info
-- TODO: handle reverse conflicts from another package's info
-- TODO: handle automatic conflict resolving
-- TODO: handle update of a single package with no dependencies
-- TODO: handle update of a single package with dependencies that don't need updating
-- TODO: handle update of a single package with dependencies that need updating
-- TODO: handle update of a single package that has a set dependency version changed
-- TODO: handle updating all packages in the database
-- TODO: handle installing optional packages
-- TODO: handle installing virtual packages and store this vpack info to database
-- TODO: handle removing virtual packages from database info
-- TODO: handle installing groups and store this group info to database
-- TODO: handle removing groups and store this group info to database
end
local function storeInstall()
-- directly set
for _,pack in ipairs(ins) do
if getPackInfo(pack) then
local info = table.copy(getPackInfo(pack))
if pack[2] then
info.version=avs.serializeVersion(pack[2])
else
info.version=info.latestVersion or info.version
end
db.set(dbpath,pack[1],info)
end
end
-- set reverse dependencies
for _,pack in pairs(ins) do
local i = avs.serializePack(pack)
local v = packInfo[i]
if v and v.dependencies then
for _,dep in ipairs(v.dependencies) do
local depname = avs.parse(dep)[1]
local dat = db.get(dbpath,depname)
if not dat then goto continue end
if type(dat.reverseDependencies)~="table" then
dat.reverseDependencies={}
end
table.insert(dat.reverseDependencies,pack[1])
db.set(dbpath,depname,dat)
::continue::
end
end
end
end
local function storeRemove()
-- directly remove
for _,pack in ipairs(rem) do
db.remove(dbpath,pack[1])
end
-- remove reverse dependencies
for _,pack in ipairs(rem) do
local pdat = getPackInfo(pack)
if not pdat.dependencies then goto continue end
for _,dep in ipairs(pdat.dependencies) do
local depname = avs.parse(dep)[1]
local dat = db.get(dbpath,depname)
if dat.reverseDependencies then
removeFromArray(pack[1],dat.reverseDependencies)
end
db.set(dbpath,depname,dat)
end
::continue::
end
end
function transaction.store()
if #ins>0 then
storeInstall()
end
if #rem>0 then
storeRemove()
end
end
return transaction
end
return { avs=avs,startTransaction=startTransaction }
+312
View File
@@ -0,0 +1,312 @@
local solvitdb = {}
local fs = require("filesystem")
local function checkValidityAndOpen(path)
local handle = assert(fs.open(path))
local data = assert(handle:read(8))
if data:sub(5, 8) == "RTFM" then
local patLength = string.unpack("<I4", data:sub(1, 4))
return handle, patLength
else
error("missing magic")
end
end
local function readPat(handle, patLength)
-- This needs the handle to be at byte 4
local data, tmpdata = ""
repeat
tmpdata = handle:read(patLength - #data)
data = data .. (tmpdata or "")
until not tmpdata
local packages = {}
for packageData in data:gmatch("(.-%.....);") do
packages[packageData:sub(1, -6)] = string.unpack("<I4", packageData:sub(-4, -1))
end
return packages
end
local function insert(filePath, location, bytes)
local chunkLength = 512
local readHandle = assert(fs.open(filePath, "r"))
local tmpFilePath = filePath .. ".tmp"
local writeHandle = assert(fs.open(tmpFilePath, "w"))
local i = 0
while true do
local readAmount = chunkLength
if readAmount > location - i then
readAmount = location - i
end
if readAmount == 0 then
break
end
local data = readHandle:read(readAmount)
i = i + readAmount
assert(writeHandle:write(data))
end
assert(writeHandle:write(bytes))
while true do
local data = readHandle:read(chunkLength)
if not data then
break
end
assert(writeHandle:write(data))
end
readHandle:close()
writeHandle:close()
fs.rename(tmpFilePath, filePath)
end
local function remove(filePath, location, length)
local chunkLength = 512
if length > 512 then
chunkLength = length
end
-- The file has to get shortened, so I have no choice but to do these shenanigans
local readHandle = assert(fs.open(filePath, "r"))
local tmpFilePath = filePath .. ".tmp"
local writeHandle = assert(fs.open(tmpFilePath, "w"))
local i = 0
while true do
local readAmount = chunkLength
if readAmount > location - i then
readAmount = location - i
end
if readAmount == 0 then
break
end
local data = readHandle:read(readAmount)
i = i + readAmount
assert(writeHandle:write(data))
end
readHandle:seek(length)
while true do
local data = readHandle:read(chunkLength)
if not data then
break
end
assert(writeHandle:write(data))
end
readHandle:close()
writeHandle:close()
fs.rename(tmpFilePath, filePath)
end
local function adjustPatLocationsAfterPackage(filePath, packageName, offset)
local readHandle, patLength = checkValidityAndOpen(filePath)
local pat = readPat(readHandle, patLength)
readHandle:close()
local modifiedLocation = pat[packageName]
local toAdjust = {}
for name, location in pairs(pat) do
if location > modifiedLocation then
table.insert(toAdjust, { name = name, location = location })
end
end
if #toAdjust == 0 then
return
end
local readHandle = assert(fs.open(filePath, "r"))
readHandle:seek(8) -- Skip header
local patBytes = assert(readHandle:read(patLength))
readHandle:close()
local patFieldPositions = {}
for _, entry in ipairs(toAdjust) do
local needle = entry.name .. "."
local startPos = patBytes:find(needle, 1, true)
patFieldPositions[entry.name] = 8 + startPos + #needle - 1
end
local writeHandle = assert(fs.open(filePath, "a"))
for _, entry in ipairs(toAdjust) do
local newLocation = entry.location + offset
writeHandle:seek("set", patFieldPositions[entry.name])
writeHandle:write(string.pack("<I4", newLocation))
end
writeHandle:close()
end
function solvitdb.create(path)
checkArg(1, path, "string")
local handle = assert(fs.open(path, "w"))
assert(handle:write("\0\0\0\0RTFM"))
handle:close()
end
function solvitdb.set(path, name, data)
checkArg(1, path, "string")
checkArg(2, name, "string")
checkArg(3, data, "table")
local function sanitize(tab)
for _, item in pairs(tab) do
if type(item) == "string" then
item = item:lower():gsub("[;|]", "-")
elseif type(item) == "table" then
sanitize(item)
end
end
end
sanitize(data)
local readHandle, patLength = checkValidityAndOpen(path)
local pat = readPat(readHandle, patLength)
local writeHandle = assert(fs.open(path, "a"))
local encodedString = ""
if data.type == "package" then
encodedString = "P"
elseif data.type == "group" then
encodedString = "G"
elseif data.type == "virtual-package" then
encodedString = "V"
end
if data.dependencies then
encodedString = encodedString .. "d" .. table.concat(data.dependencies, ";") .. "|"
end
if data.reverseDependencies then
encodedString = encodedString .. "D" .. table.concat(data.reverseDependencies, ";") .. "|"
end
if data.conflicts then
encodedString = encodedString .. "c" .. table.concat(data.conflicts, ";") .. "|"
end
if data.packages then
encodedString = encodedString .. "p" .. table.concat(data.packages, ";") .. "|"
end
if data.version then
encodedString = encodedString .. "v" .. data.version .. "|"
end
if pat[name] then
readHandle:seek(pat[name])
local oldData, tmpdata = ""
repeat
tmpdata = readHandle:read(math.huge)
oldData = oldData .. (tmpdata or "")
until oldData:find("\n", 1, true) or not tmpdata
readHandle:close()
if not oldData:find("\n", 1, true) and not tmpdata then
error("hit unexpected EOF")
end
oldData = oldData:match("^[^\n]+")
local difference = #encodedString - #oldData
writeHandle:seek("set", pat[name] + patLength + 8)
if difference == 0 then
readHandle:close()
writeHandle:write(encodedString)
writeHandle:close()
elseif difference < 0 then
writeHandle:write(encodedString)
local currentSeek = writeHandle:seek()
writeHandle:close()
remove(path, currentSeek, -difference)
adjustPatLocationsAfterPackage(path, name, difference)
elseif difference > 0 then
writeHandle:write(encodedString:sub(1, #encodedString - difference))
local currentSeek = writeHandle:seek()
writeHandle:close()
insert(path, currentSeek + 1, encodedString:sub(#data - difference, -1))
adjustPatLocationsAfterPackage(path, name, difference)
end
else
readHandle:close()
writeHandle:seek("end")
local newPackageLocation = writeHandle:seek() - patLength - 8
writeHandle:write(encodedString .. "\n")
writeHandle:close()
local patData = ("%s.%s;"):format(name, string.pack("<I4", newPackageLocation))
insert(path, patLength + 8, patData)
local newPatLength = patLength + #patData
local writeHandle = assert(fs.open(path, "a")) -- If this works, that means handles must be reopened after insert() or remove()
writeHandle:seek("set", 0)
assert(writeHandle:write(string.pack("<I4", newPatLength)))
writeHandle:close()
end
end
function solvitdb.get(path, name)
checkArg(1, path, "string")
checkArg(2, name, "string")
local handle, patLength = checkValidityAndOpen(path)
local pat = readPat(handle, patLength)
if not pat[name] then
return nil
end
handle:seek(pat[name])
local data, tmpdata = ""
repeat
tmpdata = handle:read(math.huge)
data = data .. (tmpdata or "")
until data:find("\n", 1, true) or not tmpdata
handle:close()
if not data:find("\n", 1, true) and not tmpdata then
error("hit unexpected EOF")
end
data = data:match("^[^\n]+")
local output = {}
if data:sub(1, 1) == "P" then
output.type = "package"
elseif data:sub(1, 1) == "G" then
output.type = "group"
elseif data:sub(1, 1) == "V" then
output.type = "virtual-package"
else
error("unknown package type")
end
data = "." .. data:sub(2, -1)
for series in data:gmatch("%.([dDcp][^.]*)") do
local seriesOutput
if series:sub(1, 1) == "d" then
output.dependencies = {}
seriesOutput = output.dependencies
elseif series:sub(1, 1) == "D" then
output.reverseDependencies = {}
seriesOutput = output.reverseDependencies
elseif series:sub(1, 1) == "c" then
output.conflicts = {}
seriesOutput = output.conflicts
elseif series:sub(1, 1) == "p" then
output.packages = {}
seriesOutput = output.packages
elseif series:sub(1, 1) == "v" then
output.version = series:sub(2, -1)
goto SkipSeries
end
-- Finally a case where Lua's weird table linking shenanigans are actually useful
series = series:sub(2, -1)
for seriesItem in series:gmatch("[^;]+") do
table.insert(seriesOutput, seriesItem)
end
::SkipSeries::
end
return output
end
function solvitdb.list(path)
checkArg(1, path, "string")
local handle, patLength = checkValidityAndOpen(path)
local pat = readPat(handle, patLength)
handle:close()
local list = {}
for index, _ in pairs(pat) do
table.insert(list, index)
end
setmetatable(list, {
__call = function(self)
i, value = next(self, i)
return i, value
end,
})
return list
end
function solvitdb.remove()
end
return solvitdb