mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
Address required import review feedback
This commit is contained in:
@@ -17,6 +17,7 @@ local ARCHIVE = {
|
||||
local files, dirs, arch = {}, {}, {}
|
||||
local fileDataMounts, pathMounts, stagedTemps = 0, 0, {}
|
||||
local stagedEver = false
|
||||
local failWriteOnce
|
||||
|
||||
local function resetFs()
|
||||
for k in pairs(files) do files[k] = nil end
|
||||
@@ -25,6 +26,7 @@ local function resetFs()
|
||||
fileDataMounts, pathMounts = 0, 0
|
||||
stagedTemps = {}
|
||||
stagedEver = false
|
||||
failWriteOnce = nil
|
||||
end
|
||||
|
||||
local function dirChild(key, name)
|
||||
@@ -45,6 +47,10 @@ end
|
||||
local vfs = {}
|
||||
|
||||
function vfs.write(name, data)
|
||||
if failWriteOnce == name then
|
||||
failWriteOnce = nil
|
||||
return nil, "simulated write failure"
|
||||
end
|
||||
files[name] = data
|
||||
if name:match("^mod_import_") then
|
||||
stagedTemps[name] = true
|
||||
@@ -232,6 +238,33 @@ eq(files["mods/" .. MOD_ID .. "/baseroms/stadium2.z64"], "user-owned-rom",
|
||||
check(files["mods/OldFolder/baseroms/stadium2.z64"] == nil,
|
||||
"the shadow mod tree is still removed after preservation")
|
||||
|
||||
-- A preservation write failure keeps recovery bytes outside mods/, where
|
||||
-- discovery cannot mistake a baseroms-only directory for an installed mod.
|
||||
resetFs()
|
||||
files["mods/OldFolder/manifest.json"] =
|
||||
('{"id":"%s","name":"Old Copy","version":"0.9.0","entry":"main.lua"}')
|
||||
:format(MOD_ID)
|
||||
files["mods/OldFolder/main.lua"] = "return function() end\n"
|
||||
files["mods/OldFolder/baseroms/stadium2.z64"] = "user-owned-rom"
|
||||
files["imports/mods/preserve-fail.zip"] = "PK\3\4update"
|
||||
failWriteOnce = "mods/" .. MOD_ID .. "/baseroms/stadium2.z64"
|
||||
ok, err = LauncherMods.installZip("imports/mods/preserve-fail.zip",
|
||||
{ replace = true, expectId = MOD_ID })
|
||||
check(not ok, "preservation failure rejects the update")
|
||||
check(files["mods/" .. MOD_ID .. "/manifest.json"] == nil,
|
||||
"preservation failure leaves no manifest-less tree under mods")
|
||||
eq(files["imports/baseroms-recovery/" .. MOD_ID .. "/stadium2.z64"],
|
||||
"user-owned-rom", "preservation failure stages recovery outside mods")
|
||||
|
||||
files["imports/mods/preserve-retry.zip"] = "PK\3\4update"
|
||||
ok, err = LauncherMods.installZip("imports/mods/preserve-retry.zip",
|
||||
{ replace = true, expectId = MOD_ID })
|
||||
check(ok == true, "retry restores staged baseroms (" .. tostring(err) .. ")")
|
||||
eq(files["mods/" .. MOD_ID .. "/baseroms/stadium2.z64"], "user-owned-rom",
|
||||
"retry restores the recovered baserom into the installed mod")
|
||||
check(files["imports/baseroms-recovery/" .. MOD_ID .. "/stadium2.z64"] == nil,
|
||||
"successful retry clears baserom recovery debris")
|
||||
|
||||
-- #834: a manifest-less mods/<id> tree (interrupted copy debris) must not
|
||||
-- block a plain re-import as "already installed"
|
||||
resetFs()
|
||||
|
||||
@@ -17,13 +17,17 @@ local manifest = Manifest.validate({
|
||||
id = "stadium_fx", name = "Stadium FX", version = "1.0.0", entry = "main.lua",
|
||||
required_imports = {
|
||||
{ id = "stadium2", name = "Stadium 2", file = "stadium2.z64",
|
||||
format = "n64", md5 = { DIGEST, DIGEST:upper() } },
|
||||
description = "USA dump", format = "n64", size = 8,
|
||||
md5 = { DIGEST, DIGEST:upper() } },
|
||||
},
|
||||
}, "mods/stadium_fx")
|
||||
|
||||
eq(#manifest.required_imports, 1, "required import parses")
|
||||
eq(#manifest.required_imports[1].md5, 1, "accepted MD5 values normalize and dedupe")
|
||||
eq(manifest.required_imports[1].md5[1], DIGEST, "MD5 is lowercase")
|
||||
eq(manifest.required_imports[1].description, "USA dump",
|
||||
"import description is preserved for the picker UI")
|
||||
eq(manifest.required_imports[1].size, 8, "exact import size parses")
|
||||
|
||||
local optionalManifest = Manifest.validate({
|
||||
id = "optional_fx", name = "Optional FX", version = "1.0.0", entry = "main.lua",
|
||||
@@ -49,6 +53,15 @@ check(not pcall(Manifest.validate, {
|
||||
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
||||
required_imports = { { id = "rom", file = "rom.z64", md5 = "short" } },
|
||||
}), "malformed MD5 is refused")
|
||||
check(not pcall(Manifest.validate, {
|
||||
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
||||
required_imports = { { id = "rom", file = ".rom.removed", md5 = DIGEST } },
|
||||
}), "hidden import filenames cannot collide with engine metadata")
|
||||
check(not pcall(Manifest.validate, {
|
||||
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
||||
required_imports = { { id = "rom", file = "rom.bin", md5 = DIGEST,
|
||||
max_size = RequiredImports.MAX_BYTES + 1 } },
|
||||
}), "manifest import sizes cannot exceed the hard limit")
|
||||
|
||||
local canonical = "\128\55\18\64ABCD"
|
||||
local v64 = "\55\128\64\18BADC"
|
||||
@@ -79,17 +92,14 @@ local target = Manifest.validate({
|
||||
{ id = "same_rom", file = "source.z64", format = "n64", md5 = DIGEST },
|
||||
},
|
||||
}, "mods/other_fx")
|
||||
local copied = RequiredImports.reconcile({ manifest, target }, love.filesystem, fakeHash)
|
||||
eq(#copied, 1, "matching installed import is reused")
|
||||
eq(love.filesystem.read("mods/other_fx/baseroms/source.z64"), canonical,
|
||||
"reuse creates a private per-mod copy")
|
||||
local targetRows, targetMissing = RequiredImports.inspect(target,
|
||||
love.filesystem, fakeHash)
|
||||
eq(targetMissing, 1, "matching hashes do not silently share another mod's import")
|
||||
check(not targetRows[1].present,
|
||||
"a mod needs its own explicit player-selected file")
|
||||
check(RequiredImports.remove(target, "same_rom"), "a required import can be removed")
|
||||
eq(love.filesystem.read("mods/other_fx/baseroms/source.z64"), nil,
|
||||
"remove deletes this mod's private copy")
|
||||
local copiedAfterRemove = RequiredImports.reconcile({ manifest, target },
|
||||
love.filesystem, fakeHash)
|
||||
eq(#copiedAfterRemove, 0,
|
||||
"an explicit removal is not immediately undone by automatic reuse")
|
||||
check(RequiredImports.importData(target, "same_rom", canonical, { hash = fakeHash }),
|
||||
"choosing the file again clears the removal decision")
|
||||
|
||||
@@ -103,16 +113,69 @@ local legacyTarget = Manifest.validate({
|
||||
},
|
||||
}, "mods/legacy_user")
|
||||
love.filesystem.write("mods/legacy/baseroms/manually-imported.v64", v64)
|
||||
local legacyCopied = RequiredImports.reconcile({ legacy, legacyTarget },
|
||||
local legacyRows, legacyMissing = RequiredImports.inspect(legacyTarget,
|
||||
love.filesystem, fakeHash)
|
||||
eq(#legacyCopied, 1, "an undeclared legacy baserom can satisfy a new declaration")
|
||||
eq(love.filesystem.read("mods/legacy_user/baseroms/legacy-source.z64"), canonical,
|
||||
"legacy reuse still stores canonical bytes")
|
||||
eq(legacyMissing, 1, "undeclared files in another mod are never indexed")
|
||||
check(not legacyRows[1].present, "legacy baseroms remain private to their mod")
|
||||
|
||||
local capped = Manifest.validate({
|
||||
id = "capped", name = "Capped", version = "1.0.0", entry = "main.lua",
|
||||
required_imports = {
|
||||
{ id = "small", file = "small.bin", md5 = DIGEST, max_size = 4 },
|
||||
},
|
||||
}, "mods/capped")
|
||||
local tooLarge, sizeWhy = RequiredImports.validateData(
|
||||
capped.required_imports[1], "12345", fakeHash)
|
||||
eq(tooLarge, nil, "per-import size cap rejects before hashing")
|
||||
check(tostring(sizeWhy):find("too large", 1, true) ~= nil,
|
||||
"size rejection explains the limit")
|
||||
|
||||
-- A successful validation writes an engine receipt. Matching size + modtime
|
||||
-- lets later launcher refreshes avoid reading and hashing the ROM again.
|
||||
local cacheFiles = {
|
||||
["mods/cache/baseroms/source.z64"] = canonical,
|
||||
}
|
||||
local dataReads = 0
|
||||
local cacheModtime = 123
|
||||
local cacheFs = {
|
||||
getInfo = function(path, kind)
|
||||
local data = cacheFiles[path]
|
||||
if data then return { type = "file", size = #data, modtime = cacheModtime } end
|
||||
return nil
|
||||
end,
|
||||
read = function(path)
|
||||
if path == "mods/cache/baseroms/source.z64" then dataReads = dataReads + 1 end
|
||||
return cacheFiles[path]
|
||||
end,
|
||||
write = function(path, data) cacheFiles[path] = data return true end,
|
||||
remove = function(path) cacheFiles[path] = nil return true end,
|
||||
}
|
||||
local cacheManifest = Manifest.validate({
|
||||
id = "cache", name = "Cache", version = "1.0.0", entry = "main.lua",
|
||||
required_imports = {
|
||||
{ id = "source", file = "source.z64", format = "n64", size = 8,
|
||||
md5 = DIGEST },
|
||||
},
|
||||
}, "mods/cache")
|
||||
local cacheRows = RequiredImports.inspect(cacheManifest, cacheFs, fakeHash)
|
||||
check(cacheRows[1].present, "initial cached import validation succeeds")
|
||||
cacheRows = RequiredImports.inspect(cacheManifest, cacheFs, function()
|
||||
error("unchanged cached import should not be hashed again")
|
||||
end)
|
||||
check(cacheRows[1].present, "validation receipt satisfies the next refresh")
|
||||
eq(dataReads, 1, "unchanged imported ROM is read only once")
|
||||
cacheFiles["mods/cache/baseroms/source.z64"] = "BADBYTES"
|
||||
cacheModtime = 124
|
||||
cacheRows = RequiredImports.inspect(cacheManifest, cacheFs, fakeHash)
|
||||
check(not cacheRows[1].present, "changed imported ROM bypasses a stale receipt")
|
||||
eq(dataReads, 2, "changed imported ROM is read again")
|
||||
eq(cacheFiles[RequiredImports.receiptPath(cacheManifest, cacheManifest.required_imports[1])],
|
||||
nil, "stale validation receipt is removed")
|
||||
|
||||
local rejected, why = RequiredImports.importData(target, "same_rom", "wrong",
|
||||
{ hash = fakeHash })
|
||||
eq(rejected, nil, "mismatched selection is rejected")
|
||||
check(tostring(why):find("Nintendo 64", 1, true) ~= nil,
|
||||
check(tostring(why):find("N64 ROM (.z64/.v64/.n64)", 1, true) ~= nil,
|
||||
"normalization failure explains the selected format")
|
||||
|
||||
love.filesystem.write("mods/launcher_needs/manifest.json", ([[{
|
||||
|
||||
@@ -135,6 +135,24 @@ check(ri.requiredImportNotice.text:find("MD5 mismatch", 1, true) ~= nil,
|
||||
check(ri.modNotice == nil,
|
||||
"required import rejection is not hidden in the general Mods notice")
|
||||
|
||||
-- Reported size is checked before the selected file is read into Lua.
|
||||
local savedGetInfo = love.filesystem.getInfo
|
||||
love.filesystem.getInfo = function(name, kind)
|
||||
if name == "oversized_required.bin" then
|
||||
return { type = "file", size = 10 }
|
||||
end
|
||||
return savedGetInfo(name, kind)
|
||||
end
|
||||
ri.mods[1].manifest.required_imports[1].max_size = 4
|
||||
ri._importRequiredSource = RomImporter._importRequiredSource
|
||||
ri._importRequiredData = function(self) self._oversizedWasRead = true end
|
||||
ri:_importRequiredSource("needs_source", "source", "oversized_required.bin")
|
||||
check(not ri._oversizedWasRead, "oversized required file is rejected before import")
|
||||
check(ri.requiredImportNotice.text:find("too large", 1, true) ~= nil,
|
||||
"oversized required file reports its size error in the modal")
|
||||
ri.mods[1].manifest.required_imports[1].max_size = nil
|
||||
love.filesystem.getInfo = savedGetInfo
|
||||
|
||||
ri.nativePicker = true
|
||||
ri._importRequiredSource = function(self, modId, importId, source)
|
||||
self._requiredImported = { modId = modId, importId = importId, source = source }
|
||||
|
||||
Reference in New Issue
Block a user