mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
fix(nx-mods): skip MTP AppleDouble zips and mount archives in memory
Mac OpenMTP leaves ._*.zip sidecars that fail PhysFS mount and hide a good install; prefer FileData mount on Horizon and keep success notices when a sibling fails. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -401,10 +401,14 @@ end
|
||||
local function listZipPaths(dir)
|
||||
local paths = {}
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems(dir) or {}) do
|
||||
local path = (dir == "" or dir == "/") and name or (dir .. "/" .. name)
|
||||
if name:lower():match("%.zip$")
|
||||
and love.filesystem.getInfo(path, "file") then
|
||||
paths[#paths + 1] = path
|
||||
-- Skip AppleDouble / hidden junk from Mac MTP (._foo.zip ends in .zip
|
||||
-- but is not a PhysFS archive — mount fails with "could not be opened").
|
||||
if name:sub(1, 1) ~= "." then
|
||||
local path = (dir == "" or dir == "/") and name or (dir .. "/" .. name)
|
||||
if name:lower():match("%.zip$")
|
||||
and love.filesystem.getInfo(path, "file") then
|
||||
paths[#paths + 1] = path
|
||||
end
|
||||
end
|
||||
end
|
||||
return paths
|
||||
@@ -441,20 +445,23 @@ function RomImporter:rescanModsAction()
|
||||
return
|
||||
end
|
||||
local anyOk = false
|
||||
local lastOk = nil
|
||||
local lastFail = nil
|
||||
for _, path in ipairs(candidates) do
|
||||
-- Reuse _installMod carefully: it must not remove the inbox source.
|
||||
self:_installMod(path)
|
||||
if self.modNotice and self.modNotice.ok then
|
||||
anyOk = true
|
||||
lastOk = self.modNotice
|
||||
else
|
||||
lastFail = self.modNotice
|
||||
end
|
||||
end
|
||||
if lastFail and not anyOk then
|
||||
self.modNotice = lastFail
|
||||
elseif lastFail and anyOk then
|
||||
-- Mixed: keep failure visible after successes refreshed the list.
|
||||
-- Prefer success when at least one zip installed (a leftover MTP
|
||||
-- AppleDouble / corrupt sibling must not hide a good install).
|
||||
if anyOk then
|
||||
self.modNotice = lastOk
|
||||
elseif lastFail then
|
||||
self.modNotice = lastFail
|
||||
end
|
||||
end
|
||||
|
||||
+56
-15
@@ -267,10 +267,18 @@ end
|
||||
|
||||
-- ------- install (love.filesystem)
|
||||
|
||||
-- Read a .zip source into bytes. A string is an external absolute path (like
|
||||
-- a chosen ROM) read with io.*, falling back to a save-dir-relative
|
||||
-- love.filesystem read; a love DroppedFile is opened the way RomImporter
|
||||
-- ingests dropped ROMs.
|
||||
-- Read a .zip source into bytes. Save-dir-relative paths (inbox /
|
||||
-- picked_mod.zip) prefer love.filesystem so NX/Android never hit a cwd-relative
|
||||
-- io.open that can see a different file than PhysFS. Absolute host paths
|
||||
-- (desktop picker) still use io.*. DroppedFile matches RomImporter ROM drops.
|
||||
local function isHostAbsolutePath(path)
|
||||
return type(path) == "string" and (
|
||||
path:match("^/")
|
||||
or path:match("^%a:[/\\]")
|
||||
or path:match("^[Ss][Dd][Mm][Cc]:")
|
||||
)
|
||||
end
|
||||
|
||||
local function readArchive(source)
|
||||
local t = type(source)
|
||||
if (t == "userdata" or t == "table") and type(source.open) == "function" then
|
||||
@@ -282,6 +290,10 @@ local function readArchive(source)
|
||||
return data
|
||||
end
|
||||
if t == "string" then
|
||||
if not isHostAbsolutePath(source) and love and love.filesystem then
|
||||
local data = love.filesystem.read(source)
|
||||
if data then return data end
|
||||
end
|
||||
local f = io.open(source, "rb")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
@@ -298,6 +310,12 @@ local function readArchive(source)
|
||||
return nil, "unsupported archive source"
|
||||
end
|
||||
|
||||
-- Local PK\3\4 / empty-file check before mount (corrupt MTP / AppleDouble).
|
||||
local function zipLooksValid(data)
|
||||
if type(data) ~= "string" or #data < 4 then return false end
|
||||
return data:sub(1, 2) == "PK"
|
||||
end
|
||||
|
||||
-- Shallow listing of a mounted archive shaped for locateRoot: files by name,
|
||||
-- and for each top-level directory a "<dir>/manifest.json" marker only when it
|
||||
-- actually holds one (so a lone folder with no manifest still reads as empty).
|
||||
@@ -495,21 +513,44 @@ function LauncherMods._installZipInner(source, opts)
|
||||
local fs = love.filesystem
|
||||
local data, readErr = readArchive(source)
|
||||
if not data then return nil, readErr end
|
||||
|
||||
-- stage into a save-dir temp so mount can reach it
|
||||
local tmp = ("mod_import_%d_%d.zip"):format(os.time(), math.random(0, 999999))
|
||||
local ok, writeErr = fs.write(tmp, data)
|
||||
if not ok then
|
||||
return nil, "could not stage the .zip: " .. tostring(writeErr)
|
||||
if not zipLooksValid(data) then
|
||||
local label = type(source) == "string" and (source:match("[^/\\]+$") or source)
|
||||
or "archive"
|
||||
return nil, "not a zip file: " .. tostring(label)
|
||||
.. " (need a real .zip; skip Mac ._ files from MTP)"
|
||||
end
|
||||
|
||||
-- Prefer in-memory mount (PHYSFS_mountMemory via FileData). Avoids Horizon's
|
||||
-- "file already open" failure when write-then-mount reopens a save-dir zip.
|
||||
local mount = "mod_import_mount"
|
||||
if not fs.mount(tmp, mount) then
|
||||
fs.remove(tmp)
|
||||
return nil, "that .zip could not be opened"
|
||||
local tmp = nil
|
||||
local mountKey = nil
|
||||
local mounted = false
|
||||
if fs.newFileData then
|
||||
local archiveName = ("mod_import_%d_%d.zip"):format(
|
||||
os.time(), math.random(0, 999999))
|
||||
local okFd, fd = pcall(fs.newFileData, data, archiveName)
|
||||
if okFd and fd and fs.mount(fd, mount) then
|
||||
mounted = true
|
||||
mountKey = fd
|
||||
end
|
||||
end
|
||||
if not mounted then
|
||||
-- Fallback: stage into a save-dir temp so path-mount can reach it.
|
||||
tmp = ("mod_import_%d_%d.zip"):format(os.time(), math.random(0, 999999))
|
||||
local ok, writeErr = fs.write(tmp, data)
|
||||
if not ok then
|
||||
return nil, "could not stage the .zip: " .. tostring(writeErr)
|
||||
end
|
||||
if not fs.mount(tmp, mount) then
|
||||
fs.remove(tmp)
|
||||
return nil, "that .zip could not be opened"
|
||||
end
|
||||
mountKey = tmp
|
||||
end
|
||||
local function cleanup()
|
||||
pcall(fs.unmount, tmp)
|
||||
fs.remove(tmp)
|
||||
pcall(fs.unmount, mountKey)
|
||||
if tmp then fs.remove(tmp) end
|
||||
end
|
||||
|
||||
local prefix, rootErr = LauncherMods.locateRoot(topLevelPaths(mount))
|
||||
|
||||
Reference in New Issue
Block a user