diff --git a/docs/changelog.txt b/docs/changelog.txt index 64db861534..19dcabfbbc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -67,6 +67,7 @@ Template for new versions: ## API ## Lua +- ``printall_recurse``: no longer recurses into untagged unions, which have no way to identify their live member; fixes crashes when printing structures that contain them, such as ``df.army_controller`` ## Removed diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index ccc3c25e7d..a6d7c53a9f 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3960,6 +3960,9 @@ environment by the mandatory init file dfhack.lua: * ``printall_recurse(obj)`` If the argument is a lua table or DF object reference, prints all fields recursively. + The fields of untagged unions are not printed since only one member of such a + union is live at a time and there is no way to identify which one, so + accessing any other member is undefined behavior. * ``copyall(obj)`` diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index 0dadc4fa9c..aaf58e214e 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -367,6 +367,26 @@ local function print_fields(value, seen, indent, prefix) return 0 end +-- Recursing into the members of an untagged union is unsafe: only one member +-- is live at a time and there is no tag to identify which one, so accessing +-- any of the others is undefined behavior and can crash the game. Tagged +-- unions are safe to iterate since their iterators only produce the live +-- member. +local function is_untagged_union(value) + if df.isvalid(value) ~= 'ref' then + return false + end + local vtype = value._type + if type(vtype) ~= 'table' or not vtype._union then + return false + end + -- a tagged union's iterator produces at most the one live member; an + -- untagged union's iterator produces all of them + local count = 0 + safe_iterate(value, pairs, function() count = count + 1 end) + return count > 1 +end + -- This should be same as print_array but userdata doesn't compare equal even if -- they hold same pointer. local function print_userdata(printfn, value, seen, indent) @@ -375,10 +395,15 @@ local function print_userdata(printfn, value, seen, indent) dfhack.println(strvalue) if seen[strvalue] then dfhack.print(prefix) - dfhack.println('\n') + dfhack.println('') return 0 end seen[strvalue] = true + if is_untagged_union(value) then + dfhack.print(prefix) + dfhack.println('') + return 0 + end return print_fields(value, seen, indent, prefix) end @@ -387,7 +412,7 @@ local function print_array(printfn, value, seen, indent) dfhack.println(tostring(value)) if seen[value] then dfhack.print(prefix) - dfhack.println('\n') + dfhack.println('') return 0 end seen[value] = true diff --git a/test/library/print.lua b/test/library/print.lua index 79243d8989..ea2244d3e8 100644 --- a/test/library/print.lua +++ b/test/library/print.lua @@ -190,6 +190,31 @@ function test.printall_recurse() end) end +function test.printall_recurse_untagged_union() + local t = df.army_controller:new() + dfhack.with_temp_object(t, function() + printall_recurse(t.data) + expect.eq(3, mock_print.call_count) + validate_patterns(1, + {'^