Skip to content

Commit 3063535

Browse files
authored
Fix semantics of JSONUnkeyedDecodingContainer (#62)
The following behaviors are now handled correctly: - It is no longer possible for `self.count` or `self.isAtEnd` to accidentally get out of sync with reality, they are now computed. - Bounds are now correctly checked and a more useful `DecodingError` thrown (may have performance implications; removing this check is not strictly against protocol if so). - Decoding no longer incorrectly increments the current index if the decode fails for any reason. - `decodeNil()` no longer incorrectly increments the current index if the result is not `nil`.
1 parent e20673f commit 3063535

File tree

5 files changed

+222
-71
lines changed

5 files changed

+222
-71
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ on:
99

1010
jobs:
1111

12-
"sanity-Tests":
12+
"validity-Tests":
1313
runs-on: macOS-latest
1414
steps:
1515
- name: Checkout
1616
uses: actions/checkout@v2
1717
- name: Install swiftformat
1818
run: brew install swiftformat
19-
- name: Run sanity
20-
run: ./scripts/sanity.sh .
19+
- name: Run validity
20+
run: ./scripts/validity.sh .
2121

2222
"tuxOS-Tests":
2323
runs-on: ubuntu-latest

Sources/PureSwiftJSON/Decoding/JSONUnkeyedDecodingContainer.swift

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,19 @@ struct JSONUnkeyedDecodingContainer: UnkeyedDecodingContainer {
44
let codingPath: [CodingKey]
55
let array: [JSONValue]
66

7-
let count: Int? // protocol requirement to be optional
8-
var isAtEnd: Bool
7+
var count: Int? { self.array.count }
8+
var isAtEnd: Bool { self.currentIndex >= (self.count ?? 0) }
99
var currentIndex = 0
1010

1111
init(impl: JSONDecoderImpl, codingPath: [CodingKey], array: [JSONValue]) {
1212
self.impl = impl
1313
self.codingPath = codingPath
1414
self.array = array
15-
16-
self.isAtEnd = array.count == 0
17-
self.count = array.count
1815
}
1916

2017
mutating func decodeNil() throws -> Bool {
21-
if self.array[self.currentIndex] == .null {
22-
defer {
23-
currentIndex += 1
24-
if currentIndex == count {
25-
isAtEnd = true
26-
}
27-
}
18+
if try self.getNextValue(ofType: Never.self) == .null {
19+
self.currentIndex += 1
2820
return true
2921
}
3022

@@ -34,41 +26,31 @@ struct JSONUnkeyedDecodingContainer: UnkeyedDecodingContainer {
3426
}
3527

3628
mutating func decode(_ type: Bool.Type) throws -> Bool {
37-
defer {
38-
currentIndex += 1
39-
if currentIndex == count {
40-
isAtEnd = true
41-
}
42-
}
43-
44-
guard case .bool(let bool) = self.array[self.currentIndex] else {
45-
throw createTypeMismatchError(type: type, value: self.array[self.currentIndex])
29+
let value = try self.getNextValue(ofType: Bool.self)
30+
guard case .bool(let bool) = value else {
31+
throw createTypeMismatchError(type: type, value: value)
4632
}
4733

34+
self.currentIndex += 1
4835
return bool
4936
}
5037

5138
mutating func decode(_ type: String.Type) throws -> String {
52-
defer {
53-
currentIndex += 1
54-
if currentIndex == count {
55-
isAtEnd = true
56-
}
57-
}
58-
59-
guard case .string(let string) = self.array[self.currentIndex] else {
60-
throw createTypeMismatchError(type: type, value: self.array[self.currentIndex])
39+
let value = try self.getNextValue(ofType: String.self)
40+
guard case .string(let string) = value else {
41+
throw createTypeMismatchError(type: type, value: value)
6142
}
6243

44+
self.currentIndex += 1
6345
return string
6446
}
6547

6648
mutating func decode(_: Double.Type) throws -> Double {
67-
try decodeLosslessStringConvertible()
49+
try decodeBinaryFloatingPoint()
6850
}
6951

7052
mutating func decode(_: Float.Type) throws -> Float {
71-
try decodeLosslessStringConvertible()
53+
try decodeBinaryFloatingPoint()
7254
}
7355

7456
mutating func decode(_: Int.Type) throws -> Int {
@@ -112,20 +94,33 @@ struct JSONUnkeyedDecodingContainer: UnkeyedDecodingContainer {
11294
}
11395

11496
mutating func decode<T>(_: T.Type) throws -> T where T: Decodable {
115-
let decoder = try decoderForNextElement()
116-
return try T(from: decoder)
97+
let decoder = try decoderForNextElement(ofType: T.self)
98+
let result = try T(from: decoder)
99+
100+
// Because of the requirement that the index not be incremented unless
101+
// decoding the desired result type succeeds, it can not be a tail call.
102+
// Hopefully the compiler still optimizes well enough that the result
103+
// doesn't get copied around.
104+
self.currentIndex += 1
105+
return result
117106
}
118107

119108
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws
120109
-> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey
121110
{
122-
let decoder = try decoderForNextElement()
123-
return try decoder.container(keyedBy: type)
111+
let decoder = try decoderForNextElement(ofType: KeyedDecodingContainer<NestedKey>.self, isNested: true)
112+
let container = try decoder.container(keyedBy: type)
113+
114+
self.currentIndex += 1
115+
return container
124116
}
125117

126118
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
127-
let decoder = try decoderForNextElement()
128-
return try decoder.unkeyedContainer()
119+
let decoder = try decoderForNextElement(ofType: UnkeyedDecodingContainer.self, isNested: true)
120+
let container = try decoder.unkeyedContainer()
121+
122+
self.currentIndex += 1
123+
return container
129124
}
130125

131126
mutating func superDecoder() throws -> Decoder {
@@ -134,17 +129,9 @@ struct JSONUnkeyedDecodingContainer: UnkeyedDecodingContainer {
134129
}
135130

136131
extension JSONUnkeyedDecodingContainer {
137-
private mutating func decoderForNextElement() throws -> JSONDecoderImpl {
138-
defer {
139-
currentIndex += 1
140-
if currentIndex == count {
141-
isAtEnd = true
142-
}
143-
}
144-
145-
let value = self.array[self.currentIndex]
146-
var newPath = self.codingPath
147-
newPath.append(ArrayKey(index: self.currentIndex))
132+
private mutating func decoderForNextElement<T>(ofType: T.Type, isNested: Bool = false) throws -> JSONDecoderImpl {
133+
let value = try self.getNextValue(ofType: T.self, isNested: isNested)
134+
let newPath = self.codingPath + [ArrayKey(index: self.currentIndex)]
148135

149136
return JSONDecoderImpl(
150137
userInfo: self.impl.userInfo,
@@ -153,6 +140,34 @@ extension JSONUnkeyedDecodingContainer {
153140
)
154141
}
155142

143+
/// - Note: Instead of having the `isNested` parameter, it would have been quite nice to just check whether
144+
/// `T` conforms to either `KeyedDecodingContainer` or `UnkeyedDecodingContainer`. Unfortunately, since
145+
/// `KeyedDecodingContainer` takes a generic parameter (the `Key` type), we can't just ask if `T` is one, and
146+
/// type-erasure workarounds are not appropriate to this use case due to, among other things, the inability to
147+
/// conform most of the types that would matter. We also can't use `KeyedDecodingContainerProtocol` for the
148+
/// purpose, as it isn't even an existential and conformance to it can't be checked at runtime at all.
149+
///
150+
/// However, it's worth noting that the value of `isNested` is always a compile-time constant and the compiler
151+
/// can quite neatly remove whichever branch of the `if` is not taken during optimization, making doing it this
152+
/// way _much_ more performant (for what little it matters given that it's only checked in case of an error).
153+
@inline(__always)
154+
private func getNextValue<T>(ofType: T.Type, isNested: Bool = false) throws -> JSONValue {
155+
guard !self.isAtEnd else {
156+
if isNested {
157+
throw DecodingError.valueNotFound(T.self,
158+
.init(codingPath: self.codingPath,
159+
debugDescription: "Cannot get nested keyed container -- unkeyed container is at end.",
160+
underlyingError: nil))
161+
} else {
162+
throw DecodingError.valueNotFound(T.self,
163+
.init(codingPath: [ArrayKey(index: self.currentIndex)],
164+
debugDescription: "Unkeyed container is at end.",
165+
underlyingError: nil))
166+
}
167+
}
168+
return self.array[self.currentIndex]
169+
}
170+
156171
@inline(__always) private func createTypeMismatchError(type: Any.Type, value: JSONValue) -> DecodingError {
157172
let codingPath = self.codingPath + [ArrayKey(index: self.currentIndex)]
158173
return DecodingError.typeMismatch(type, .init(
@@ -161,42 +176,32 @@ extension JSONUnkeyedDecodingContainer {
161176
}
162177

163178
@inline(__always) private mutating func decodeFixedWidthInteger<T: FixedWidthInteger>() throws -> T {
164-
defer {
165-
currentIndex += 1
166-
if currentIndex == count {
167-
isAtEnd = true
168-
}
169-
}
170-
171-
guard case .number(let number) = self.array[self.currentIndex] else {
172-
throw self.createTypeMismatchError(type: T.self, value: self.array[self.currentIndex])
179+
let value = try self.getNextValue(ofType: T.self)
180+
guard case .number(let number) = value else {
181+
throw self.createTypeMismatchError(type: T.self, value: value)
173182
}
174183

175184
guard let integer = T(number) else {
176185
throw DecodingError.dataCorruptedError(in: self,
177186
debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self).")
178187
}
179188

189+
self.currentIndex += 1
180190
return integer
181191
}
182192

183-
@inline(__always) private mutating func decodeLosslessStringConvertible<T: LosslessStringConvertible>() throws -> T {
184-
defer {
185-
currentIndex += 1
186-
if currentIndex == count {
187-
isAtEnd = true
188-
}
189-
}
190-
191-
guard case .number(let number) = self.array[self.currentIndex] else {
192-
throw self.createTypeMismatchError(type: T.self, value: self.array[self.currentIndex])
193+
@inline(__always) private mutating func decodeBinaryFloatingPoint<T: LosslessStringConvertible>() throws -> T {
194+
let value = try self.getNextValue(ofType: T.self)
195+
guard case .number(let number) = value else {
196+
throw self.createTypeMismatchError(type: T.self, value: value)
193197
}
194198

195199
guard let float = T(number) else {
196200
throw DecodingError.dataCorruptedError(in: self,
197201
debugDescription: "Parsed JSON number <\(number)> does not fit in \(T.self).")
198202
}
199203

204+
self.currentIndex += 1
200205
return float
201206
}
202207
}

Tests/LearningTests/FoundationJSONDecoderTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,73 @@ class FoundationJSONDecoderTests: XCTestCase {
268268
XCTFail("Unexpected error: \(error)")
269269
}
270270
}
271+
272+
func testDecodePastEndOfUnkeyedContainer() {
273+
struct HelloWorld: Decodable {
274+
enum CodingKeys: String, CodingKey { case list }
275+
276+
init(from decoder: Decoder) throws {
277+
var container = try decoder.container(keyedBy: CodingKeys.self).nestedUnkeyedContainer(forKey: .list)
278+
_ = try container.decode(String.self)
279+
}
280+
}
281+
282+
let json = #"{"list":[]}"#
283+
284+
XCTAssertThrowsError(_ = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
285+
guard case .valueNotFound(let type, let context) = (error as? DecodingError) else {
286+
return XCTFail("Unexpected error: \(error)")
287+
}
288+
XCTAssertTrue(type is String.Type)
289+
XCTAssertEqual(context.codingPath.count, 1)
290+
XCTAssertEqual(context.codingPath.first?.intValue, 0)
291+
XCTAssertEqual(context.debugDescription, "Unkeyed container is at end.")
292+
}
293+
}
294+
295+
func testDecodeNestedKeyedContainerPastEndOfUnkeyedContainer() {
296+
struct HelloWorld: Decodable {
297+
enum CodingKeys: String, CodingKey { case list }
298+
299+
init(from decoder: Decoder) throws {
300+
var container = try decoder.container(keyedBy: CodingKeys.self).nestedUnkeyedContainer(forKey: .list)
301+
_ = try container.nestedContainer(keyedBy: CodingKeys.self)
302+
}
303+
}
304+
305+
let json = #"{"list":[]}"#
306+
307+
XCTAssertThrowsError(_ = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
308+
guard case .some(.valueNotFound(let type, let context)) = (error as? DecodingError) else {
309+
return XCTFail("Unexpected error: \(error)")
310+
}
311+
XCTAssertTrue(type is KeyedDecodingContainer<HelloWorld.CodingKeys>.Type)
312+
XCTAssertEqual(context.codingPath.count, 1)
313+
XCTAssertEqual(context.codingPath.first?.stringValue, HelloWorld.CodingKeys.list.rawValue)
314+
XCTAssertEqual(context.debugDescription, "Cannot get nested keyed container -- unkeyed container is at end.")
315+
}
316+
}
317+
318+
func testDecodeNestedUnkeyedContainerPastEndOfUnkeyedContainer() {
319+
struct HelloWorld: Decodable {
320+
enum CodingKeys: String, CodingKey { case list }
321+
322+
init(from decoder: Decoder) throws {
323+
var container = try decoder.container(keyedBy: CodingKeys.self).nestedUnkeyedContainer(forKey: .list)
324+
_ = try container.nestedUnkeyedContainer()
325+
}
326+
}
327+
328+
let json = #"{"list":[]}"#
329+
330+
XCTAssertThrowsError(_ = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
331+
guard case .some(.valueNotFound(let type, let context)) = (error as? DecodingError) else {
332+
return XCTFail("Unexpected error: \(error)")
333+
}
334+
XCTAssertTrue(type is UnkeyedDecodingContainer.Protocol)
335+
XCTAssertEqual(context.codingPath.count, 1)
336+
XCTAssertEqual(context.codingPath.first?.stringValue, HelloWorld.CodingKeys.list.rawValue)
337+
XCTAssertEqual(context.debugDescription, "Cannot get nested keyed container -- unkeyed container is at end.")
338+
}
339+
}
271340
}

0 commit comments

Comments
 (0)