mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 00:02:23 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10314bcdcf | |||
| d573878a2f | |||
| a66efe207d | |||
| 40977337b1 | |||
| 1598f34954 | |||
| 3c3e2c54c5 | |||
| 00c72c441b | |||
| 4046b28a8c | |||
| a86d57ac44 | |||
| c9d67582ae | |||
| 1cfd91a503 | |||
| c0ad6d0328 | |||
| 84de8c9cb1 |
@@ -262,7 +262,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- name: Setup .NET 8
|
- name: Setup .NET 8
|
||||||
uses: actions/setup-dotnet@v6
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: "8.0.x"
|
dotnet-version: "8.0.x"
|
||||||
- name: Publish gen1tls (win-x64 Native AOT)
|
- name: Publish gen1tls (win-x64 Native AOT)
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ Every mod contains a root `manifest.json` defining its metadata, supported games
|
|||||||
| `optional_imports` | `array` | User-supplied files that unlock optional mod functionality. They use the same validation and private-copy flow but never block the mod from loading. |
|
| `optional_imports` | `array` | User-supplied files that unlock optional mod functionality. They use the same validation and private-copy flow but never block the mod from loading. |
|
||||||
| `conflicts` / `incompatible` | `array` | List of mod IDs that cannot run concurrently with this mod. |
|
| `conflicts` / `incompatible` | `array` | List of mod IDs that cannot run concurrently with this mod. |
|
||||||
| `permissions` | `array` | Requested privileges (e.g. `["engine_internals"]`, `["network"]`, `["filesystem"]`). |
|
| `permissions` | `array` | Requested privileges (e.g. `["engine_internals"]`, `["network"]`, `["filesystem"]`). |
|
||||||
| `log_url` | `string` | Optional https URL for `mod.postLog` log reporting (api 2; requires the `network` permission). |
|
|
||||||
| `github` | `string` | GitHub repository (`"owner/repo"`) used for update checks and dependency download links. |
|
| `github` | `string` | GitHub repository (`"owner/repo"`) used for update checks and dependency download links. |
|
||||||
|
|
||||||
### Declaring Dependencies & Scoping
|
### Declaring Dependencies & Scoping
|
||||||
@@ -865,39 +864,6 @@ with a full standard library that the sandbox cannot reach, so handing one
|
|||||||
to a mod would undo every other rule; `mod.fetch`'s workers run engine
|
to a mod would undo every other rule; `mod.fetch`'s workers run engine
|
||||||
code, so a mod gets asynchrony without gaining any new reach.
|
code, so a mod gets asynchrony without gaining any new reach.
|
||||||
|
|
||||||
## Log reporting
|
|
||||||
|
|
||||||
`mod.postLog(body, opts)` is the one-way exception to the rule that a mod
|
|
||||||
decides where it talks. It reports a debug/crash log to the https URL the
|
|
||||||
manifest declares in `log_url`, and it is the only API that may not be
|
|
||||||
pointed at a caller-chosen address:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"permissions": ["network"],
|
|
||||||
"log_url": "https://logs.example.com/receive"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The URL is validated at load: it must be `https://`, and declaring it
|
|
||||||
without the `network` permission is a load violation for api 2 mods. The
|
|
||||||
destination is reviewed when the mod ships, not chosen per call, so a mod
|
|
||||||
cannot aim this at arbitrary hosts or read back anything a server replies.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- fire and forget; poll() never blocks, same shape as mod.fetch
|
|
||||||
local job = mod:postLog("session crashed at 0x1f3a\n" .. logText)
|
|
||||||
```
|
|
||||||
|
|
||||||
`postLog(body, opts)` returns the same opaque handle as `mod.fetch:get`,
|
|
||||||
polled and released through `mod.fetch:poll` / `mod.fetch:release`. `opts`
|
|
||||||
is a closed list with one switch: `format`, either `"text"` (the default)
|
|
||||||
or `"json"`. `json` wraps the body in an envelope of `{ ts, mod, format,
|
|
||||||
body }` so a server can attribute and sort reports; any other key or value
|
|
||||||
is refused before a job is submitted. The body is capped at 64 KB, the
|
|
||||||
transfer is bounded by the same worker ceilings as `mod.fetch`, and the
|
|
||||||
response body is never returned to the mod.
|
|
||||||
|
|
||||||
## Background jobs
|
## Background jobs
|
||||||
|
|
||||||
`mod.fetch` covers work waiting on a server. `mod.job` covers work waiting on
|
`mod.fetch` covers work waiting on a server. `mod.job` covers work waiting on
|
||||||
|
|||||||
@@ -12,6 +12,48 @@
|
|||||||
"tintColor": "3b5ca8",
|
"tintColor": "3b5ca8",
|
||||||
"category": "games",
|
"category": "games",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.1.92",
|
||||||
|
"date": "2026-08-15",
|
||||||
|
"size": 11364274,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.92/gen1recomp++-0.1.92-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.91",
|
||||||
|
"date": "2026-08-15",
|
||||||
|
"size": 11351477,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.91/gen1recomp++-0.1.91-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.90",
|
||||||
|
"date": "2026-08-15",
|
||||||
|
"size": 11344338,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.90/gen1recomp++-0.1.90-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @1Jamie\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.89",
|
||||||
|
"date": "2026-08-15",
|
||||||
|
"size": 11343841,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.89/gen1recomp++-0.1.89-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @1Jamie\n- @anxiousintrovert\n- @AverageConsumer\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.88",
|
||||||
|
"date": "2026-08-14",
|
||||||
|
"size": 11311237,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.88/gen1recomp++-0.1.88-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @bryanthaboi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.1.87",
|
||||||
|
"date": "2026-08-14",
|
||||||
|
"size": 11310910,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.87/gen1recomp++-0.1.87-ios.ipa",
|
||||||
|
"localizedDescription": "Download the correct version for your computer below.\n\n## Contributors\n\n- @AverageConsumer\n- @bryanthaboi\n- @mleo2003\n- @ShaneMcGovernIE\n- MaxTomahawk"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.1.86",
|
"version": "0.1.86",
|
||||||
"date": "2026-08-14",
|
"date": "2026-08-14",
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
/Users/bryanbassett/Documents/development/pokemon-gen1-recomp-project/.bazinga/mods/timekeepers_hut
|
||||||
@@ -417,56 +417,4 @@ function HostShell.httpGet(url, userAgent, accept, maxTime)
|
|||||||
return body
|
return body
|
||||||
end
|
end
|
||||||
|
|
||||||
-- POST returning success/failure. Strictly one-way: the response body is
|
|
||||||
-- discarded, only the HTTP status class is surfaced (postLog callers never
|
|
||||||
-- trust the reply). curl --data-binary reads the payload from a pipe, so a
|
|
||||||
-- large body never lands in the command line; the Android bridge has no POST
|
|
||||||
-- transport, and httpPost reports that instead of half-working through
|
|
||||||
-- httpDownload (a GET round-trip to a POST endpoint would be a lie).
|
|
||||||
function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
|
||||||
if type(url) ~= "string" or url == "" then return nil, "missing url" end
|
|
||||||
if type(body) ~= "string" then return nil, "missing body" end
|
|
||||||
userAgent = userAgent or "gen1recomp"
|
|
||||||
if HostShell.haveCurl() then
|
|
||||||
-- --data-binary @- keeps the payload out of argv (command-line length
|
|
||||||
-- limits on Windows) and preserves every byte including trailing
|
|
||||||
-- newlines. No -f, matching httpGet: the response body is discarded
|
|
||||||
-- anyway, and curl's stderr carries the real diagnosis on failure.
|
|
||||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
|
||||||
.. "--connect-timeout 10 --max-time %d ")
|
|
||||||
:format(tonumber(maxTime) or 40)
|
|
||||||
.. "-X POST "
|
|
||||||
.. "-H " .. HostShell.quote("User-Agent: " .. userAgent) .. " "
|
|
||||||
if contentType then
|
|
||||||
cmd = cmd .. "-H " .. HostShell.quote("Content-Type: " .. contentType) .. " "
|
|
||||||
end
|
|
||||||
cmd = cmd .. "-H " .. HostShell.quote("Content-Length: " .. tostring(#body)) .. " "
|
|
||||||
.. "--data-binary @- "
|
|
||||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
|
||||||
.. HostShell.quote(url) .. " 2>&1"
|
|
||||||
local pipe = HostShell.popen(cmd, "rw")
|
|
||||||
if not pipe then return nil, "could not run curl" end
|
|
||||||
local writeOk, werr = pcall(pipe.write, pipe, body)
|
|
||||||
if not writeOk then
|
|
||||||
HostShell.pclose(pipe)
|
|
||||||
return nil, "could not write body: " .. tostring(werr)
|
|
||||||
end
|
|
||||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
|
||||||
HostShell.pclose(pipe)
|
|
||||||
if not readOk then
|
|
||||||
return nil, fetchError(url, nil, tostring(out))
|
|
||||||
end
|
|
||||||
local _, status, noise = splitCurlOutput(out)
|
|
||||||
if not status then return nil, fetchError(url, nil, noise) end
|
|
||||||
if status < 200 or status >= 300 then
|
|
||||||
return nil, fetchError(url, status, "log post rejected")
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if not haveBridge() then
|
|
||||||
return nil, "no network transport on this platform"
|
|
||||||
end
|
|
||||||
return nil, "no POST transport on this platform"
|
|
||||||
end
|
|
||||||
|
|
||||||
return HostShell
|
return HostShell
|
||||||
|
|||||||
@@ -215,19 +215,12 @@ function LauncherMods.checkDependencies(manifest, options, version, installedMan
|
|||||||
return m and not m.experimental
|
return m and not m.experimental
|
||||||
end
|
end
|
||||||
|
|
||||||
local function conflictApplies(spec, other)
|
|
||||||
return not spec.range or (other and other.version
|
|
||||||
and Semver.satisfies(other.version, spec.range))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- (a) Conflicts declared by target manifest
|
-- (a) Conflicts declared by target manifest
|
||||||
if type(manifest.conflictSpecs) == "table" then
|
if type(manifest.conflictSpecs) == "table" then
|
||||||
for _, spec in ipairs(manifest.conflictSpecs) do
|
for _, spec in ipairs(manifest.conflictSpecs) do
|
||||||
local conflictId = spec.id
|
local conflictId = spec.id
|
||||||
local installedOther = installedMap[conflictId]
|
local installedOther = installedMap[conflictId]
|
||||||
if installedOther and isEnabled(conflictId)
|
if installedOther and isEnabled(conflictId) and not conflictIdsSeen[conflictId] then
|
||||||
and conflictApplies(spec, installedOther)
|
|
||||||
and not conflictIdsSeen[conflictId] then
|
|
||||||
conflictIdsSeen[conflictId] = true
|
conflictIdsSeen[conflictId] = true
|
||||||
hasIssues = true
|
hasIssues = true
|
||||||
depsResult[#depsResult + 1] = {
|
depsResult[#depsResult + 1] = {
|
||||||
@@ -245,12 +238,11 @@ function LauncherMods.checkDependencies(manifest, options, version, installedMan
|
|||||||
|
|
||||||
-- (b) Reverse conflicts declared by installed mods against target manifest
|
-- (b) Reverse conflicts declared by installed mods against target manifest
|
||||||
if manifest.id then
|
if manifest.id then
|
||||||
local installedTarget = installedMap[manifest.id] or manifest
|
|
||||||
for _, other in ipairs(manifests) do
|
for _, other in ipairs(manifests) do
|
||||||
if other.id ~= manifest.id and isEnabled(other.id) and not conflictIdsSeen[other.id] then
|
if other.id ~= manifest.id and isEnabled(other.id) and not conflictIdsSeen[other.id] then
|
||||||
local conflicts = other.conflictSpecs or {}
|
local conflicts = other.conflictSpecs or {}
|
||||||
for _, spec in ipairs(conflicts) do
|
for _, spec in ipairs(conflicts) do
|
||||||
if spec.id == manifest.id and conflictApplies(spec, installedTarget) then
|
if spec.id == manifest.id then
|
||||||
conflictIdsSeen[other.id] = true
|
conflictIdsSeen[other.id] = true
|
||||||
hasIssues = true
|
hasIssues = true
|
||||||
depsResult[#depsResult + 1] = {
|
depsResult[#depsResult + 1] = {
|
||||||
|
|||||||
@@ -1094,23 +1094,6 @@ function Loader:_api(mod)
|
|||||||
return { available = function() return false end,
|
return { available = function() return false end,
|
||||||
get = refuse, poll = refuse, release = refuse, cancel = refuse }
|
get = refuse, poll = refuse, release = refuse, cancel = refuse }
|
||||||
end)(),
|
end)(),
|
||||||
-- One-way crash-log reporting to the https URL the manifest declares in
|
|
||||||
-- log_url. The destination is reviewed at load, not chosen per call, so
|
|
||||||
-- a mod cannot aim this at arbitrary hosts; the response body is never
|
|
||||||
-- returned, and the worker pool bounds the transfer. Same handle/poll/
|
|
||||||
-- release shape as mod.fetch, so mod.job's sibling patterns carry over.
|
|
||||||
postLog = (function()
|
|
||||||
if mod.manifest.permissionSet.network and mod.manifest.log_url then
|
|
||||||
return function(_, body, opts)
|
|
||||||
return Net.postLog(loader, modId, mod.manifest.log_url, body, opts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local function refuse()
|
|
||||||
error(('[%s] mod.postLog needs the "network" permission and a '
|
|
||||||
.. "log_url in manifest.json"):format(modId), 2)
|
|
||||||
end
|
|
||||||
return refuse
|
|
||||||
end)(),
|
|
||||||
-- Background compute, behind the "background" permission. The worker
|
-- Background compute, behind the "background" permission. The worker
|
||||||
-- rebuilds this mod's sandbox before loading the script, so a job is the
|
-- rebuilds this mod's sandbox before loading the script, so a job is the
|
||||||
-- one thing love.thread is not: off the main thread without a Lua state
|
-- one thing love.thread is not: off the main thread without a Lua state
|
||||||
|
|||||||
@@ -308,24 +308,6 @@ function Manifest.validate(raw, path)
|
|||||||
|
|
||||||
local github = Manifest.parseGithub(raw.github)
|
local github = Manifest.parseGithub(raw.github)
|
||||||
|
|
||||||
-- log_url: the mod's one-way crash-log reporting destination. https-only,
|
|
||||||
-- declared in the manifest so the engine reviews the target at load instead
|
|
||||||
-- of trusting per-call URLs from gameplay code, and gated on the `network`
|
|
||||||
-- permission the mod must also declare. api 1 mods never carry it: it is a
|
|
||||||
-- load violation, not a warning, because a postLog-capable mod that does not
|
|
||||||
-- opt in to networking is a bug in the manifest itself.
|
|
||||||
local logUrl = nil
|
|
||||||
if raw.log_url ~= nil then
|
|
||||||
if strict and not permissionSet.network then
|
|
||||||
violation(strict, raw.id, "log_url requires the network permission")
|
|
||||||
elseif strict and (type(raw.log_url) ~= "string"
|
|
||||||
or not raw.log_url:match("^https://")) then
|
|
||||||
violation(strict, raw.id, "log_url must be an https:// URL")
|
|
||||||
elseif strict then
|
|
||||||
logUrl = raw.log_url
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
assert(raw.experimental == nil or type(raw.experimental) == "boolean",
|
assert(raw.experimental == nil or type(raw.experimental) == "boolean",
|
||||||
"experimental must be a boolean")
|
"experimental must be a boolean")
|
||||||
local experimental = raw.experimental == true
|
local experimental = raw.experimental == true
|
||||||
@@ -432,7 +414,6 @@ function Manifest.validate(raw, path)
|
|||||||
affects_link = affectsLink,
|
affects_link = affectsLink,
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
permissionSet = permissionSet,
|
permissionSet = permissionSet,
|
||||||
log_url = logUrl,
|
|
||||||
options_schema = optionalFile(raw.options_schema, "options_schema"),
|
options_schema = optionalFile(raw.options_schema, "options_schema"),
|
||||||
assets_transforms = optionalFile(raw.assets_transforms, "assets_transforms"),
|
assets_transforms = optionalFile(raw.assets_transforms, "assets_transforms"),
|
||||||
required_imports = requiredImports,
|
required_imports = requiredImports,
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ local Net = {}
|
|||||||
Net.MAX_INFLIGHT = 4
|
Net.MAX_INFLIGHT = 4
|
||||||
-- Clamp on the caller's timeout, so a mod cannot pin a worker indefinitely.
|
-- Clamp on the caller's timeout, so a mod cannot pin a worker indefinitely.
|
||||||
Net.MAX_SECONDS = 30
|
Net.MAX_SECONDS = 30
|
||||||
-- A log body ceiling. Debug logs are kilobytes, and a server operator has no
|
|
||||||
-- reason to accept a mod uploading arbitrary megabytes to its endpoint.
|
|
||||||
Net.MAX_BODY = 65536
|
|
||||||
|
|
||||||
local function fetch()
|
local function fetch()
|
||||||
return require("src.net.Fetch")
|
return require("src.net.Fetch")
|
||||||
@@ -102,62 +99,6 @@ function Net.get(loader, modId, url, opts)
|
|||||||
return handle
|
return handle
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The closed list of postLog format switches. Anything outside it is a
|
|
||||||
-- caller bug, rejected before a job is submitted, so the surface stays
|
|
||||||
-- exactly two shapes on the wire.
|
|
||||||
local POST_FORMATS = { text = true, json = true }
|
|
||||||
|
|
||||||
-- A one-way log POST to the mod's manifest-declared log_url (https only,
|
|
||||||
-- validated in Manifest.lua). Same shape as get(): opaque handle, per-mod
|
|
||||||
-- in-flight ceiling, user agent naming the mod. The response body is never
|
|
||||||
-- returned -- a postLog is fire-and-forget reporting, and the engine has no
|
|
||||||
-- reason to hand a mod a server's reply.
|
|
||||||
function Net.postLog(loader, modId, logUrl, body, opts)
|
|
||||||
if type(body) ~= "string" or body == "" then
|
|
||||||
return nil, "log body must be a non-empty string"
|
|
||||||
end
|
|
||||||
if #body > Net.MAX_BODY then
|
|
||||||
return nil, ("log body too large (%d bytes, limit %d)"):format(#body, Net.MAX_BODY)
|
|
||||||
end
|
|
||||||
opts = type(opts) == "table" and opts or {}
|
|
||||||
for key in pairs(opts) do
|
|
||||||
if key ~= "format" then
|
|
||||||
return nil, ("unknown log option %q (format is the only switch)"):format(tostring(key))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local format = opts.format or "text"
|
|
||||||
if not POST_FORMATS[format] then
|
|
||||||
return nil, ("unknown log format %q (text and json only)"):format(tostring(format))
|
|
||||||
end
|
|
||||||
local denial = Net.urlDenial(logUrl)
|
|
||||||
if denial then return nil, denial end
|
|
||||||
local b = bucket(loader, modId)
|
|
||||||
if inflight(b) >= Net.MAX_INFLIGHT then
|
|
||||||
return nil, ("too many requests in flight (limit %d); poll and release "
|
|
||||||
.. "the ones you have"):format(Net.MAX_INFLIGHT)
|
|
||||||
end
|
|
||||||
local payload = body
|
|
||||||
local contentType = "text/plain"
|
|
||||||
if format == "json" then
|
|
||||||
local Json = require("src.link.Json")
|
|
||||||
payload = Json.encode({
|
|
||||||
ts = os.time(),
|
|
||||||
mod = modId,
|
|
||||||
format = "json",
|
|
||||||
body = body,
|
|
||||||
})
|
|
||||||
contentType = "application/json"
|
|
||||||
end
|
|
||||||
local id = fetch().post(logUrl, payload, {
|
|
||||||
userAgent = "gen1recomp-mod/" .. tostring(modId),
|
|
||||||
contentType = contentType,
|
|
||||||
maxSeconds = Net.MAX_SECONDS,
|
|
||||||
})
|
|
||||||
local handle = {}
|
|
||||||
b[handle] = id
|
|
||||||
return handle
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A copy of the job's state, never the engine's own table. An unknown or
|
-- A copy of the job's state, never the engine's own table. An unknown or
|
||||||
-- forged handle reads as an error rather than nil, so a mod that lost track of
|
-- forged handle reads as an error rather than nil, so a mod that lost track of
|
||||||
-- one cannot spin waiting on it forever.
|
-- one cannot spin waiting on it forever.
|
||||||
|
|||||||
@@ -132,17 +132,6 @@ function Fetch.get(url, opts)
|
|||||||
accept = opts.accept, maxSeconds = opts.maxSeconds })
|
accept = opts.accept, maxSeconds = opts.maxSeconds })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- POST a body to a URL, one-way. The result carries no body: postLog
|
|
||||||
-- reporting never trusts a server's reply, so the worker surfaces only
|
|
||||||
-- ok/error and the transport's complaint.
|
|
||||||
-- opts: { userAgent, contentType, maxSeconds }
|
|
||||||
function Fetch.post(url, body, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
return submit({ kind = "post", url = url, body = body,
|
|
||||||
userAgent = opts.userAgent or "gen1recomp",
|
|
||||||
contentType = opts.contentType, maxSeconds = opts.maxSeconds })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Download a URL to `saveRel`, a path relative to the LOVE save directory.
|
-- Download a URL to `saveRel`, a path relative to the LOVE save directory.
|
||||||
-- Progress is reported as a 0..1 fraction when `size` is known.
|
-- Progress is reported as a 0..1 fraction when `size` is known.
|
||||||
function Fetch.download(url, saveRel, opts)
|
function Fetch.download(url, saveRel, opts)
|
||||||
|
|||||||
@@ -99,20 +99,6 @@ local function doDownload(job)
|
|||||||
post({ id = job.id, ok = true, path = rel, done = true })
|
post({ id = job.id, ok = true, path = rel, done = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
local function doPost(job)
|
|
||||||
if not HostShell then
|
|
||||||
post({ id = job.id, ok = false, err = "no transport" })
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local ok, err = HostShell.httpPost(job.url, job.body, job.contentType,
|
|
||||||
job.userAgent, tonumber(job.maxSeconds) or GET_MAX_SECONDS)
|
|
||||||
if not ok then
|
|
||||||
post({ id = job.id, ok = false, err = err or "post failed" })
|
|
||||||
return
|
|
||||||
end
|
|
||||||
post({ id = job.id, ok = true, done = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
local job = cmdCh:demand()
|
local job = cmdCh:demand()
|
||||||
-- The flag is checked before the job's KIND, so a worker woken by a
|
-- The flag is checked before the job's KIND, so a worker woken by a
|
||||||
@@ -128,9 +114,6 @@ while true do
|
|||||||
elseif job.kind == "get" then
|
elseif job.kind == "get" then
|
||||||
local ok, err = pcall(doGet, job)
|
local ok, err = pcall(doGet, job)
|
||||||
if not ok then post({ id = job.id, ok = false, err = tostring(err) }) end
|
if not ok then post({ id = job.id, ok = false, err = tostring(err) }) end
|
||||||
elseif job.kind == "post" then
|
|
||||||
local ok, err = pcall(doPost, job)
|
|
||||||
if not ok then post({ id = job.id, ok = false, err = tostring(err) }) end
|
|
||||||
elseif job.kind == "download" then
|
elseif job.kind == "download" then
|
||||||
local ok, err = pcall(doDownload, job)
|
local ok, err = pcall(doDownload, job)
|
||||||
if not ok then post({ id = job.id, ok = false, err = tostring(err) }) end
|
if not ok then post({ id = job.id, ok = false, err = tostring(err) }) end
|
||||||
|
|||||||
@@ -550,51 +550,6 @@ local installedColorlib = Manifest.validate({
|
|||||||
version = "1.0.0",
|
version = "1.0.0",
|
||||||
entry = "main.lua",
|
entry = "main.lua",
|
||||||
}, "mods/colorlib")
|
}, "mods/colorlib")
|
||||||
local unconditionalConflict = LauncherMods.checkDependencies(testTargetManifest,
|
|
||||||
nil, nil, { testTargetManifest, installedColorlib })
|
|
||||||
check(unconditionalConflict.hasIssues == true,
|
|
||||||
"dependency resolver reports an unversioned conflict")
|
|
||||||
|
|
||||||
local function rangeTarget(version, conflicts)
|
|
||||||
return Manifest.validate({
|
|
||||||
id = "range_target",
|
|
||||||
name = "Range Target",
|
|
||||||
version = version,
|
|
||||||
entry = "main.lua",
|
|
||||||
conflicts = conflicts or {},
|
|
||||||
}, "mods/range_target")
|
|
||||||
end
|
|
||||||
local function rangeSource(conflicts)
|
|
||||||
return Manifest.validate({
|
|
||||||
id = "range_source",
|
|
||||||
name = "Range Source",
|
|
||||||
version = "1.0.0",
|
|
||||||
entry = "main.lua",
|
|
||||||
conflicts = conflicts or {},
|
|
||||||
}, "mods/range_source")
|
|
||||||
end
|
|
||||||
|
|
||||||
local forwardSource = rangeSource({ "range_target@<2.0.0" })
|
|
||||||
local matchingTarget = rangeTarget("1.4.0")
|
|
||||||
local nonmatchingTarget = rangeTarget("2.0.0")
|
|
||||||
local forwardMatching = LauncherMods.checkDependencies(forwardSource,
|
|
||||||
nil, nil, { forwardSource, matchingTarget })
|
|
||||||
check(forwardMatching.hasIssues == true and #forwardMatching.deps == 1,
|
|
||||||
"dependency resolver applies a matching forward conflict range")
|
|
||||||
local forwardNonmatching = LauncherMods.checkDependencies(forwardSource,
|
|
||||||
nil, nil, { forwardSource, nonmatchingTarget })
|
|
||||||
check(forwardNonmatching.hasIssues == false and #forwardNonmatching.deps == 0,
|
|
||||||
"dependency resolver ignores a nonmatching forward conflict range")
|
|
||||||
|
|
||||||
local reverseSource = rangeSource({ "range_target@<2.0.0" })
|
|
||||||
local reverseMatching = LauncherMods.checkDependencies(matchingTarget,
|
|
||||||
nil, nil, { reverseSource, matchingTarget })
|
|
||||||
check(reverseMatching.hasIssues == true and #reverseMatching.deps == 1,
|
|
||||||
"dependency resolver applies a matching reverse conflict range")
|
|
||||||
local reverseNonmatching = LauncherMods.checkDependencies(nonmatchingTarget,
|
|
||||||
nil, nil, { reverseSource, nonmatchingTarget })
|
|
||||||
check(reverseNonmatching.hasIssues == false and #reverseNonmatching.deps == 0,
|
|
||||||
"dependency resolver ignores a nonmatching reverse conflict range")
|
|
||||||
-- ------- scoped dependency tests
|
-- ------- scoped dependency tests
|
||||||
local Json = require("src.link.Json")
|
local Json = require("src.link.Json")
|
||||||
local scopedDepManifest = Manifest.validate({
|
local scopedDepManifest = Manifest.validate({
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
-- mod.postLog: one-way log reporting to the manifest-declared log_url.
|
|
||||||
-- The things this pins are the strict ones -- https-only destination that
|
|
||||||
-- lives in the manifest (not per-call), a closed list of format switches,
|
|
||||||
-- a body ceiling, opaque per-mod handles, and refusal without the network
|
|
||||||
-- permission.
|
|
||||||
|
|
||||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
||||||
|
|
||||||
local T = require("tests.modkit")
|
|
||||||
local Net = require("src.mods.Net")
|
|
||||||
local Fetch = require("src.net.Fetch")
|
|
||||||
|
|
||||||
-- Stand in for the worker pool: jobs resolve when the test says so, so no
|
|
||||||
-- test here touches a socket.
|
|
||||||
local submitted, nextId, states = {}, 0, {}
|
|
||||||
Fetch.post = function(url, body, opts)
|
|
||||||
nextId = nextId + 1
|
|
||||||
submitted[nextId] = { url = url, body = body, opts = opts }
|
|
||||||
states[nextId] = { status = "pending", progress = 0 }
|
|
||||||
return nextId
|
|
||||||
end
|
|
||||||
Fetch.poll = function(id) return states[id] or { status = "error", err = "unknown job" } end
|
|
||||||
Fetch.isPending = function(id) return (states[id] or {}).status == "pending" end
|
|
||||||
Fetch.release = function(id) states[id] = nil end
|
|
||||||
Fetch.cancel = function(id)
|
|
||||||
if states[id] and states[id].status == "pending" then states[id].status = "cancelled" end
|
|
||||||
end
|
|
||||||
Fetch.available = function() return true end
|
|
||||||
|
|
||||||
local LOGGER = {
|
|
||||||
["mods/log_sender/manifest.json"] = [[{
|
|
||||||
"id": "log_sender",
|
|
||||||
"name": "Log Sender",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"entry": "main.lua",
|
|
||||||
"api": 2,
|
|
||||||
"permissions": ["network"],
|
|
||||||
"log_url": "https://logs.example.com/logs"
|
|
||||||
}]],
|
|
||||||
["mods/log_sender/main.lua"] = [[
|
|
||||||
local mod = ...
|
|
||||||
mod.exports.send = function(body, opts)
|
|
||||||
local handle, err = mod:postLog(body, opts)
|
|
||||||
if not handle then return nil, err end
|
|
||||||
return handle
|
|
||||||
end
|
|
||||||
mod.exports.poll = function(h) return mod.fetch:poll(h) end
|
|
||||||
mod.exports.release = function(h) return mod.fetch:release(h) end
|
|
||||||
]],
|
|
||||||
}
|
|
||||||
|
|
||||||
local function manifest(id, extra)
|
|
||||||
return ('{"id": "%s", "name": "T", "version": "1.0.0", "entry": "main.lua", '
|
|
||||||
.. '"api": 2%s}'):format(id, extra or "")
|
|
||||||
end
|
|
||||||
|
|
||||||
local NO_URL = {
|
|
||||||
["mods/log_no_url/manifest.json"] = manifest("log_no_url", ', "permissions": ["network"]'),
|
|
||||||
["mods/log_no_url/main.lua"] = [[
|
|
||||||
local mod = ...
|
|
||||||
mod.exports.try = function()
|
|
||||||
local ok, err = pcall(function() return mod:postLog("body") end)
|
|
||||||
return ok, err
|
|
||||||
end
|
|
||||||
]],
|
|
||||||
}
|
|
||||||
|
|
||||||
-- ------------------------------------------------ the closed opts list
|
|
||||||
local run = T.sdk.loadMods({ "mods/log_sender" }, { fs = T.sdk.memfs(LOGGER) })
|
|
||||||
T.eq(#run.errors, 0, "the logger mod loads clean (" .. tostring(run.errors[1]) .. ")")
|
|
||||||
local api = run.loader.exports.log_sender
|
|
||||||
|
|
||||||
local bad, badErr = api.send("body", { format = "xml" })
|
|
||||||
T.eq(bad, nil, "an unknown format is refused")
|
|
||||||
T.check(badErr and badErr:find("text and json only", 1, true), "and names the allowed ones")
|
|
||||||
|
|
||||||
local badKey, keyErr = api.send("body", { envelope = true })
|
|
||||||
T.eq(badKey, nil, "an unknown opt key is refused")
|
|
||||||
T.check(keyErr and keyErr:find("format is the only switch", 1, true), "and says so")
|
|
||||||
|
|
||||||
-- ------------------------------------------------ body validation
|
|
||||||
local empty, emptyErr = api.send("")
|
|
||||||
T.eq(empty, nil, "an empty body is refused")
|
|
||||||
local big = string.rep("x", Net.MAX_BODY + 1)
|
|
||||||
local bigH, bigErr = api.send(big)
|
|
||||||
T.eq(bigH, nil, "an oversized body is refused")
|
|
||||||
T.check(bigErr and bigErr:find("limit", 1, true), "and names the limit")
|
|
||||||
|
|
||||||
-- ------------------------------------------------- text format (default)
|
|
||||||
local handle, err = api.send("hello log")
|
|
||||||
T.check(handle ~= nil, "a plain text post returns a handle (" .. tostring(err) .. ")")
|
|
||||||
T.eq(type(handle), "table", "the handle is opaque, not the engine's job id")
|
|
||||||
T.eq(api.poll(handle).status, "pending", "a fresh post polls as pending")
|
|
||||||
|
|
||||||
local sent
|
|
||||||
for _, job in pairs(submitted) do
|
|
||||||
if job.url == "https://logs.example.com/logs" and job.body == "hello log" then sent = job end
|
|
||||||
end
|
|
||||||
T.check(sent ~= nil, "the post reached the pool with the manifest URL")
|
|
||||||
T.eq(sent.opts.contentType, "text/plain", "plain text posts as text/plain")
|
|
||||||
T.check(sent.opts.userAgent:find("log_sender", 1, true),
|
|
||||||
"the request identifies the calling mod")
|
|
||||||
|
|
||||||
-- -------------------------------------------------- json format
|
|
||||||
local jh, jerr = api.send("line one", { format = "json" })
|
|
||||||
T.check(jh ~= nil, "a json post returns a handle (" .. tostring(jerr) .. ")")
|
|
||||||
local jsent
|
|
||||||
for _, job in pairs(submitted) do
|
|
||||||
if job.opts.contentType == "application/json" then jsent = job end
|
|
||||||
end
|
|
||||||
T.check(jsent ~= nil, "json posts as application/json")
|
|
||||||
local decoded = require("src.link.Json").decode(jsent.body)
|
|
||||||
T.eq(type(decoded), "table", "the json body is a table")
|
|
||||||
T.eq(decoded.format, "json", "the envelope names its format")
|
|
||||||
T.eq(decoded.mod, "log_sender", "the envelope names the mod")
|
|
||||||
T.eq(decoded.body, "line one", "the payload survives the envelope")
|
|
||||||
|
|
||||||
-- completion flows through poll, like get
|
|
||||||
states[1] = { status = "ok", progress = 1 }
|
|
||||||
local got = api.poll(handle)
|
|
||||||
T.eq(got.status, "ok", "a completed post polls ok")
|
|
||||||
|
|
||||||
api.release(handle)
|
|
||||||
run.release()
|
|
||||||
|
|
||||||
-- --------------------------------------- manifest without log_url refuses
|
|
||||||
local nurl = T.sdk.loadMods({ "mods/log_no_url" }, { fs = T.sdk.memfs(NO_URL) })
|
|
||||||
T.eq(#nurl.errors, 0, "no log_url loads clean (" .. tostring(nurl.errors[1]) .. ")")
|
|
||||||
local okCall, callErr = nurl.loader.exports.log_no_url.try()
|
|
||||||
T.check(not okCall and callErr:find("log_url", 1, true),
|
|
||||||
"postLog without log_url names the missing manifest field")
|
|
||||||
nurl.release()
|
|
||||||
|
|
||||||
-- ------------------------------------------- manifest validation: the gate
|
|
||||||
-- log_url without the network permission is a load violation in a strict
|
|
||||||
-- manifest: the mod declares a network capability it did not opt in to. The
|
|
||||||
-- violation fires inside manifest validation, so the mod never enters
|
|
||||||
-- loader.mods at all.
|
|
||||||
local badManifest = T.sdk.loadMods({ "mods/log_bad" }, { fs = T.sdk.memfs({
|
|
||||||
["mods/log_bad/manifest.json"] = manifest("log_bad",
|
|
||||||
', "log_url": "https://logs.example.com/logs"'),
|
|
||||||
["mods/log_bad/main.lua"] = "local mod = ...",
|
|
||||||
}) })
|
|
||||||
T.eq(badManifest.mods.log_bad, nil,
|
|
||||||
"log_url without network: the mod is refused before load")
|
|
||||||
|
|
||||||
-- a non-https log_url is refused even with the permission
|
|
||||||
local httpManifest = T.sdk.loadMods({ "mods/log_http" }, { fs = T.sdk.memfs({
|
|
||||||
["mods/log_http/manifest.json"] = manifest("log_http",
|
|
||||||
', "permissions": ["network"], "log_url": "http://logs.example.com/logs"'),
|
|
||||||
["mods/log_http/main.lua"] = "local mod = ...",
|
|
||||||
}) })
|
|
||||||
T.eq(httpManifest.mods.log_http, nil,
|
|
||||||
"an http log_url: the mod is refused before load")
|
|
||||||
|
|
||||||
-- an api 1 manifest carries no strict surface: log_url is ignored, and the
|
|
||||||
-- mod loads (its postLog call still refuses -- there is no log_url to use)
|
|
||||||
local api1 = T.sdk.loadMods({ "mods/log_api1" }, { fs = T.sdk.memfs({
|
|
||||||
["mods/log_api1/manifest.json"] = [[{
|
|
||||||
"id": "log_api1", "name": "T", "version": "1.0.0", "entry": "main.lua",
|
|
||||||
"api": 1, "log_url": "https://logs.example.com/logs"
|
|
||||||
}]],
|
|
||||||
["mods/log_api1/main.lua"] = "local mod = ...",
|
|
||||||
}) })
|
|
||||||
T.eq(#api1.errors, 0, "an api 1 manifest ignores log_url ("
|
|
||||||
.. tostring(api1.errors[1]) .. ")")
|
|
||||||
T.check(api1.loader.mods.log_api1 ~= nil, "and the mod loads")
|
|
||||||
|
|
||||||
T.finish("mod_postlog")
|
|
||||||
Reference in New Issue
Block a user