Skip to content

Commit 79afdb7

Browse files
ampagenttmm
andcommitted
fix(zero-swift): parse, write, compare, and hash deep JSON without recursion
JSONParser accepts 5,000 levels, but parsing recursed and overflowed a 512 KB stack (a macOS test or cooperative thread) at about 300 levels in a debug build. The parser, JSONSerializer, the query hash's visitValue, and JSONValue's hash now keep the arrays and objects they are in on an explicit stack. JSONValue's == recurses for the first 64 levels, which is faster, and goes on without recursion past them. Freeing a deep value still recurses (Array's and JSONObject's deinit): a 5,000-deep value fits in 1 MB in a release build, not in 512 KB. SweepWireTests no longer needs an 8 MB stack to parse; deepRowValue runs on 512 KB and frees on 8 MB. JSONDepthTests parses, writes, compares, and hashes 5,000-deep arrays and objects on a 512 KB thread. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
1 parent c494ff9 commit 79afdb7

8 files changed

Lines changed: 521 additions & 182 deletions

File tree

‎.changeset/json-parser-depth.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'zero-swift': patch
3+
---
4+
5+
JSON nested as deep as the parser accepts (5,000 levels) no longer overflows the stack when it is parsed, written, compared, hashed, or hashed as a custom query's arguments, even on a thread with a 512 KB stack, such as a test or cooperative thread on macOS. Before, a debug build overflowed at about 300 levels on such a thread. Freeing such a value still recurses: in a release build, a 5,000-deep value fits in a 1 MB stack, such as the iPhone main thread's, but not in 512 KB.

