Files
gen1recomp/tests/rom_importer_no_picker_test.lua
T
hernan0078 9c5f33e187 Fix #482: guard love.system.pickFile so Import ROM cannot crash
Pressing Import ROM on iOS takes the whole app down:

  src/import/RomImporter.lua: attempt to call field 'pickFile' (a nil value)

love.system.pickFile is a NATIVE BRIDGE, not part of LOVE. It exists only on
builds that compiled one -- Android, and iOS builds patched by
mobile/ios/patch_love_src.py -- so on a build without it the field is simply
nil. RomImporter:546 routes iOS down the same path as Android
(`mobileOS == "Android" or mobileOS == "iOS"`), and all three mobile pick
sites called the field unguarded.

That is why the reports say "any version": nothing about it is version
specific. Red, Blue and Yellow all reach the same call.

Every one of those call sites already handles a device with no document
picker -- Choose falls back to "No picker available, copy your ROM into:"
plus the save directory, and the mod / save rows have their own notices --
and love.system.createFile at its single call site was already guarded this
way. These three were not, so the fallback that was written for exactly this
case could never be reached.

Route them through one small helper that answers false when the bridge is
absent. A build without a picker now degrades to the copy-into-the-save-folder
flow, which on iOS is a working path: the Files app exposes the app's
Documents folder and GRBootstrap sweeps what lands there into the save dir.

tests/rom_importer_no_picker_test.lua covers Import ROM, Import mod and
Import save with the bridge missing, and asserts the picker is still used
when it is present. Reverting the fix reproduces the reported error exactly.

Reported in #482 (confirmed by three people) and #512.
2026-07-31 16:38:15 -04:00

98 lines
3.5 KiB
Lua

-- #482: pressing Import ROM crashed on iOS with
--
-- src/import/RomImporter.lua: attempt to call field 'pickFile' (a nil value)
--
-- love.system.pickFile is a native bridge, not part of LÖVE, so it is absent
-- on any mobile build that did not compile one. The mobile path called it
-- unguarded, so a missing bridge took the app down instead of falling back to
-- the copy-into-the-save-folder flow each caller already had for a device with
-- no document picker.
--
-- Self-contained: `luajit tests/rom_importer_no_picker_test.lua`; also
-- dofile'd by tests/run_tests.lua.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local S = require("tests.harness").suite("rom importer without a picker")
local eq = S.eq
local check = S.check
local RomImporter = require("src.import.RomImporter")
love.system = love.system or {}
local saved = {
getOS = love.system.getOS,
pickFile = love.system.pickFile,
getSaveDirectory = love.filesystem.getSaveDirectory,
}
love.filesystem.getSaveDirectory = function() return "/tmp/pokemon-love2d" end
love.system.getOS = function() return "iOS" end
-- the condition the crash reports were in: a build with no native bridge
love.system.pickFile = nil
local function freshImporter()
return setmetatable({
android = true, -- RomImporter treats iOS as the mobile path
workState = nil,
ready = { red = false, blue = false, yellow = false },
notice = nil,
modNotice = nil,
saveNotice = {},
chooseVersion = nil,
startData = function(self, data, displayName)
self._started = { data = data, name = displayName }
end,
_installMod = function(self, name) self._mod = name end,
_importSave = function(self, version, name) self._save = name end,
}, RomImporter)
end
-- ------- Import ROM
local ri = freshImporter()
local ok, err = pcall(function() ri:choose("red") end)
check(ok, "Import ROM does not crash without a picker: " .. tostring(err))
check(ri.notice ~= nil, "and it explains itself instead of doing nothing")
eq(ri.notice.detail, "/tmp/pokemon-love2d",
"pointing at the folder to copy the ROM into")
check(not ri.pickPending, "with no pick left pending on a picker that never opened")
-- Yellow takes the same path: the crash was never version-specific.
ri = freshImporter()
ok = pcall(function() ri:choose("yellow") end)
check(ok, "Import ROM for Yellow does not crash either")
-- ------- Import mod .zip
ri = freshImporter()
ok, err = pcall(function() ri:chooseMod() end)
check(ok, "Import mod does not crash without a picker: " .. tostring(err))
check(ri.modNotice ~= nil and ri.modNotice.ok == false,
"and reports that the picker could not open")
-- ------- Import save
ri = freshImporter()
ok, err = pcall(function() ri:chooseSaveImport("red") end)
check(ok, "Import save does not crash without a picker: " .. tostring(err))
check(ri.saveNotice.red ~= nil and ri.saveNotice.red.ok == false,
"and reports that the picker could not open")
eq(ri.androidPendingVersion, nil,
"leaving no pending import for a pick that never happened")
-- ------- and the picker is still used when the bridge IS there
local pickCalls = 0
love.system.pickFile = function() pickCalls = pickCalls + 1 return true end
ri = freshImporter()
ri:choose("red")
eq(pickCalls, 1, "a build WITH the bridge still opens the picker")
check(ri.pickPending, "and waits for the pick to come back")
love.system.getOS = saved.getOS
love.system.pickFile = saved.pickFile
love.filesystem.getSaveDirectory = saved.getSaveDirectory
S.finish()