mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
path read
This commit is contained in:
@@ -254,7 +254,7 @@ engine's globals. Every chunk you author gets it: `main.lua`, your
|
|||||||
| `os.getenv`, `os.execute`, `os.remove`, `os.rename`, `os.exit` | nothing; `os.time`/`os.date`/`os.clock` still work |
|
| `os.getenv`, `os.execute`, `os.remove`, `os.rename`, `os.exit` | nothing; `os.time`/`os.date`/`os.clock` still work |
|
||||||
| `package`, `dofile`, `loadfile`, `debug`, `getfenv`, `setfenv` | `require` for the supported engine modules |
|
| `package`, `dofile`, `loadfile`, `debug`, `getfenv`, `setfenv` | `require` for the supported engine modules |
|
||||||
| `require("ffi")`, `require("love.*")` | the `love` table you are given |
|
| `require("ffi")`, `require("love.*")` | the `love` table you are given |
|
||||||
| `love.filesystem` | `mod.storage` (per-mod, per-playthrough) and `mod:read` |
|
| `love.filesystem` | `mod.storage` (per-mod, per-playthrough), `mod:read` for a known file, `mod:list` / `mod:info` to iterate your own directory |
|
||||||
| `love.thread`, `love.event` | `mod.events`, `mod.hooks` |
|
| `love.thread`, `love.event` | `mod.events`, `mod.hooks` |
|
||||||
| `love.system` | `mod.device:powerInfo()` for battery information; `mod.steps` (with the `steps` permission) for the step bridge |
|
| `love.system` | `mod.device:powerInfo()` for battery information; `mod.steps` (with the `steps` permission) for the step bridge |
|
||||||
|
|
||||||
@@ -269,9 +269,12 @@ Three consequences worth knowing before you write against it:
|
|||||||
`mod.find("your_id").exports` — the channel that was always the intended
|
`mod.find("your_id").exports` — the channel that was always the intended
|
||||||
one. The same goes for the standard library: `string`, `table` and `math`
|
one. The same goes for the standard library: `string`, `table` and `math`
|
||||||
are per-mod copies, so patching one is a local decision.
|
are per-mod copies, so patching one is a local decision.
|
||||||
- **Paths cannot climb.** `mod:read`, `mod.assets:path` and `mod.assets:image`
|
- **Paths cannot climb.** `mod:read`, `mod:list`, `mod:info`, `mod.assets:path`
|
||||||
join to your own directory, and `..`, absolute paths and drive letters are
|
and `mod.assets:image` join to your own directory, and `..`, absolute paths
|
||||||
refused. So are `entry` and `options_schema` in your manifest.
|
and drive letters are refused. So are `entry` and `options_schema` in your
|
||||||
|
manifest. `mod:list("assets")` is the sandboxed `getDirectoryItems` for a
|
||||||
|
folder you shipped; `mod:info` tells file from directory so a walk can
|
||||||
|
recurse.
|
||||||
- **Ship source, not bytecode.** A precompiled entry file is refused.
|
- **Ship source, not bytecode.** A precompiled entry file is refused.
|
||||||
|
|
||||||
`permissions` in the manifest is still a disclosure the manager shows the
|
`permissions` in the manifest is still a disclosure the manager shows the
|
||||||
|
|||||||
@@ -1163,6 +1163,39 @@ function Loader:_api(mod)
|
|||||||
api.content[alias] = self:_contentApi(mod, self.content[canonical],
|
api.content[alias] = self:_contentApi(mod, self.content[canonical],
|
||||||
("the %s registry is deprecated; use %s"):format(alias, canonical))
|
("the %s registry is deprecated; use %s"):format(alias, canonical))
|
||||||
end
|
end
|
||||||
|
-- A relative path inside this mod, or the mod root when relative is
|
||||||
|
-- omitted. Empty is the one listing case SafePath.safe rejects on
|
||||||
|
-- purpose (it is not a file), so it is special-cased here.
|
||||||
|
local function ownPath(relative, what)
|
||||||
|
if relative == nil or relative == "" then return mod.path end
|
||||||
|
return SafePath.join(mod.path, relative, what)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shallow directory listing, the sandboxed stand-in for
|
||||||
|
-- love.filesystem.getDirectoryItems. Names only, sorted, never a host
|
||||||
|
-- path. A missing directory is an empty list, not an error.
|
||||||
|
local function listOwn(_, relative)
|
||||||
|
local dir = ownPath(relative, "mod:list")
|
||||||
|
local fs = loader.fs
|
||||||
|
if not (fs and fs.getDirectoryItems) then return {} end
|
||||||
|
local items = fs.getDirectoryItems(dir) or {}
|
||||||
|
local out = {}
|
||||||
|
for i = 1, #items do out[i] = items[i] end
|
||||||
|
table.sort(out)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- love.filesystem.getInfo for a path inside this mod. type is "file" or
|
||||||
|
-- "directory"; size is set for files. nil when the path does not exist.
|
||||||
|
local function infoOwn(_, relative)
|
||||||
|
local path = ownPath(relative, "mod:info")
|
||||||
|
local fs = loader.fs
|
||||||
|
if not (fs and fs.getInfo) then return nil end
|
||||||
|
local info = fs.getInfo(path)
|
||||||
|
if not info then return nil end
|
||||||
|
return { type = info.type, size = info.size }
|
||||||
|
end
|
||||||
|
|
||||||
-- assets keeps the v1 alias to the content accessors and adds the file
|
-- assets keeps the v1 alias to the content accessors and adds the file
|
||||||
-- helpers on top, so mod.assets.pokemon and mod.assets:image both resolve
|
-- helpers on top, so mod.assets.pokemon and mod.assets:image both resolve
|
||||||
api.assets = setmetatable({
|
api.assets = setmetatable({
|
||||||
@@ -1179,12 +1212,16 @@ function Loader:_api(mod)
|
|||||||
loader.imageCache[full] = image
|
loader.imageCache[full] = image
|
||||||
return image
|
return image
|
||||||
end,
|
end,
|
||||||
|
list = listOwn,
|
||||||
|
info = infoOwn,
|
||||||
}, { __index = api.content })
|
}, { __index = api.content })
|
||||||
-- the mod's own directory and nothing above it: PhysFS already refuses a
|
-- the mod's own directory and nothing above it: PhysFS already refuses a
|
||||||
-- climb, but loader.fs is injectable and has no such floor
|
-- climb, but loader.fs is injectable and has no such floor
|
||||||
function api:read(relative)
|
function api:read(relative)
|
||||||
return loader.fs.read(SafePath.join(self.path, relative, "mod:read"))
|
return loader.fs.read(SafePath.join(self.path, relative, "mod:read"))
|
||||||
end
|
end
|
||||||
|
api.list = listOwn
|
||||||
|
api.info = infoOwn
|
||||||
-- mod.world materializes on first touch, like the image helper above: a
|
-- mod.world materializes on first touch, like the image helper above: a
|
||||||
-- headless load must not drag the world stack in, and the Game the facade
|
-- headless load must not drag the world stack in, and the Game the facade
|
||||||
-- acts on is still being wired when the entry chunk runs
|
-- acts on is still being wired when the entry chunk runs
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ function Sandbox.moduleDenial(name, permissionSet)
|
|||||||
local reason = DENIED[root]
|
local reason = DENIED[root]
|
||||||
if reason then
|
if reason then
|
||||||
return ("%s is not available to mods (it grants %s); use mod.storage, "
|
return ("%s is not available to mods (it grants %s); use mod.storage, "
|
||||||
.. "mod:read and the engine API instead"):format(name, reason)
|
.. "mod:read, mod:list and the engine API instead"):format(name, reason)
|
||||||
end
|
end
|
||||||
if DENIED_PREFIX[root] and name ~= root then
|
if DENIED_PREFIX[root] and name ~= root then
|
||||||
return ("%s is not available to mods; use mod.storage, mod:read and the "
|
return ("%s is not available to mods; use mod.storage, mod:read, mod:list "
|
||||||
.. "engine API instead"):format(name)
|
.. "and the engine API instead"):format(name)
|
||||||
end
|
end
|
||||||
if NETWORK[root] and not (permissionSet or {}).network then
|
if NETWORK[root] and not (permissionSet or {}).network then
|
||||||
return ("%s needs the \"network\" permission in manifest.json"):format(name)
|
return ("%s needs the \"network\" permission in manifest.json"):format(name)
|
||||||
@@ -68,7 +68,7 @@ end
|
|||||||
-- without an edit here.
|
-- without an edit here.
|
||||||
-- value is the replacement to name in the error, or true when there is none
|
-- value is the replacement to name in the error, or true when there is none
|
||||||
local BLOCKED_LOVE = {
|
local BLOCKED_LOVE = {
|
||||||
filesystem = "mod.storage and mod:read", thread = true,
|
filesystem = "mod.storage, mod:read and mod:list", thread = true,
|
||||||
system = "mod.device:powerInfo() for battery information, mod.steps for "
|
system = "mod.device:powerInfo() for battery information, mod.steps for "
|
||||||
.. "the step bridge", event = true,
|
.. "the step bridge", event = true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ local PROBE = [[
|
|||||||
out.readBackslash = attempt(function() return mod:read("..\\secret.txt") end)
|
out.readBackslash = attempt(function() return mod:read("..\\secret.txt") end)
|
||||||
out.assetsEscape = attempt(function() return mod.assets:path("../../x.png") end)
|
out.assetsEscape = attempt(function() return mod.assets:path("../../x.png") end)
|
||||||
out.readOwn = mod:read("data/note.txt")
|
out.readOwn = mod:read("data/note.txt")
|
||||||
|
out.listAssets = mod:list("assets")
|
||||||
|
out.listSprites = mod:list("assets/sprites")
|
||||||
|
out.listRoot = mod:list()
|
||||||
|
out.assetsList = mod.assets:list("assets")
|
||||||
|
out.infoAssets = mod:info("assets")
|
||||||
|
out.infoNote = mod:info("data/note.txt")
|
||||||
|
out.infoMissing = mod:info("nope")
|
||||||
|
out.listMissing = mod:list("nope")
|
||||||
|
out.listEscape = attempt(function() return mod:list("../secret") end)
|
||||||
|
out.infoEscape = attempt(function() return mod:info("../../x") end)
|
||||||
|
|
||||||
_G.SANDBOX_LEAK = "escaped"
|
_G.SANDBOX_LEAK = "escaped"
|
||||||
out.globalsAreOwn = _G ~= nil and _G.SANDBOX_LEAK == "escaped"
|
out.globalsAreOwn = _G ~= nil and _G.SANDBOX_LEAK == "escaped"
|
||||||
@@ -81,6 +91,8 @@ local FILES = {
|
|||||||
["mods/fix_sandbox/manifest.json"] = manifest("fix_sandbox"),
|
["mods/fix_sandbox/manifest.json"] = manifest("fix_sandbox"),
|
||||||
["mods/fix_sandbox/main.lua"] = PROBE,
|
["mods/fix_sandbox/main.lua"] = PROBE,
|
||||||
["mods/fix_sandbox/data/note.txt"] = "own file",
|
["mods/fix_sandbox/data/note.txt"] = "own file",
|
||||||
|
["mods/fix_sandbox/assets/front.png"] = "png",
|
||||||
|
["mods/fix_sandbox/assets/sprites/walk.png"] = "png",
|
||||||
}
|
}
|
||||||
|
|
||||||
local run = T.sdk.loadMods({ "mods/fix_sandbox" }, { fs = T.sdk.memfs(FILES) })
|
local run = T.sdk.loadMods({ "mods/fix_sandbox" }, { fs = T.sdk.memfs(FILES) })
|
||||||
@@ -155,6 +167,24 @@ T.check(out.readAbsolute ~= false, "mod:read refuses an absolute path")
|
|||||||
T.check(out.readBackslash ~= false, "mod:read refuses a backslash climb")
|
T.check(out.readBackslash ~= false, "mod:read refuses a backslash climb")
|
||||||
T.check(out.assetsEscape ~= false, "mod.assets:path refuses a climb")
|
T.check(out.assetsEscape ~= false, "mod.assets:path refuses a climb")
|
||||||
T.eq(out.readOwn, "own file", "and the mod's own files still read")
|
T.eq(out.readOwn, "own file", "and the mod's own files still read")
|
||||||
|
T.same(out.listAssets, { "front.png", "sprites" },
|
||||||
|
"mod:list names the children of a directory inside the mod")
|
||||||
|
T.same(out.listSprites, { "walk.png" },
|
||||||
|
"and a nested directory")
|
||||||
|
T.check(out.listRoot and out.listRoot[1] ~= nil,
|
||||||
|
"mod:list() with no path lists the mod root")
|
||||||
|
T.same(out.assetsList, out.listAssets,
|
||||||
|
"mod.assets:list is the same listing")
|
||||||
|
T.eq(out.infoAssets and out.infoAssets.type, "directory",
|
||||||
|
"mod:info reports a directory")
|
||||||
|
T.eq(out.infoNote and out.infoNote.type, "file",
|
||||||
|
"and a file")
|
||||||
|
T.eq(out.infoMissing, nil, "mod:info is nil for a missing path")
|
||||||
|
T.same(out.listMissing, {}, "mod:list of a missing path is empty, not an error")
|
||||||
|
T.check(out.listEscape and out.listEscape:find("must stay inside", 1, true),
|
||||||
|
"mod:list cannot climb out of the mod directory: " .. tostring(out.listEscape))
|
||||||
|
T.check(out.infoEscape and out.infoEscape:find("must stay inside", 1, true),
|
||||||
|
"mod:info cannot climb either")
|
||||||
run.release()
|
run.release()
|
||||||
|
|
||||||
-- ------- the grammar itself
|
-- ------- the grammar itself
|
||||||
|
|||||||
Reference in New Issue
Block a user