Skip to content

Commit a5c49df

Browse files
committed
Fix encoding escaping bugs (#66)
1 parent 41e7946 commit a5c49df

File tree

12 files changed

+247
-114
lines changed

12 files changed

+247
-114
lines changed

.github/workflows/ci.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
"tuxOS-Tests":
2323
runs-on: ubuntu-latest
2424
strategy:
25+
fail-fast: false
2526
matrix:
2627
images:
2728
- swift:5.1
@@ -47,6 +48,7 @@ jobs:
4748
"tuxOS-Performance-Tests":
4849
runs-on: ubuntu-latest
4950
strategy:
51+
fail-fast: false
5052
matrix:
5153
images:
5254
- swift:5.1
@@ -68,6 +70,7 @@ jobs:
6870
"tuxOS-Integration-Tests":
6971
runs-on: ubuntu-latest
7072
strategy:
73+
fail-fast: false
7174
matrix:
7275
images:
7376
- swift:5.1
@@ -88,11 +91,12 @@ jobs:
8891
"macOS-Tests":
8992
runs-on: macOS-latest
9093
strategy:
94+
fail-fast: false
9195
matrix:
9296
xcode:
9397
- Xcode_11.1.app
9498
- Xcode_11.6.app
95-
- Xcode_12.app
99+
- Xcode_12.2.app
96100
steps:
97101
- name: Checkout
98102
uses: actions/checkout@v2
@@ -112,11 +116,12 @@ jobs:
112116
"macOS-Performance-Tests":
113117
runs-on: macOS-latest
114118
strategy:
119+
fail-fast: false
115120
matrix:
116121
xcode:
117122
- Xcode_11.1.app
118123
- Xcode_11.6.app
119-
- Xcode_12.app
124+
- Xcode_12.2.app
120125
steps:
121126
- name: Checkout
122127
uses: actions/checkout@v2

Sources/PureSwiftJSON/JSONValue.swift

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,52 @@ extension JSONValue {
8383
// quotation marks, except for the characters that MUST be escaped:
8484
// quotation mark, reverse solidus, and the control characters (U+0000
8585
// through U+001F).
86-
// https://tools.ietf.org/html/rfc7159#section-7
86+
// https://tools.ietf.org/html/rfc8259#section-7
8787

8888
// copy the current range over
8989
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
90-
bytes.append(UInt8(ascii: "\\"))
91-
bytes.append(stringBytes[nextIndex])
90+
switch stringBytes[nextIndex] {
91+
case UInt8(ascii: "\""): // quotation mark
92+
bytes.append(UInt8(ascii: "\\"))
93+
bytes.append(UInt8(ascii: "\""))
94+
case UInt8(ascii: "\\"): // reverse solidus
95+
bytes.append(UInt8(ascii: "\\"))
96+
bytes.append(UInt8(ascii: "\\"))
97+
case 0x08: // backspace
98+
bytes.append(UInt8(ascii: "\\"))
99+
bytes.append(UInt8(ascii: "b"))
100+
case 0x0C: // form feed
101+
bytes.append(UInt8(ascii: "\\"))
102+
bytes.append(UInt8(ascii: "f"))
103+
case 0x0A: // line feed
104+
bytes.append(UInt8(ascii: "\\"))
105+
bytes.append(UInt8(ascii: "n"))
106+
case 0x0D: // carriage return
107+
bytes.append(UInt8(ascii: "\\"))
108+
bytes.append(UInt8(ascii: "r"))
109+
case 0x09: // tab
110+
bytes.append(UInt8(ascii: "\\"))
111+
bytes.append(UInt8(ascii: "t"))
112+
default:
113+
func valueToAscii(_ value: UInt8) -> UInt8 {
114+
switch value {
115+
case 0 ... 9:
116+
return value + UInt8(ascii: "0")
117+
case 10 ... 15:
118+
return value - 10 + UInt8(ascii: "A")
119+
default:
120+
preconditionFailure()
121+
}
122+
}
123+
bytes.append(UInt8(ascii: "\\"))
124+
bytes.append(UInt8(ascii: "u"))
125+
bytes.append(UInt8(ascii: "0"))
126+
bytes.append(UInt8(ascii: "0"))
127+
let first = stringBytes[nextIndex] / 16
128+
let remaining = stringBytes[nextIndex] % 16
129+
bytes.append(valueToAscii(first))
130+
bytes.append(valueToAscii(remaining))
131+
}
92132

93133
nextIndex = stringBytes.index(after: nextIndex)
94134
startCopyIndex = nextIndex

Sources/PureSwiftJSON/Parsing/DocumentReader.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
// if we have a high surrogate we expect a low surrogate next
144144
let highSurrogateBitPattern = bitPattern
145145
guard let (escapeChar, _) = read(),
146-
let (uChar, _) = read()
146+
let (uChar, _) = read()
147147
else {
148148
throw JSONError.unexpectedEndOfFile
149149
}
@@ -180,20 +180,20 @@
180180
}
181181

182182
@inlinable mutating func parseUnicodeHexSequence() throws -> UInt16 {
183-
// As stated in RFC-7159 an escaped unicode character is 4 HEXDIGITs long
184-
// https://tools.ietf.org/html/rfc7159#section-7
183+
// As stated in RFC-8259 an escaped unicode character is 4 HEXDIGITs long
184+
// https://tools.ietf.org/html/rfc8259#section-7
185185
guard let (firstHex, startIndex) = read(),
186-
let (secondHex, _) = read(),
187-
let (thirdHex, _) = read(),
188-
let (forthHex, _) = read()
186+
let (secondHex, _) = read(),
187+
let (thirdHex, _) = read(),
188+
let (forthHex, _) = read()
189189
else {
190190
throw JSONError.unexpectedEndOfFile
191191
}
192192

193193
guard let first = DocumentReader.hexAsciiTo4Bits(firstHex),
194-
let second = DocumentReader.hexAsciiTo4Bits(secondHex),
195-
let third = DocumentReader.hexAsciiTo4Bits(thirdHex),
196-
let forth = DocumentReader.hexAsciiTo4Bits(forthHex)
194+
let second = DocumentReader.hexAsciiTo4Bits(secondHex),
195+
let third = DocumentReader.hexAsciiTo4Bits(thirdHex),
196+
let forth = DocumentReader.hexAsciiTo4Bits(forthHex)
197197
else {
198198
let hexString = String(decoding: [firstHex, secondHex, thirdHex, forthHex], as: Unicode.UTF8.self)
199199
throw JSONError.invalidHexDigitSequence(hexString, index: startIndex)

Sources/PureSwiftJSON/Parsing/JSONParser.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ public struct JSONParser {
9292

9393
mutating func parseNull() throws {
9494
guard self.reader.read()?.0 == UInt8(ascii: "u"),
95-
self.reader.read()?.0 == UInt8(ascii: "l"),
96-
self.reader.read()?.0 == UInt8(ascii: "l")
95+
self.reader.read()?.0 == UInt8(ascii: "l"),
96+
self.reader.read()?.0 == UInt8(ascii: "l")
9797
else {
9898
guard let value = reader.value else {
9999
throw JSONError.unexpectedEndOfFile
@@ -109,8 +109,8 @@ public struct JSONParser {
109109
switch self.reader.value {
110110
case UInt8(ascii: "t"):
111111
guard self.reader.read()?.0 == UInt8(ascii: "r"),
112-
self.reader.read()?.0 == UInt8(ascii: "u"),
113-
self.reader.read()?.0 == UInt8(ascii: "e")
112+
self.reader.read()?.0 == UInt8(ascii: "u"),
113+
self.reader.read()?.0 == UInt8(ascii: "e")
114114
else {
115115
guard let value = reader.value else {
116116
throw JSONError.unexpectedEndOfFile
@@ -122,9 +122,9 @@ public struct JSONParser {
122122
return true
123123
case UInt8(ascii: "f"):
124124
guard self.reader.read()?.0 == UInt8(ascii: "a"),
125-
self.reader.read()?.0 == UInt8(ascii: "l"),
126-
self.reader.read()?.0 == UInt8(ascii: "s"),
127-
self.reader.read()?.0 == UInt8(ascii: "e")
125+
self.reader.read()?.0 == UInt8(ascii: "l"),
126+
self.reader.read()?.0 == UInt8(ascii: "s"),
127+
self.reader.read()?.0 == UInt8(ascii: "e")
128128
else {
129129
guard let value = reader.value else {
130130
throw JSONError.unexpectedEndOfFile
@@ -207,7 +207,7 @@ public struct JSONParser {
207207

208208
case UInt8(ascii: "e"), UInt8(ascii: "E"):
209209
guard numbersSinceControlChar > 0,
210-
pastControlChar == .operand || pastControlChar == .decimalPoint
210+
pastControlChar == .operand || pastControlChar == .decimalPoint
211211
else {
212212
throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: index)
213213
}

Tests/LearningTests/FoundationJSONDecoderTests.swift

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ class FoundationJSONDecoderTests: XCTestCase {
1010
}
1111
}
1212

13-
do {
14-
let json = #"{"hello":"world"}"#
15-
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
16-
XCTFail("Did not expect to get a result: \(result)")
17-
} catch Swift.DecodingError.typeMismatch(let type, let context) {
18-
// expected
13+
let json = #"{"hello":"world"}"#
14+
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
15+
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
16+
return XCTFail("Unexpected error: \(error)")
17+
}
1918
XCTAssertTrue(type == [Any].self)
20-
print(context)
21-
} catch {
22-
XCTFail("Unexpected error: \(error)")
2319
}
2420
}
2521

@@ -35,16 +31,12 @@ class FoundationJSONDecoderTests: XCTestCase {
3531
}
3632
}
3733

38-
do {
39-
let json = #"["haha", "hihi"]"#
40-
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
41-
XCTFail("Did not expect to get a result: \(result)")
42-
} catch Swift.DecodingError.typeMismatch(let type, let context) {
43-
// expected
34+
let json = #"["haha", "hihi"]"#
35+
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
36+
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
37+
return XCTFail("Unexpected error: \(error)")
38+
}
4439
XCTAssertTrue(type == [String: Any].self)
45-
print(context)
46-
} catch {
47-
XCTFail("Unexpected error: \(error)")
4840
}
4941
}
5042

@@ -62,16 +54,12 @@ class FoundationJSONDecoderTests: XCTestCase {
6254
}
6355
}
6456

65-
do {
66-
let json = #""haha""#
67-
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
68-
XCTFail("Did not expect to get a result: \(result)")
69-
} catch Swift.DecodingError.typeMismatch(let type, let context) {
70-
// expected
57+
let json = #""haha""#
58+
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
59+
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
60+
return XCTFail("Unexpected error: \(error)")
61+
}
7162
XCTAssertTrue(type == [String: Any].self)
72-
print(context)
73-
} catch {
74-
XCTFail("Unexpected error: \(error)")
7563
}
7664
}
7765
#endif
@@ -89,16 +77,12 @@ class FoundationJSONDecoderTests: XCTestCase {
8977
}
9078
}
9179

92-
do {
93-
let json = #"{"hello": 12}"#
94-
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
95-
XCTFail("Did not expect to get a result: \(result)")
96-
} catch Swift.DecodingError.typeMismatch(let type, let context) {
97-
// expected
80+
let json = #"{"hello": 12}"#
81+
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
82+
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
83+
return XCTFail("Unexpected error: \(error)")
84+
}
9885
XCTAssertTrue(type == String.self)
99-
print(context)
100-
} catch {
101-
XCTFail("Unexpected error: \(error)")
10286
}
10387
}
10488

@@ -115,16 +99,14 @@ class FoundationJSONDecoderTests: XCTestCase {
11599
}
116100
}
117101

118-
do {
119-
let json = "{}"
120-
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
121-
XCTFail("Did not expect to get a result: \(result)")
122-
} catch Swift.DecodingError.keyNotFound(let codingKey, let context) {
123-
// expected
102+
let json = "{}"
103+
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
104+
guard case Swift.DecodingError.keyNotFound(let codingKey, let context) = error else {
105+
return XCTFail("Unexpected error: \(error)")
106+
}
107+
124108
XCTAssertEqual(codingKey as? HelloWorld.CodingKeys, .hello)
125109
XCTAssertEqual(context.debugDescription, "No value associated with key CodingKeys(stringValue: \"hello\", intValue: nil) (\"hello\").")
126-
} catch {
127-
XCTFail("Unexpected error: \(error)")
128110
}
129111
}
130112

