scansion/stanzacmp.lua

Thu, 23 Mar 2023 12:14:53 +0000

author
Matthew Wild <mwild1@gmail.com>
date
Thu, 23 Mar 2023 12:14:53 +0000
changeset 172
2c17151ed21b
parent 170
db73c4c317ce
permissions
-rw-r--r--

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;
};

mercurial