mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
CLOSES #1496
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
-- Hostile link traffic: every message type this build reads, with every
|
||||
-- field replaced by every wrong Lua type, driven through the real Session
|
||||
-- choke point and then into the real consumers (trade session, link battle,
|
||||
-- spectator battle, tournament screen -- including its draw).
|
||||
--
|
||||
-- The three payloads from the "How to Troll Pokemon Players" writeup are
|
||||
-- rows in the table below: action.slot as a table, hash.parts as a number,
|
||||
-- and pick.index out of range.
|
||||
--
|
||||
-- Self-contained; run directly or via run_link_tests.lua:
|
||||
-- luajit tests/link_hostile.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local Data = require("src.core.Data")
|
||||
if not Data.pokemon then Data:load() end
|
||||
local Font = require("src.render.Font")
|
||||
Font.load(Data)
|
||||
|
||||
local Input = require("src.core.Input")
|
||||
Input:init()
|
||||
|
||||
local Json = require("src.link.Json")
|
||||
local LinkBattle = require("src.link.LinkBattle")
|
||||
local Net = require("src.link.Net")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Protocol = require("src.link.Protocol")
|
||||
local Session = require("src.link.Session")
|
||||
local Tournament = require("src.link.Tournament")
|
||||
local Wire = require("src.link.Wire")
|
||||
|
||||
local failures = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("ok " .. msg)
|
||||
else
|
||||
failures = failures + 1
|
||||
print("FAIL " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- corpus
|
||||
|
||||
local HOSTILE = { {}, { 1, 2, 3 }, { type = "x" }, 0, -1, 123, 1.5,
|
||||
math.huge, "s", "", true, false }
|
||||
|
||||
local function copy(v)
|
||||
if type(v) ~= "table" then return v end
|
||||
local out = {}
|
||||
for k, val in pairs(v) do out[k] = copy(val) end
|
||||
return out
|
||||
end
|
||||
|
||||
local packedMon = Protocol.packMon(Pokemon.new(Data, "PIKACHU", 12))
|
||||
|
||||
local TEMPLATES = {
|
||||
{ type = "hello", protocol = 2, name = "RED", mode = "battle",
|
||||
engineVersion = "1.0.0", apiVersion = "1", generation = 1,
|
||||
fingerprint = "abc123", linkModified = false,
|
||||
mods = { { id = "demo", version = "1.0", affectsLink = true } } },
|
||||
{ type = "records", pokemon = { PIKACHU = "a" }, moves = { TACKLE = "b" },
|
||||
heldItems = { BERRY = "c" } },
|
||||
{ type = "party", mons = { copy(packedMon) }, seed = 1234, forceLevel = 50 },
|
||||
{ type = "pick", index = 1 },
|
||||
{ type = "confirm", ok = true },
|
||||
{ type = "action", kind = "move", slot = 1, index = 1 },
|
||||
{ type = "hash", turn = 1, value = "v",
|
||||
parts = { actives = "a", volatile = "b", bench = "c" } },
|
||||
{ type = "replace", index = 1 },
|
||||
{ type = "bye" },
|
||||
{ type = "forfeit" },
|
||||
{ type = "spectate", side = "host",
|
||||
msg = { type = "action", kind = "move", slot = 1 } },
|
||||
{ type = "hosted", code = "ABCDEF" },
|
||||
{ type = "paired" },
|
||||
{ type = "peer_gone" },
|
||||
{ type = "join_error", reason = "not_found" },
|
||||
{ type = "tournament_hosted", code = "ABCDEF", turnLimit = 6,
|
||||
requiredPartySize = 3, minLevel = 5, maxLevel = 50, forceLevel = 50,
|
||||
participating = true },
|
||||
{ type = "tournament_host_error", reason = "party_ineligible",
|
||||
requiredPartySize = 3, minLevel = 5, maxLevel = 50 },
|
||||
{ type = "tournament_join_error", reason = "party_ineligible",
|
||||
requiredPartySize = 3, minLevel = 5, maxLevel = 50 },
|
||||
{ type = "tournament_roster", players = { "RED", "BLUE" },
|
||||
spectators = { "GREEN" }, turnLimit = 6, requiredPartySize = 3,
|
||||
minLevel = 5, maxLevel = 50, forceLevel = 50 },
|
||||
{ type = "bracket_update", tournament = { code = "ABCDEF", turnLimit = 6,
|
||||
requiredPartySize = 3, minLevel = 5, maxLevel = 50, status = "active",
|
||||
round = 1, champion = "RED",
|
||||
rounds = { { round = 1, matches = { { a = "RED", b = "BLUE",
|
||||
winner = "RED", bye = false, state = "live" } } } } } },
|
||||
{ type = "match_start", opponent = "BLUE", round = 1, turnLimit = 6,
|
||||
role = "host" },
|
||||
{ type = "match_start_spectate", round = 1, playerHost = "RED",
|
||||
playerGuest = "BLUE" },
|
||||
{ type = "tournament_bye", round = 1 },
|
||||
{ type = "tournament_over", champion = "RED" },
|
||||
{ type = "a_type_this_build_has_never_heard_of", payload = { n = 1 } },
|
||||
}
|
||||
|
||||
local NESTED = {
|
||||
{ "hash", { "parts", "actives" } },
|
||||
{ "party", { "mons", 1 } },
|
||||
{ "party", { "mons", 1, "dvs" } },
|
||||
{ "party", { "mons", 1, "moves" } },
|
||||
{ "party", { "mons", 1, "moves", 1 } },
|
||||
{ "party", { "mons", 1, "nickname" } },
|
||||
{ "party", { "mons", 1, "level" } },
|
||||
{ "party", { "mons", 1, "otId" } },
|
||||
{ "hello", { "mods", 1 } },
|
||||
{ "spectate", { "msg" } },
|
||||
{ "spectate", { "msg", "slot" } },
|
||||
{ "bracket_update", { "tournament", "rounds" } },
|
||||
{ "bracket_update", { "tournament", "rounds", 1, "matches" } },
|
||||
{ "bracket_update", { "tournament", "rounds", 1, "matches", 1, "a" } },
|
||||
{ "tournament_roster", { "players", 1 } },
|
||||
}
|
||||
|
||||
local function templateFor(kind)
|
||||
for _, t in ipairs(TEMPLATES) do
|
||||
if t.type == kind then return t end
|
||||
end
|
||||
end
|
||||
|
||||
local function setPath(root, path, value)
|
||||
local node = root
|
||||
for i = 1, #path - 1 do
|
||||
node = node[path[i]]
|
||||
if type(node) ~= "table" then return false end
|
||||
end
|
||||
node[path[#path]] = value
|
||||
return true
|
||||
end
|
||||
|
||||
local corpus = {}
|
||||
local function add(msg) corpus[#corpus + 1] = msg end
|
||||
|
||||
add(false); add(true); add(123); add("string"); add({}); add({ 1, 2, 3 })
|
||||
add({ type = 5 }); add({ type = {} }); add({ type = true })
|
||||
add({ type = ("x"):rep(4096) })
|
||||
|
||||
for _, template in ipairs(TEMPLATES) do
|
||||
add(copy(template))
|
||||
for key in pairs(template) do
|
||||
if key ~= "type" then
|
||||
for _, bad in ipairs(HOSTILE) do
|
||||
local m = copy(template)
|
||||
m[key] = copy(bad)
|
||||
add(m)
|
||||
end
|
||||
local missing = copy(template)
|
||||
missing[key] = nil
|
||||
add(missing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, row in ipairs(NESTED) do
|
||||
local template = templateFor(row[1])
|
||||
for _, bad in ipairs(HOSTILE) do
|
||||
local m = copy(template)
|
||||
if setPath(m, row[2], copy(bad)) then add(m) end
|
||||
end
|
||||
local m = copy(template)
|
||||
if setPath(m, row[2], nil) then add(m) end
|
||||
end
|
||||
|
||||
print(("hostile corpus: %d messages"):format(#corpus))
|
||||
|
||||
-- ---------------------------------------------------------------- session
|
||||
|
||||
local function fakeTransport(inbox)
|
||||
local transport = { paired = true, closed = false, error = nil,
|
||||
inbox = inbox, sent = {} }
|
||||
function transport:update() end
|
||||
function transport:poll()
|
||||
local messages = self.inbox
|
||||
self.inbox = {}
|
||||
return messages
|
||||
end
|
||||
function transport:send(m) table.insert(self.sent, m) end
|
||||
function transport:close() self.closed = true end
|
||||
return transport
|
||||
end
|
||||
|
||||
local transport = fakeTransport(copy(corpus))
|
||||
local session = Session.new(transport, { role = "guest", kind = "link" })
|
||||
local okUpdate, updateErr = pcall(session.update, session)
|
||||
check(okUpdate, "the whole hostile corpus goes through Session without throwing"
|
||||
.. (okUpdate and "" or (": " .. tostring(updateErr))))
|
||||
check(session:getFailure() == nil,
|
||||
"a hostile peer cannot latch a terminal failure on the session")
|
||||
check(session:getStatus() == "paired", "the session is still usable afterwards")
|
||||
local survivors = session:poll()
|
||||
check(#survivors > 0, "well-formed messages still get through")
|
||||
check(session.dropped > 0, "malformed messages are counted as dropped")
|
||||
|
||||
for _, msg in ipairs(survivors) do
|
||||
if type(msg.type) ~= "string" then
|
||||
check(false, "every delivered message has a string type")
|
||||
break
|
||||
end
|
||||
end
|
||||
check(true, "every delivered message has a string type")
|
||||
|
||||
for _, msg in ipairs(survivors) do
|
||||
local ok = pcall(Json.encode, msg)
|
||||
if not ok then
|
||||
check(false, "every delivered message is still encodable (" .. msg.type .. ")")
|
||||
break
|
||||
end
|
||||
end
|
||||
check(true, "every delivered message is still encodable")
|
||||
|
||||
-- ---------------------------------------------------------------- consumers
|
||||
|
||||
local function makeFakeGame(species, name)
|
||||
local save = require("src.core.SaveData").newGame()
|
||||
save.player.name = name or "RED"
|
||||
table.insert(save.party, Pokemon.new(Data, species, 20))
|
||||
local stack = { list = {} }
|
||||
function stack:push(s, ...)
|
||||
table.insert(self.list, s)
|
||||
if s.enter then s:enter(...) end
|
||||
end
|
||||
function stack:pop() return table.remove(self.list) end
|
||||
function stack:top() return self.list[#self.list] end
|
||||
function stack:update(dt)
|
||||
local t = self:top()
|
||||
if t and t.update then t:update(dt) end
|
||||
end
|
||||
return { data = Data, input = Input, stack = stack, save = save }
|
||||
end
|
||||
|
||||
do
|
||||
local crashed
|
||||
for _, msg in ipairs(survivors) do
|
||||
local party = { Pokemon.new(Data, "KADABRA", 30) }
|
||||
local t = Protocol.TradeSession.new(Data, party)
|
||||
local ok, err = pcall(function()
|
||||
t:handle({ type = "party", mons = Protocol.packParty({
|
||||
Pokemon.new(Data, "MACHOKE", 32) }) })
|
||||
t:handle(msg)
|
||||
t:pick(1)
|
||||
t:handle(msg)
|
||||
t:confirm(true)
|
||||
t:handle(msg)
|
||||
if t.stage == "done" then t:apply(nil) end
|
||||
end)
|
||||
if not ok then crashed = ("%s: %s"):format(tostring(msg.type), tostring(err)) end
|
||||
if crashed then break end
|
||||
end
|
||||
check(not crashed, "the trade session survives every hostile message"
|
||||
.. (crashed and (": " .. crashed) or ""))
|
||||
end
|
||||
|
||||
do
|
||||
local party = { Pokemon.new(Data, "KADABRA", 30) }
|
||||
local t = Protocol.TradeSession.new(Data, party)
|
||||
t:handle({ type = "party",
|
||||
mons = Protocol.packParty({ Pokemon.new(Data, "MACHOKE", 32) }) })
|
||||
t:pick(1)
|
||||
t:handle(Wire.sanitize({ type = "pick", index = 0 }))
|
||||
t:confirm(true)
|
||||
t:handle(Wire.sanitize({ type = "confirm", ok = true }))
|
||||
check(t.stage ~= "done", "an out-of-range pick never reaches a committed trade")
|
||||
check(t.stage == "cancelled", "...it cancels the trade instead")
|
||||
end
|
||||
|
||||
do
|
||||
local gameA = makeFakeGame("CHARIZARD", "RED")
|
||||
local gameB = makeFakeGame("BLASTOISE", "BLUE")
|
||||
local netA, netB = Net.loopbackPair()
|
||||
local battleA = LinkBattle.newHost(gameA, netA, {
|
||||
myParty = Protocol.packParty(gameA.save.party),
|
||||
theirParty = Protocol.packParty(gameB.save.party),
|
||||
theirName = "BLUE", seed = 4242 })
|
||||
gameA.stack:push(battleA)
|
||||
local crashed
|
||||
for _, msg in ipairs(survivors) do
|
||||
table.insert(netA.inbox, copy(msg))
|
||||
local ok, err = pcall(function()
|
||||
Input.pressed = {}
|
||||
gameA.stack:update(1 / 60)
|
||||
end)
|
||||
if not ok then
|
||||
crashed = ("%s: %s"):format(tostring(msg.type), tostring(err))
|
||||
break
|
||||
end
|
||||
end
|
||||
check(not crashed, "a link battle survives every hostile message"
|
||||
.. (crashed and (": " .. crashed) or ""))
|
||||
end
|
||||
|
||||
do
|
||||
local gameSpec = makeFakeGame("RATTATA", "WATCHER")
|
||||
local specInbox = {}
|
||||
local specNet = {
|
||||
closed = false,
|
||||
update = function() end,
|
||||
poll = function()
|
||||
local msgs = specInbox
|
||||
specInbox = {}
|
||||
return msgs
|
||||
end,
|
||||
send = function() end,
|
||||
close = function() end,
|
||||
}
|
||||
local battle = LinkBattle.newSpectator(gameSpec, specNet, {
|
||||
hostParty = Protocol.packParty(makeFakeGame("CHARIZARD").save.party),
|
||||
guestParty = Protocol.packParty(makeFakeGame("BLASTOISE").save.party),
|
||||
hostName = "RED", guestName = "BLUE", seed = 99 })
|
||||
gameSpec.stack:push(battle)
|
||||
local crashed
|
||||
for _, msg in ipairs(corpus) do
|
||||
for _, side in ipairs({ "host", "guest", 5, {} }) do
|
||||
local wrapped = Wire.sanitize({ type = "spectate", side = side,
|
||||
msg = copy(msg) })
|
||||
if wrapped then table.insert(specInbox, wrapped) end
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
Input.pressed = {}
|
||||
gameSpec.stack:update(1 / 60)
|
||||
end)
|
||||
if not ok then
|
||||
crashed = tostring(err)
|
||||
break
|
||||
end
|
||||
end
|
||||
check(not crashed, "a spectator battle survives every hostile envelope"
|
||||
.. (crashed and (": " .. crashed) or ""))
|
||||
end
|
||||
|
||||
do
|
||||
local game = makeFakeGame("PIKACHU", "RED")
|
||||
local exits = 0
|
||||
local t = setmetatable({
|
||||
game = game,
|
||||
stage = "bracket",
|
||||
index = 1,
|
||||
settingsIndex = 1,
|
||||
settings = { turnLimit = 6, requiredPartySize = 3, minLevel = "ANY",
|
||||
maxLevel = "ANY", forceLevel = "ANY", participating = true },
|
||||
roster = {},
|
||||
spectatorRoster = {},
|
||||
isCreator = false,
|
||||
net = { send = function() end, close = function() end,
|
||||
take = function() return nil end,
|
||||
poll = function() return {} end,
|
||||
hasPending = function() return false end },
|
||||
}, Tournament)
|
||||
t.exitWith = function(self) exits = exits + 1 end
|
||||
local crashed
|
||||
for _, msg in ipairs(survivors) do
|
||||
local ok, err = pcall(function()
|
||||
t:handleMessage(msg)
|
||||
t:draw()
|
||||
end)
|
||||
if not ok then
|
||||
crashed = ("%s: %s"):format(tostring(msg.type), tostring(err))
|
||||
break
|
||||
end
|
||||
if type(t.roster) ~= "table" or type(t.spectatorRoster) ~= "table" then
|
||||
crashed = ("%s left a non-table roster"):format(tostring(msg.type))
|
||||
break
|
||||
end
|
||||
end
|
||||
check(not crashed, "the tournament screen survives every hostile message"
|
||||
.. (crashed and (": " .. crashed) or ""))
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- json
|
||||
do
|
||||
local deep = ("["):rep(4096) .. ("]"):rep(4096)
|
||||
local value, err = Json.decode(deep)
|
||||
check(value == nil and err ~= nil, "a deeply nested document is refused")
|
||||
local long = '{"type":"hello","name":"' .. ("x"):rep(1024) .. '"}'
|
||||
check(Json.decode(long, 256) == nil, "a document past the caller's cap is refused")
|
||||
check(Json.decode(long) ~= nil, "...and the cap is opt-in for other callers")
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------- net caps
|
||||
do
|
||||
local n = Net.new()
|
||||
n.rxBuf = ("x"):rep(Net.MAX_LINE + 1)
|
||||
n:drainLines()
|
||||
check(n.closed and n.error ~= nil,
|
||||
"a peer that never sends a newline closes the connection")
|
||||
check(#n.rxBuf == 0, "...and the buffer is released")
|
||||
end
|
||||
|
||||
print(("\nlink hostile: %d messages, %d failures"):format(#corpus, failures))
|
||||
assert(failures == 0, failures .. " hostile-input failure(s)")
|
||||
return true
|
||||
Reference in New Issue
Block a user