Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)``

Expand Down
29 changes: 27 additions & 2 deletions library/lua/dfhack.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -375,10 +395,15 @@ local function print_userdata(printfn, value, seen, indent)
dfhack.println(strvalue)
if seen[strvalue] then
dfhack.print(prefix)
dfhack.println('<Cyclic reference! Skipping fields>\n')
dfhack.println('<Cyclic reference! Skipping fields>')
return 0
end
seen[strvalue] = true
if is_untagged_union(value) then
dfhack.print(prefix)
dfhack.println('<Untagged union! Skipping fields>')
return 0
end
return print_fields(value, seen, indent, prefix)
end

Expand All @@ -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('<Cyclic reference! skipping fields>\n')
dfhack.println('<Cyclic reference! skipping fields>')
return 0
end
seen[value] = true
Expand Down
25 changes: 25 additions & 0 deletions test/library/print.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{'^<army_controller.T_data: ',
'', '^<Untagged union'})
end)
end

function test.printall_recurse_tagged_union()
local EQ = '^%s+= $'
local t = df.unit_patient_profile_completed_jobst:new()
dfhack.with_temp_object(t, function()
-- RecoverWounded maps to the 'bed_id' member via tag_op_history
t.job_type = df.job_type.RecoverWounded
printall_recurse(t.info)
expect.eq(5, mock_print.call_count)
validate_patterns(1,
{'^<unit_patient_profile_completed_jobst.T_info: ',
'', '^bed_id$', EQ, '^%-1$'})
end)
end

function test.printall_recurse_cyclic_userdata()
local t = df.job_list_link:new()
dfhack.with_temp_object(t, function()
Expand Down
Loading