Skip to content

Commit 2219019

Browse files
authored
Introduce a recursive cycle check when writing (#345)
I noticed that writing can blow up when there are recusrive objects that reference each other. (For example, in HTTP.jl, `Response` and `Request` reference each other) This PR proposes a simple API for the `CompactContext` and `PrettyContext` where `objectid` of objects will be tracked recursively when writing and it's configurable what should be written out when a recursive cycle is detected. Custom contexts can "hook in" to this behavior by subtyping `RecursiveCheckContext` and including the required fields (see docs for new context). Otherwise, there shouldn't be any functional change to APIs in any way.
1 parent 4906286 commit 2219019

File tree

2 files changed

+82
-16
lines changed

2 files changed

+82
-16
lines changed

src/Writer.jl

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,27 +83,43 @@ abstract type JSONContext <: StructuralContext end
8383
"""
8484
Internal implementation detail.
8585
86+
To handle recursive references in objects/arrays when writing, by default we want
87+
to track references to objects seen and break recursion cycles to avoid stack overflows.
88+
Subtypes of `RecursiveCheckContext` must include two fields in order to allow recursive
89+
cycle checking to work properly when writing:
90+
* `objectids::Set{UInt64}`: set of object ids in the current stack of objects being written
91+
* `recursive_cycle_token::Any`: Any string, `nothing`, or object to be written when a cycle is detected
92+
"""
93+
abstract type RecursiveCheckContext <: JSONContext end
94+
95+
"""
96+
Internal implementation detail.
97+
8698
Keeps track of the current location in the array or object, which winds and
8799
unwinds during serialization.
88100
"""
89-
mutable struct PrettyContext{T<:IO} <: JSONContext
101+
mutable struct PrettyContext{T<:IO} <: RecursiveCheckContext
90102
io::T
91103
step::Int # number of spaces to step
92104
state::Int # number of steps at present
93105
first::Bool # whether an object/array was just started
106+
objectids::Set{UInt64}
107+
recursive_cycle_token
94108
end
95-
PrettyContext(io::IO, step) = PrettyContext(io, step, 0, false)
109+
PrettyContext(io::IO, step, recursive_cycle_token=nothing) = PrettyContext(io, step, 0, false, Set{UInt64}(), recursive_cycle_token)
96110

97111
"""
98112
Internal implementation detail.
99113
100114
For compact printing, which in JSON is fully recursive.
101115
"""
102-
mutable struct CompactContext{T<:IO} <: JSONContext
116+
mutable struct CompactContext{T<:IO} <: RecursiveCheckContext
103117
io::T
104118
first::Bool
119+
objectids::Set{UInt64}
120+
recursive_cycle_token
105121
end
106-
CompactContext(io::IO) = CompactContext(io, false)
122+
CompactContext(io::IO, recursive_cycle_token=nothing) = CompactContext(io, false, Set{UInt64}(), recursive_cycle_token)
107123

108124
"""
109125
Internal implementation detail.
@@ -265,12 +281,26 @@ end
265281
show_json(io::SC, ::CS, ::Nothing) = show_null(io)
266282
show_json(io::SC, ::CS, ::Missing) = show_null(io)
267283

284+
recursive_cycle_check(f, io, s, id) = f()
285+
286+
function recursive_cycle_check(f, io::RecursiveCheckContext, s, id)
287+
if id in io.objectids
288+
show_json(io, s, io.recursive_cycle_token)
289+
else
290+
push!(io.objectids, id)
291+
f()
292+
delete!(io.objectids, id)
293+
end
294+
end
295+
268296
function show_json(io::SC, s::CS, x::Union{AbstractDict, NamedTuple})
269-
begin_object(io)
270-
for kv in pairs(x)
271-
show_pair(io, s, kv)
297+
recursive_cycle_check(io, s, objectid(x)) do
298+
begin_object(io)
299+
for kv in pairs(x)
300+
show_pair(io, s, kv)
301+
end
302+
end_object(io)
272303
end
273-
end_object(io)
274304
end
275305

276306
function show_json(io::SC, s::CS, kv::Pair)
@@ -280,19 +310,23 @@ function show_json(io::SC, s::CS, kv::Pair)
280310
end
281311

282312
function show_json(io::SC, s::CS, x::CompositeTypeWrapper)
283-
begin_object(io)
284-
for fn in x.fns
285-
show_pair(io, s, fn, getproperty(x.wrapped, fn))
313+
recursive_cycle_check(io, s, objectid(x.wrapped)) do
314+
begin_object(io)
315+
for fn in x.fns
316+
show_pair(io, s, fn, getproperty(x.wrapped, fn))
317+
end
318+
end_object(io)
286319
end
287-
end_object(io)
288320
end
289321

290322
function show_json(io::SC, s::CS, x::Union{AbstractVector, Tuple})
291-
begin_array(io)
292-
for elt in x
293-
show_element(io, s, elt)
323+
recursive_cycle_check(io, s, objectid(x)) do
324+
begin_array(io)
325+
for elt in x
326+
show_element(io, s, elt)
327+
end
328+
end_array(io)
294329
end
295-
end_array(io)
296330
end
297331

298332
"""

test/runtests.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,36 @@ end
8585
end
8686
end
8787

88+
mutable struct R1
89+
id::Int
90+
obj
91+
end
92+
93+
struct MyCustomWriteContext <: JSON.Writer.RecursiveCheckContext
94+
io
95+
objectids::Set{UInt64}
96+
recursive_cycle_token
97+
end
98+
MyCustomWriteContext(io) = MyCustomWriteContext(io, Set{UInt64}(), nothing)
99+
Base.print(io::MyCustomWriteContext, x::UInt8) = Base.print(io.io, x)
100+
for delegate in [:indent,
101+
:delimit,
102+
:separate,
103+
:begin_array,
104+
:end_array,
105+
:begin_object,
106+
:end_object]
107+
@eval JSON.Writer.$delegate(io::MyCustomWriteContext) = JSON.Writer.$delegate(io.io)
108+
end
109+
110+
@testset "RecursiveCheckContext" begin
111+
x = R1(1, nothing)
112+
x.obj = x
113+
str = JSON.json(x)
114+
@test str == "{\"id\":1,\"obj\":null}"
115+
io = IOBuffer()
116+
str = JSON.show_json(MyCustomWriteContext(JSON.Writer.CompactContext(io)), JSON.Serializations.StandardSerialization(), x)
117+
@test String(take!(io)) == "{\"id\":1,\"obj\":null}"
118+
end
119+
88120
# Check that printing to the default stdout doesn't fail

0 commit comments

Comments
 (0)