Thu, 23 Mar 2023 12:14:53 +0000
client: Fix timeout handling
Previously, the timeout handler would fire an error that would get caught and
logged by the timer code. However that error never reached the upper levels of
scansion, leading to the whole thing just hanging.
Now we just trigger resumption of the async runner, and throw the error from
there if we haven't received the stanza yet.
With this change, timeouts are now correctly handled and reported as failures.
-- This is my attempt at a utility library to compare two XMPP stanzas -- It is not testing for exact equivalency, but through some vague rules that felt right. -- For example, the second stanza passed to stanzas_match() is allowed to have unexpected -- elements and attributes at the top level. Beyond this, they must match exactly, except -- for whitespace differences only. -- -- There are probably bugs, and it can probably be smarter, but I don't want to spend too -- much time on it right now. local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")); end local function wants_strict(tag, default) local opt = tag.attr["scansion:strict"] or default or "yes"; if opt == "no" or opt == "false" or opt == "0" then return false; elseif opt == "yes" or opt == "true" or opt == "1" then return true; end error("Unexpected scansion:strict value: "..opt); end local function is_wildcard(k, v) if v == "{scansion:any}" then return "attr:"..k; end return (v:match("^{scansion:capture:([^}]+)}$")); end -- stanza1 == expected, stanza2 == variable -- captures is an optional table to store captures (captures["foo"] == {scansion:capture:foo}) local function stanzas_strict_match(stanza1, stanza2, captures) if stanza1.name ~= stanza2.name or stanza1.attr.xmlns ~= stanza2.attr.xmlns then return false; end for k, v in pairs(stanza1.attr) do local wildcard = is_wildcard(k, v); if not k:match("^scansion:") and not wildcard and stanza2.attr[k] ~= v then return false; end if wildcard and captures then captures[wildcard] = stanza2.attr[k]; end end for k, v in pairs(stanza2.attr) do local wildcard = is_wildcard(k, stanza1.attr[k]); if not wildcard and stanza1.attr[k] ~= v then return false; end if wildcard and captures then captures[wildcard] = v; end end if #stanza1.tags ~= #stanza2.tags then return false; end local stanza2_pos = 1; for _, child in ipairs(stanza1) do if type(child) == "table" or child:match("%S") then local match; local child2 = stanza2[stanza2_pos]; while child2 and not(type(child2) == "table" or child2:match("%S")) do stanza2_pos = stanza2_pos + 1; child2 = stanza2[stanza2_pos]; end if type(child) ~= type(child2) then return false; end if type(child) == "table" and child2.name == child.name and child2.attr.xmlns == child.attr.xmlns then -- Strict deep match match = stanzas_strict_match(child, child2, captures); elseif type(child) == "string" then -- Text nodes, must be equal, ignoring leading/trailing whitespace match = trim(child) == trim(child2); end if not match then return false; end stanza2_pos = stanza2_pos + 1; end end return true; end -- Everything in stanza1 should be present in stanza2 local function stanzas_match(stanza1, stanza2, captures) if wants_strict(stanza1, stanza1.attr.xmlns == nil and "no" or "yes") then return stanzas_strict_match(stanza1, stanza2, captures); end if stanza1.name ~= stanza2.name or stanza1.attr.xmlns ~= stanza2.attr.xmlns then return false; end for k, v in pairs(stanza1.attr) do local wildcard = is_wildcard(k, v); if not k:match("^scansion:") and not wildcard and stanza2.attr[k] ~= v then return false; end if wildcard and captures then captures[wildcard] = stanza2.attr[k]; end end local matched_children = {}; for _, child in ipairs(stanza1) do if type(child) == "table" or child:match("%S") then local match; -- Iterate through remaining nodes in stanza2, looking for a match local stanza2_pos = 1; while stanza2_pos <= #stanza2 do if not matched_children[stanza2_pos] then local child2 = stanza2[stanza2_pos]; stanza2_pos = stanza2_pos + 1; if type(child2) == type(child) then if type(child) == "table" and child2.name == child.name and child2.attr.xmlns == child.attr.xmlns then match = stanzas_match(child, child2, captures); elseif type(child) == "string" then -- Text nodes, must be equal, ignoring leading/trailing whitespace match = trim(child) == trim(child2); end if match then break; end end end end if not match then --print(_, "No match for ", child:pretty_print()) return false; end end end return true; end return { stanzas_match = stanzas_match; stanzas_strict_match = stanzas_strict_match; };