Tests/LearningTests/FoundationJSONEncoderTests.swift

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,44 +22,39 @@ class FoundationJSONEncoderTests: XCTestCase {
2222
]))
2323
}
2424

25-
#if canImport(Darwin)
25+
#if swift(>=5.2) || canImport(Darwin)
2626
// this works only on Darwin, on Linux an error is thrown.
2727
func testEncodeNull() throws {
2828
let result = try Foundation.JSONEncoder().encode(nil as String?)
2929

3030
let json = String(data: result, encoding: .utf8)
3131
XCTAssertEqual(json, "null")
3232
}
33-
#endif
3433

35-
#if canImport(Darwin)
3634
// this works only on Darwin, on Linux an error is thrown.
37-
func testEncodeTopLevelString() throws {
38-
let result = try Foundation.JSONEncoder().encode("Hello World")
35+
func testEncodeTopLevelString() {
36+
var result: Data?
37+
XCTAssertNoThrow(result = try Foundation.JSONEncoder().encode("Hello World"))
3938

40-
let json = String(data: result, encoding: .utf8)
41-
XCTAssertEqual(json, #""Hello World""#)
39+
XCTAssertEqual(try String(data: XCTUnwrap(result), encoding: .utf8), #""Hello World""#)
4240
}
4341
#endif
4442

45-
func testEncodeTopLevelDoubleNaN() throws {
46-
do {
47-
_ = try Foundation.JSONEncoder().encode(Double.nan)
48-
} catch Swift.EncodingError.invalidValue(let value as Double, _) {
49-
XCTAssert(value.isNaN) // expected
50-
} catch {
51-
XCTFail("Unexpected error: \(error)")
43+
func testEncodeTopLevelDoubleNaN() {
44+
XCTAssertThrowsError(try Foundation.JSONEncoder().encode(Double.nan)) { error in
45+
guard case Swift.EncodingError.invalidValue(let value as Double, _) = error else {
46+
return XCTFail("Unexpected error: \(error)")
47+
}
48+
XCTAssert(value.isNaN)
5249
}
5350
}
5451

55-
func testEncodeTopLevelDoubleInfinity() throws {
56-
do {
57-
_ = try Foundation.JSONEncoder().encode(Double.infinity)
58-
} catch Swift.EncodingError.invalidValue(let value as Double, let context) {
59-
print(context)
60-
XCTAssert(value.isInfinite) // expected
61-
} catch {
62-
XCTFail("Unexpected error: \(error)")
52+
func testEncodeTopLevelDoubleInfinity() {
53+
XCTAssertThrowsError(try Foundation.JSONEncoder().encode(Double.infinity)) { error in
54+
guard case Swift.EncodingError.invalidValue(let value as Double, _) = error else {
55+
return XCTFail("Unexpected error: \(error)")
56+
}
57+
XCTAssert(value.isInfinite)
6358
}
6459
}
6560

@@ -100,6 +95,29 @@ class FoundationJSONEncoderTests: XCTestCase {
10095
XCTFail("Unexpected error: \(error)")
10196
}
10297
}
98+
99+
#if swift(>=5.2) || canImport(Darwin)
100+
func testEncodeLineFeed() {
101+
let input = String(decoding: [10], as: Unicode.UTF8.self)
102+
var result: Data?
103+
XCTAssertNoThrow(result = try JSONEncoder().encode(input))
104+
XCTAssertEqual(try Array(XCTUnwrap(result)), [34, 92, 110, 34])
105+
}
106+
107+
func testEncodeBackspace() {
108+
let input = String(decoding: [08], as: Unicode.UTF8.self)
109+
var result: Data?
110+
XCTAssertNoThrow(result = try JSONEncoder().encode(input))
111+
XCTAssertEqual(try Array(XCTUnwrap(result)), [34, 92, 98, 34])
112+
}
113+
114+
func testEncodeCarriageReturn() {
115+
let input = String(decoding: [13], as: Unicode.UTF8.self)
116+
var result: Data?
117+
XCTAssertNoThrow(result = try JSONEncoder().encode(input))
118+
XCTAssertEqual(try Array(XCTUnwrap(result)), [34, 92, 114, 34])
119+
}
120+
#endif
103121
}
104122

105123
extension FoundationJSONEncoderTests.HelloWorld.SubType: Encodable {

0 commit comments

Comments
 (0)