mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 17:51:37 -04:00
235 lines
9.8 KiB
Lua
235 lines
9.8 KiB
Lua
local Serializer = require("src.core.SaveSerializer")
|
|
local Store = require("src.box.Store")
|
|
local GameVersion = require("src.core.GameVersion")
|
|
|
|
local Transaction = { PATH = "box/pending.lua" }
|
|
|
|
local function allowed(path)
|
|
if path == Store.PATH then return true end
|
|
if type(path) ~= "string" then return false end
|
|
local scope = path:match("^saves/([%w%._%-]+)/slot%d+%.lua$")
|
|
return scope ~= nil and (GameVersion.VERSIONS[scope] ~= nil
|
|
or (#scope <= 69 and scope:match("^cart_%w[%w%._%-]*$") ~= nil))
|
|
end
|
|
|
|
function Transaction.assetValid(path, body)
|
|
if type(path) ~= "string" or not path:match("^box/showcase/%d+%.png$")
|
|
or type(body) ~= "string" or #body < 24 or #body > 2 * 1024 * 1024
|
|
or body:sub(1, 8) ~= "\137PNG\r\n\26\n" then return false end
|
|
local function be(i)
|
|
local a, b, c, d = body:byte(i, i + 3)
|
|
return ((a * 256 + b) * 256 + c) * 256 + d
|
|
end
|
|
return be(17) == 1136 and be(21) == 432
|
|
end
|
|
|
|
local function write(fs, path, body)
|
|
local dir = path:match("^(.*)/[^/]+$")
|
|
if dir and fs.createDirectory then
|
|
local ok, err = fs.createDirectory(dir)
|
|
if not ok then return nil, err or "Could not create the save directory." end
|
|
end
|
|
local ok, err = fs.write(path, body)
|
|
if not ok then return nil, err or "Could not write the save." end
|
|
if fs.read(path) ~= body then return nil, "The save could not be verified after writing." end
|
|
return true
|
|
end
|
|
|
|
local function validJournal(journal)
|
|
if type(journal) ~= "table" or (journal.format ~= 1 and journal.format ~= 2 and journal.format ~= 3)
|
|
or type(journal.jobs) ~= "table" or #journal.jobs < 1
|
|
or #journal.jobs > (journal.format ~= 1 and 512 or 2) then return nil, "Invalid Box transfer journal." end
|
|
local seen = {}
|
|
for _, job in ipairs(journal.jobs) do
|
|
if type(job) ~= "table" then return nil, "Invalid Box transfer target." end
|
|
local asset = journal.format == 2 and job.kind == "asset"
|
|
local options = journal.format == 2 and job.path == "options.lua"
|
|
local deleting = journal.format == 2 and job.deleted == true and job.path ~= Store.PATH and not asset and not options
|
|
if type(job) ~= "table" or not (allowed(job.path) or asset or options) or seen[job.path]
|
|
or (not deleting and type(job.after) ~= "string") or deleting and job.after ~= nil
|
|
or (job.before ~= nil and type(job.before) ~= "string") then
|
|
return nil, "Invalid Box transfer target."
|
|
end
|
|
seen[job.path] = true
|
|
if asset then
|
|
if not Transaction.assetValid(job.path, job.after) then return nil, "Invalid Box wallpaper." end
|
|
elseif not deleting then
|
|
local data = Serializer.decode(job.after)
|
|
if type(data) ~= "table" then return nil, "Invalid transfer save data." end
|
|
if job.path == Store.PATH and not Store.validate(data) then return nil, "Invalid Box data." end
|
|
end
|
|
end
|
|
if journal.format ~= 3 and not seen[Store.PATH] then return nil, "The transfer does not include Box storage." end
|
|
return journal
|
|
end
|
|
|
|
local function quarantine(fs, body)
|
|
local stamp = os.time()
|
|
for n = 1, 1000 do
|
|
local path = ("box/conflicts/pending-%d-%d.lua"):format(stamp, n)
|
|
if not fs.getInfo(path) then
|
|
local ok, err = write(fs, path, body)
|
|
if not ok then return nil, err end
|
|
ok, err = fs.remove(Transaction.PATH)
|
|
if not ok or fs.getInfo(Transaction.PATH) then
|
|
return nil, err or "The unfinished Box transfer record could not be set aside."
|
|
end
|
|
return path
|
|
end
|
|
end
|
|
return nil, "Too many unfinished Box transfer records are waiting in box/conflicts."
|
|
end
|
|
|
|
local function abort(fs, journal, body)
|
|
local wrote = false
|
|
for _, job in ipairs(journal.jobs) do
|
|
local current = fs.read(job.path)
|
|
if not job.conflict and current ~= job.before then
|
|
local ok, why
|
|
if job.before == nil then
|
|
ok, why = fs.remove(job.path)
|
|
if ok and fs.getInfo(job.path) then ok = nil end
|
|
else ok, why = write(fs, job.path, job.before) end
|
|
if not ok then return nil, why or "Could not undo the unfinished Box transfer." end
|
|
wrote = true
|
|
end
|
|
if not job.conflict and job.after ~= nil and fs.read(job.path .. ".bak") == job.after then
|
|
local ok, why
|
|
if job.before == nil then ok, why = fs.remove(job.path .. ".bak")
|
|
else ok, why = write(fs, job.path .. ".bak", job.before) end
|
|
if not ok then return nil, why or "Could not undo the unfinished Box transfer." end
|
|
end
|
|
end
|
|
if wrote and journal.format ~= 2 then
|
|
local sync = package.loaded["src.sync.SyncEngine"]
|
|
local engine = type(sync) == "table" and sync._shared
|
|
if type(engine) == "table" then engine:noteSaveWritten() end
|
|
end
|
|
local kept, err = quarantine(fs, body)
|
|
if not kept then return nil, err end
|
|
return true, false, "A save changed while a Box transfer was pending. The transfer was undone and its record kept at " .. kept .. "."
|
|
end
|
|
|
|
local function touched(fs, journal)
|
|
if type(journal) ~= "table" or type(journal.jobs) ~= "table" then return false end
|
|
for _, job in pairs(journal.jobs) do
|
|
if type(job) == "table" and type(job.path) == "string" then
|
|
local ok, current = pcall(fs.read, job.path)
|
|
if not ok then return true end
|
|
if current ~= job.before then
|
|
if job.deleted == true and current == nil then return true end
|
|
if type(job.after) == "string" and type(current) == "string"
|
|
and job.after:sub(1, #current) == current then return true end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function Transaction.recover(fs)
|
|
if not fs or not fs.read or not fs.getInfo then return nil, "Save storage is unavailable." end
|
|
local body = fs.read(Transaction.PATH)
|
|
if not body then
|
|
if fs.getInfo(Transaction.PATH) then return nil, "The pending Box transfer could not be read." end
|
|
return true, false
|
|
end
|
|
local journal = Serializer.decode(body)
|
|
if journal == nil then
|
|
local kept, err = quarantine(fs, body)
|
|
if not kept then return nil, err end
|
|
return true, false, "An unfinished Box transfer record was unreadable and was set aside at " .. kept .. "."
|
|
end
|
|
local valid, why = validJournal(journal)
|
|
if not valid then
|
|
if touched(fs, journal) then return nil, why end
|
|
local kept, err = quarantine(fs, body)
|
|
if not kept then return nil, err end
|
|
return true, false, why .. " The unfinished Box transfer record was set aside at " .. kept .. "."
|
|
end
|
|
local conflict = false
|
|
for _, job in ipairs(journal.jobs) do
|
|
local current = fs.read(job.path)
|
|
if current == nil and fs.getInfo(job.path) then return nil, "A transfer save could not be read." end
|
|
local interrupted = journal.format ~= 1 and type(current) == "string" and type(job.after) == "string"
|
|
and job.after:sub(1, #current) == current
|
|
if current ~= job.before and current ~= job.after and not interrupted
|
|
and (journal.format ~= 1 or current == nil or Serializer.decode(current) ~= nil) then
|
|
job.conflict, conflict = true, true
|
|
end
|
|
end
|
|
if conflict then return abort(fs, journal, body) end
|
|
for _, job in ipairs(journal.jobs) do
|
|
if fs.read(job.path) ~= job.after then
|
|
if job.before then
|
|
local ok, err = write(fs, job.path .. ".box-bak", job.before)
|
|
if not ok then return nil, err end
|
|
end
|
|
if job.deleted then
|
|
local ok, err = fs.remove(job.path)
|
|
if not ok or fs.getInfo(job.path) then return nil, err or "Could not remove the synced save." end
|
|
else
|
|
local ok, err = write(fs, job.path, job.after)
|
|
if not ok then return nil, err end
|
|
end
|
|
end
|
|
if job.deleted then
|
|
for _, suffix in ipairs({ ".bak", ".tmp" }) do
|
|
if fs.getInfo(job.path .. suffix) then
|
|
local ok, err = fs.remove(job.path .. suffix)
|
|
if not ok or fs.getInfo(job.path .. suffix) then return nil, err or "Could not remove a deleted save's recovery file." end
|
|
end
|
|
end
|
|
elseif fs.read(job.path .. ".bak") ~= job.after then
|
|
local ok, err = write(fs, job.path .. ".bak", job.after)
|
|
if not ok then return nil, err end
|
|
end
|
|
if not job.deleted and fs.getInfo(job.path .. ".tmp") then
|
|
local ok, err = fs.remove(job.path .. ".tmp")
|
|
if not ok or fs.getInfo(job.path .. ".tmp") then return nil, err or "Could not clear a stale save recovery file." end
|
|
end
|
|
end
|
|
local ok, err = fs.remove(Transaction.PATH)
|
|
if not ok or fs.getInfo(Transaction.PATH) then
|
|
return nil, err or "The completed Box transfer journal could not be cleared."
|
|
end
|
|
local stored = fs.read(Store.PATH)
|
|
if stored and fs.read(Store.PATH .. ".bak") == stored
|
|
and fs.getInfo(Store.PATH .. ".box-bak") then
|
|
fs.remove(Store.PATH .. ".box-bak")
|
|
end
|
|
if journal.format == 1 or journal.format == 3 then
|
|
local sync = package.loaded["src.sync.SyncEngine"]
|
|
local engine = type(sync) == "table" and sync._shared
|
|
if type(engine) == "table" then engine:noteSaveWritten() end
|
|
end
|
|
return true, true
|
|
end
|
|
|
|
local function commit(fs, jobs, format)
|
|
local ok, why = Transaction.recover(fs)
|
|
if not ok then return nil, why end
|
|
local journal = { format = format, jobs = jobs }
|
|
local valid, err = validJournal(journal)
|
|
if not valid then return nil, err end
|
|
for _, job in ipairs(jobs) do
|
|
if fs.read(job.path) ~= job.before then return nil, "That save changed. Reload before transferring." end
|
|
end
|
|
local body = Serializer.encode(journal)
|
|
ok, err = write(fs, Transaction.PATH, body)
|
|
if not ok and fs.read(Transaction.PATH) ~= body then
|
|
fs.remove(Transaction.PATH)
|
|
return nil, err
|
|
end
|
|
local applied
|
|
ok, applied, err = Transaction.recover(fs)
|
|
if not ok then return nil, "Box transfer is pending: " .. tostring(applied) end
|
|
if not applied then return nil, err or "The Box transfer was not applied." end
|
|
return true
|
|
end
|
|
|
|
function Transaction.commit(fs, jobs) return commit(fs, jobs, 1) end
|
|
function Transaction.commitSync(fs, jobs) return commit(fs, jobs, 2) end
|
|
function Transaction.commitIdentity(fs, jobs) return commit(fs, jobs, 3) end
|
|
|
|
return Transaction
|