‎swift/Sources/Zero/Hash/QueryHash.swift‎

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,35 +133,83 @@ package enum QueryHash {
133133
extension QueryHash.State {
134134
typealias H = QueryHash
135135

136+
/// Visits `v` without recursion: the arrays and objects it is in are on
137+
/// a stack, so nesting takes heap, not stack.
136138
mutating func visitValue(_ v: JSONValue) {
139+
if visitScalar(v) { return }
140+
// An array, or an object's entries in `Object.keys(v).sort()` order,
141+
// and the index of the next value.
142+
var containers: [(entries: Entries, next: Int)] = []
143+
var value = v
144+
nested: while true {
145+
switch value {
146+
case .array(let a):
147+
mix(H.tagArr)
148+
containers.append((.array(a), 0))
149+
case .object(let o):
150+
mix(H.tagObj)
151+
// By UTF-16 code unit, whatever the order.
152+
containers.append((.object(o.sorted { StringCompare.utf16Less($0.key, $1.key) }), 0))
153+
default:
154+
preconditionFailure("not an array or object")
155+
}
156+
// Visits values until one is an array or object, ending the
157+
// containers that are done.
158+
while var container = containers.popLast() {
159+
while container.next < container.entries.count {
160+
let e: JSONValue
161+
switch container.entries {
162+
case .array(let a):
163+
e = a[container.next]
164+
case .object(let entries):
165+
mixString(entries[container.next].key)
166+
e = entries[container.next].value
167+
}
168+
container.next += 1
169+
if visitScalar(e) { continue }
170+
containers.append(container)
171+
value = e
172+
continue nested
173+
}
174+
mix(H.tagEnd)
175+
}
176+
return
177+
}
178+
}
179+
180+
enum Entries {
181+
case array([JSONValue])
182+
case object([(key: String, value: JSONValue)])
183+
184+
var count: Int {
185+
switch self {
186+
case .array(let a): a.count
187+
case .object(let o): o.count
188+
}
189+
}
190+
}
191+
192+
/// Visits `v` unless it is an array or object; returns whether it did.
193+
private mutating func visitScalar(_ v: JSONValue) -> Bool {
137194
switch v {
138195
case .string(let s):
139196
mixString(s)
140197
case .number(let n):
141198
// NaN and Infinity are null on the wire (JSON.stringify writes null).
142199
if !n.isFinite {
143200
mix(H.tagNull)
144-
return
201+
} else {
202+
mix(JSNumber.isInt32(n) ? H.tagInt : H.tagFloat)
203+
mixNumber(n)
145204
}
146-
mix(JSNumber.isInt32(n) ? H.tagInt : H.tagFloat)
147-
mixNumber(n)
148205
case .bool(let b):
149206
mix(b ? H.tagTrue : H.tagFalse)
150207
case .null:
151208
mix(H.tagNull)
152-
case .array(let a):
153-
mix(H.tagArr)
154-
for e in a { visitValue(e) }
155-
mix(H.tagEnd)
156-
case .object(let o):
157-
mix(H.tagObj)
158-
// `Object.keys(v).sort()`: by UTF-16 code unit, whatever the order.
159-
for (k, value) in o.sorted(by: { StringCompare.utf16Less($0.key, $1.key) }) {
160-
mixString(k)
161-
visitValue(value)
162-
}
163-
mix(H.tagEnd)
209+
case .array, .object:
210+
return false
164211
}
212+
return true
165213
}
166214

167215
mutating func visitOptionalString(_ s: String?) {

‎swift/Sources/Zero/JSON/JSONObject.swift‎

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -403,33 +403,19 @@ extension JSONObject: ExpressibleByDictionaryLiteral {
403403
extension JSONObject: Hashable {
404404
/// The same keys, by code unit, with equal values, in any order.
405405
public static func == (a: JSONObject, b: JSONObject) -> Bool {
406-
if a.storage === b.storage { return true }
407-
guard a.count == b.count else { return false }
408-
for i in 0..<a.count {
409-
let (key, value) = a[i]
410-
// Usually the keys are in the same order.
411-
if StringCompare.equal(key, b.storage.key(at: i)) {
412-
if value != b.storage.value(at: i) { return false }
413-
} else {
414-
guard let j = b.index(forKey: key), value == b.storage.value(at: j) else {
415-
return false
416-
}
417-
}
418-
}
419-
return true
406+
JSONValue.object(a) == .object(b)
420407
}
421408

409+
/// Whether the two share their storage, so are equal.
410+
func hasSameStorage(as other: JSONObject) -> Bool { storage === other.storage }
411+
412+
func key(at i: Int) -> String { storage.key(at: i) }
413+
414+
func value(at i: Int) -> JSONValue { storage.value(at: i) }
415+
422416
/// Independent of the order, as `==` is.
423417
public func hash(into hasher: inout Hasher) {
424-
var acc = 0
425-
for (key, value) in self {
426-
var h = Hasher()
427-
h.combine(JSKey(key))
428-
h.combine(value)
429-
acc ^= h.finalize()
430-
}
431-
hasher.combine(count)
432-
hasher.combine(acc)
418+
JSONValue.object(self).hash(into: &hasher)
433419
}
434420
}
435421

‎swift/Sources/Zero/JSON/JSONParser.swift‎

Lines changed: 109 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ package struct JSONParser {
3030
package static func parse(_ buffer: UnsafeBufferPointer<UInt8>) throws(Error) -> JSONValue {
3131
var p = JSONParser(buffer)
3232
p.skipWhitespace()
33-
let value = try p.parseValue(depth: 0)
33+
let value = try p.parseValue()
3434
p.skipWhitespace()
3535
if p.i != p.end { throw p.error("Unexpected non-whitespace character after JSON") }
3636
return value
@@ -57,89 +57,128 @@ package struct JSONParser {
5757
}
5858
}
5959

60-
private mutating func parseValue(depth: Int) throws(Error) -> JSONValue {
61-
guard i < end else { throw error("Unexpected end of JSON input") }
62-
switch b[i] {
63-
case UInt8(ascii: "{"): return try parseObject(depth: depth)
64-
case UInt8(ascii: "["): return try parseArray(depth: depth)
65-
case UInt8(ascii: "\""): return .string(try parseString())
66-
case UInt8(ascii: "t"): try expectLiteral("true"); return .bool(true)
67-
case UInt8(ascii: "f"): try expectLiteral("false"); return .bool(false)
68-
case UInt8(ascii: "n"): try expectLiteral("null"); return .null
69-
case UInt8(ascii: "-"), UInt8(ascii: "0")...UInt8(ascii: "9"): return .number(try parseNumber())
70-
default: throw error("Unexpected token")
71-
}
60+
/// An array or object that a value being parsed is in.
61+
private enum Container {
62+
case array([JSONValue])
63+
/// An object and the key of the value being parsed.
64+
case object(JSONObject, key: String)
7265
}
7366

74-
private mutating func expectLiteral(_ literal: StaticString) throws(Error) {
75-
let n = literal.utf8CodeUnitCount
76-
guard end - i >= n else { throw error("Unexpected end of JSON input") }
77-
let p = literal.utf8Start
78-
for k in 0..<n where b[i + k] != p[k] {
79-
throw error("Unexpected token")
80-
}
81-
i += n
82-
}
83-
84-
private mutating func parseObject(depth: Int) throws(Error) -> JSONValue {
85-
if depth >= Self.maxDepth { throw error("JSON nested too deeply") }
86-
i += 1 // {
87-
var object = JSONObject()
88-
skipWhitespace()
89-
if i < end && b[i] == UInt8(ascii: "}") {
90-
i += 1
91-
return .object(object)
92-
}
67+
/// Parses a value without recursion, so nesting takes heap, not stack.
68+
/// The innermost open array or object is `array` or `object` (with
69+
/// `key`); the ones it is in are on `parents`. They move between the two
70+
/// (`take()`), so each stays uniquely referenced and adding to it doesn't
71+
/// copy it.
72+
private mutating func parseValue() throws(Error) -> JSONValue {
73+
var parents: [Container] = []
74+
var array: [JSONValue]?
75+
var object: JSONObject?
76+
var key = ""
9377
while true {
94-
skipWhitespace()
9578
guard i < end else { throw error("Unexpected end of JSON input") }
96-
guard b[i] == UInt8(ascii: "\"") else { throw error("Expected property name") }
97-
let key = try parseString()
98-
skipWhitespace()
99-
guard i < end, b[i] == UInt8(ascii: ":") else {
100-
throw error("Expected ':' after property name")
101-
}
102-
i += 1
103-
skipWhitespace()
104-
object[key] = try parseValue(depth: depth + 1)
105-
skipWhitespace()
106-
guard i < end else { throw error("Unexpected end of JSON input") }
107-
if b[i] == UInt8(ascii: ",") {
79+
var value: JSONValue
80+
switch b[i] {
81+
case UInt8(ascii: "{"), UInt8(ascii: "["):
82+
let isObject = b[i] == UInt8(ascii: "{")
83+
if parents.count + (array == nil && object == nil ? 0 : 1) >= Self.maxDepth {
84+
throw error("JSON nested too deeply")
85+
}
10886
i += 1
87+
skipWhitespace()
88+
if i < end && b[i] == (isObject ? UInt8(ascii: "}") : UInt8(ascii: "]")) {
89+
i += 1
90+
value = isObject ? .object(JSONObject()) : .array([])
91+
break
92+
}
93+
if let a = array.take() {
94+
parents.append(.array(a))
95+
} else if let o = object.take() {
96+
parents.append(.object(o, key: key))
97+
}
98+
if isObject {
99+
object = JSONObject()
100+
key = try parseKey()
101+
} else {
102+
array = []
103+
}
109104
continue
105+
case UInt8(ascii: "\""): value = .string(try parseString())
106+
case UInt8(ascii: "t"): try expectLiteral("true"); value = .bool(true)
107+
case UInt8(ascii: "f"): try expectLiteral("false"); value = .bool(false)
108+
case UInt8(ascii: "n"): try expectLiteral("null"); value = .null
109+
case UInt8(ascii: "-"), UInt8(ascii: "0")...UInt8(ascii: "9"):
110+
value = .number(try parseNumber())
111+
default: throw error("Unexpected token")
110112
}
111-
if b[i] == UInt8(ascii: "}") {
112-
i += 1
113-
return .object(object)
113+
// Adds the value to its container, closing the containers that end
114+
// after it, until one has another value.
115+
while true {
116+
skipWhitespace()
117+
if array != nil {
118+
array!.append(value)
119+
guard i < end else { throw error("Unexpected end of JSON input") }
120+
if b[i] == UInt8(ascii: ",") {
121+
i += 1
122+
skipWhitespace()
123+
break
124+
}
125+
guard b[i] == UInt8(ascii: "]") else {
126+
throw error("Expected ',' or ']' after array element")
127+
}
128+
i += 1
129+
value = .array(array.take()!)
130+
} else if object != nil {
131+
// A repeated key keeps its first place and its last value.
132+
object![key] = value
133+
guard i < end else { throw error("Unexpected end of JSON input") }
134+
if b[i] == UInt8(ascii: ",") {
135+
i += 1
136+
key = try parseKey()
137+
break
138+
}
139+
guard b[i] == UInt8(ascii: "}") else {
140+
throw error("Expected ',' or '}' after property value")
141+
}
142+
i += 1
143+
value = .object(object.take()!)
144+
} else {
145+
return value
146+
}
147+
guard let parent = parents.popLast() else { continue }
148+
switch consume parent {
149+
case .array(let a):
150+
array = consume a
151+
case .object(let o, let k):
152+
object = consume o
153+
key = consume k
154+
}
114155
}
115-
throw error("Expected ',' or '}' after property value")
116156
}
117157
}
118158

119-
private mutating func parseArray(depth: Int) throws(Error) -> JSONValue {
120-
if depth >= Self.maxDepth { throw error("JSON nested too deeply") }
121-
i += 1 // [
122-
var array: [JSONValue] = []
159+
/// A property name and its colon, and the whitespace around them.
160+
private mutating func parseKey() throws(Error) -> String {
123161
skipWhitespace()
124-
if i < end && b[i] == UInt8(ascii: "]") {
125-
i += 1
126-
return .array(array)
162+
guard i < end else { throw error("Unexpected end of JSON input") }
163+
guard b[i] == UInt8(ascii: "\"") else { throw error("Expected property name") }
164+
let key = try parseString()
165+
skipWhitespace()
166+
guard i < end, b[i] == UInt8(ascii: ":") else {
167+
throw error("Expected ':' after property name")
127168
}
128-
while true {
129-
skipWhitespace()
130-
array.append(try parseValue(depth: depth + 1))
131-
skipWhitespace()
132-
guard i < end else { throw error("Unexpected end of JSON input") }
133-
if b[i] == UInt8(ascii: ",") {
134-
i += 1
135-
continue
136-
}
137-
if b[i] == UInt8(ascii: "]") {
138-
i += 1
139-
return .array(array)
140-
}
141-
throw error("Expected ',' or ']' after array element")
169+
i += 1
170+
skipWhitespace()
171+
return key
172+
}
173+
174+
private mutating func expectLiteral(_ literal: StaticString) throws(Error) {
175+
let n = literal.utf8CodeUnitCount
176+
guard end - i >= n else { throw error("Unexpected end of JSON input") }
177+
let p = literal.utf8Start
178+
for k in 0..<n where b[i + k] != p[k] {
179+
throw error("Unexpected token")
142180
}
181+
i += n
143182
}
144183

145184
private mutating func parseString() throws(Error) -> String {

0 commit comments

Comments
 (0)