This commit is contained in:
bryanthaboi
2026-08-21 14:03:27 -04:00
19 changed files with 1593 additions and 15 deletions
+37
View File
@@ -287,6 +287,43 @@ function CacheFs.write(rel, data)
return love.filesystem.write(rel, data)
end
-- Open a cache-relative file for streaming replacement. The returned handle
-- has write(bytes) and close() methods and follows the same portable/save-dir
-- routing as CacheFs.write without forcing the caller to hold the whole file
-- in one Lua string.
function CacheFs.openWrite(rel)
rel = withPrefix(rel)
local root = CacheFs.root()
if root then
ensureParents(root, rel)
local f, err = io.open(realPath(root, rel), "wb")
if not f then return nil, err end
return {
write = function(_, data)
local ok, writeErr = f:write(data)
if not ok then return nil, writeErr end
return true
end,
close = function() f:close() end,
}
end
if not (love and love.filesystem and love.filesystem.newFile) then
return nil, "streaming cache writes are unavailable"
end
local parent = rel:match("^(.*)/[^/]+$")
if parent and not love.filesystem.createDirectory(parent) then
local info = love.filesystem.getInfo(parent)
local reason = info and ("a " .. info.type .. " already exists there")
or "unknown reason"
return nil, "could not create " .. parent .. ": " .. reason
end
local file, makeErr = love.filesystem.newFile(rel)
if not file then return nil, makeErr or "could not create cache file" end
local ok, openErr = file:open("w")
if not ok then return nil, openErr or "could not open cache file" end
return file
end
-- read cache-relative `rel`; returns the bytes or nil
function CacheFs.read(rel)
rel = withPrefix(rel)
+162 -5
View File
@@ -388,6 +388,142 @@ local function externalFileSize(path)
return size
end
local function openImportSource(path)
-- Desktop picker paths live outside LÖVE's virtual filesystem. Prefer the
-- native file handle so a 1.46 GiB disc is never copied to a temp file or
-- read into one Lua string before validation.
local native = io.open(path, "rb")
if native then
local size, sizeErr = native:seek("end")
if size == nil or size == false then
native:close()
return nil, sizeErr or "could not determine source file size"
end
local reset, resetErr = native:seek("set", 0)
if reset == nil or reset == false then
native:close()
return nil, resetErr or "could not rewind source file"
end
return {
size = size,
read = function(_, n) return native:read(n) end,
close = function() native:close() end,
}
end
if love and love.filesystem and love.filesystem.newFile then
local file, makeErr = love.filesystem.newFile(path)
if not file then return nil, makeErr or "could not open source file" end
local ok, openErr = file:open("r")
if not ok then return nil, openErr or "could not open source file" end
local size = file.getSize and file:getSize() or nil
return {
size = size,
read = function(_, n) return file:read(n) end,
close = function() file:close() end,
}
end
return nil, "streaming source access is unavailable"
end
local function streamRequiredImport(manifest, importId, source)
local RequiredImports = require("src.mods.RequiredImports")
local spec = RequiredImports.spec(manifest, importId)
if not spec then return nil, "Import declaration was not found." end
if spec.format == "n64" then
return nil, "streaming canonicalization is unavailable for N64 imports"
end
local input, openErr = openImportSource(source)
if not input then return nil, openErr end
local sizeErr = RequiredImports.sizeError(spec, input.size, false)
if sizeErr then input:close(); return nil, sizeErr end
local CacheFs = require("src.import.CacheFs")
local destination = RequiredImports.path(manifest, spec)
local savedPrefix = CacheFs.prefix
local output
local inputClosed, outputClosed = false, false
local function closeInput()
if inputClosed then return end
inputClosed = true
pcall(function() input:close() end)
end
local function closeOutput()
if outputClosed or not output then return end
outputClosed = true
pcall(function() output:close() end)
end
local resultOk, resultDetail
CacheFs.prefix = ""
local ran, thrown = xpcall(function()
CacheFs.remove(RequiredImports.receiptPath(manifest, spec))
CacheFs.remove(destination)
local makeErr
output, makeErr = CacheFs.openWrite(destination)
if not output then
resultDetail = makeErr or "could not create imported file"
return
end
local MD5 = require("src.mods.StreamMD5")
local md5 = MD5.new()
local total, chunkBytes = 0, 4 * 1024 * 1024
while true do
local chunk = input:read(chunkBytes)
if not chunk or #chunk == 0 then break end
md5:update(chunk)
local wrote, writeErr = output:write(chunk)
if wrote == false or wrote == nil then
resultDetail = "could not copy import: "
.. tostring(writeErr or "write failed")
return
end
total = total + #chunk
if #chunk < chunkBytes then break end
end
closeInput()
closeOutput()
if input.size and total ~= input.size then
resultDetail = ("source read ended early (expected %d bytes, copied %d)")
:format(input.size, total)
return
end
local storedSizeErr = RequiredImports.sizeError(spec, total, true)
if storedSizeErr then resultDetail = storedSizeErr; return end
local digest = md5:final()
-- acceptStoredDigest uses the normal engine path/receipt rules, so
-- restore the caller's prefix before handing control to it.
CacheFs.prefix = savedPrefix
local accepted, detail = RequiredImports.acceptStoredDigest(
manifest, importId, digest, love.filesystem)
if not accepted then resultDetail = detail; return end
resultOk, resultDetail = true, detail
end, function(err)
if debug and debug.traceback then
return debug.traceback(tostring(err), 2)
end
return tostring(err)
end)
-- finally: resource handles and the process-global CacheFs prefix must
-- be restored even when a read/hash/write helper raises a Lua error.
closeInput()
closeOutput()
CacheFs.prefix = ""
if not ran or not resultOk then
pcall(function() CacheFs.remove(destination) end)
end
CacheFs.prefix = savedPrefix
if not ran then
return nil, "could not copy import: " .. tostring(thrown)
end
if not resultOk then return nil, resultDetail end
return true, resultDetail
end
local function readDroppedFile(file)
local ok, openError = file:open("r")
if not ok then return nil, openError end
@@ -1234,11 +1370,13 @@ local function chooseRequiredFile()
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
"$d.Title='" .. prompt .. "';",
"$d.Filter='All files (*.*)|*.*';",
-- Required imports can be multi-gigabyte optical-disc images. Do NOT
-- stage them through %TEMP%: that doubles free-space requirements and a
-- failed Copy-Item can leave a plausible-looking truncated temp file.
-- Stream the selected source directly into the mod-owned destination.
"if($d.ShowDialog() -eq 'OK'){",
"$t=Join-Path $env:TEMP 'pokeport_required_import.bin';",
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
"[Console]::Write($t)}",
"[Console]::Write($d.FileName)}",
})
return commandOutput(
'powershell -NoProfile -STA -Command "' .. script .. '"')
@@ -2078,8 +2216,13 @@ function RomImporter:_importRequiredSource(modId, importId, source, confirmed)
return nil
end
local RequiredImports = require("src.mods.RequiredImports")
local info = love.filesystem.getInfo(source, "file")
local size = info and info.size or externalFileSize(source)
-- A desktop picker returns a host path. Ask the host file handle first;
-- love.filesystem.getInfo is only authoritative for virtual/save paths.
local size = externalFileSize(source)
if not size then
local info = love.filesystem.getInfo(source, "file")
size = info and info.size or nil
end
local sizeErr = RequiredImports.sizeError(spec, size, false)
if sizeErr then
requiredImportNotice(self, modId, importId, sizeErr)
@@ -2102,6 +2245,20 @@ function RomImporter:_importRequiredSource(modId, importId, source, confirmed)
}
return nil
end
if type(size) == "number" and size > RequiredImports.LARGE_WARN_BYTES
and spec.format ~= "n64" then
local ok, result = streamRequiredImport(manifest, importId, source)
if ok then
self.requiredImportNotice = nil
self.modNotice = { ok = true, text = "Imported " .. tostring(importId)
.. " for " .. tostring(manifest.name or manifest.id) .. "." }
self:_refreshMods()
return true
end
requiredImportNotice(self, modId, importId, result)
self.modNotice = nil
return nil
end
local data = love.filesystem.read(source)
if not data then data = readExternalPath(source) end
if not data then