Skip to content

Commit

Permalink
Fix encoding escaping bugs (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabianfett committed Nov 1, 2020
1 parent 41e7946 commit a5c49df
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 114 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
"tuxOS-Tests":
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
images:
- swift:5.1
Expand All @@ -47,6 +48,7 @@ jobs:
"tuxOS-Performance-Tests":
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
images:
- swift:5.1
Expand All @@ -68,6 +70,7 @@ jobs:
"tuxOS-Integration-Tests":
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
images:
- swift:5.1
Expand All @@ -88,11 +91,12 @@ jobs:
"macOS-Tests":
runs-on: macOS-latest
strategy:
fail-fast: false
matrix:
xcode:
- Xcode_11.1.app
- Xcode_11.6.app
- Xcode_12.app
- Xcode_12.2.app
steps:
- name: Checkout
uses: actions/checkout@v2
Expand All @@ -112,11 +116,12 @@ jobs:
"macOS-Performance-Tests":
runs-on: macOS-latest
strategy:
fail-fast: false
matrix:
xcode:
- Xcode_11.1.app
- Xcode_11.6.app
- Xcode_12.app
- Xcode_12.2.app
steps:
- name: Checkout
uses: actions/checkout@v2
Expand Down
46 changes: 43 additions & 3 deletions Sources/PureSwiftJSON/JSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,52 @@ extension JSONValue {
// quotation marks, except for the characters that MUST be escaped:
// quotation mark, reverse solidus, and the control characters (U+0000
// through U+001F).
// https://tools.ietf.org/html/rfc7159#section-7
// https://tools.ietf.org/html/rfc8259#section-7

// copy the current range over
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
bytes.append(UInt8(ascii: "\\"))
bytes.append(stringBytes[nextIndex])
switch stringBytes[nextIndex] {
case UInt8(ascii: "\""): // quotation mark
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "\""))
case UInt8(ascii: "\\"): // reverse solidus
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "\\"))
case 0x08: // backspace
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "b"))
case 0x0C: // form feed
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "f"))
case 0x0A: // line feed
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "n"))
case 0x0D: // carriage return
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "r"))
case 0x09: // tab
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "t"))
default:
func valueToAscii(_ value: UInt8) -> UInt8 {
switch value {
case 0 ... 9:
return value + UInt8(ascii: "0")
case 10 ... 15:
return value - 10 + UInt8(ascii: "A")
default:
preconditionFailure()
}
}
bytes.append(UInt8(ascii: "\\"))
bytes.append(UInt8(ascii: "u"))
bytes.append(UInt8(ascii: "0"))
bytes.append(UInt8(ascii: "0"))
let first = stringBytes[nextIndex] / 16
let remaining = stringBytes[nextIndex] % 16
bytes.append(valueToAscii(first))
bytes.append(valueToAscii(remaining))
}

nextIndex = stringBytes.index(after: nextIndex)
startCopyIndex = nextIndex
Expand Down
18 changes: 9 additions & 9 deletions Sources/PureSwiftJSON/Parsing/DocumentReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
// if we have a high surrogate we expect a low surrogate next
let highSurrogateBitPattern = bitPattern
guard let (escapeChar, _) = read(),
let (uChar, _) = read()
let (uChar, _) = read()
else {
throw JSONError.unexpectedEndOfFile
}
Expand Down Expand Up @@ -180,20 +180,20 @@
}

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

guard let first = DocumentReader.hexAsciiTo4Bits(firstHex),
let second = DocumentReader.hexAsciiTo4Bits(secondHex),
let third = DocumentReader.hexAsciiTo4Bits(thirdHex),
let forth = DocumentReader.hexAsciiTo4Bits(forthHex)
let second = DocumentReader.hexAsciiTo4Bits(secondHex),
let third = DocumentReader.hexAsciiTo4Bits(thirdHex),
let forth = DocumentReader.hexAsciiTo4Bits(forthHex)
else {
let hexString = String(decoding: [firstHex, secondHex, thirdHex, forthHex], as: Unicode.UTF8.self)
throw JSONError.invalidHexDigitSequence(hexString, index: startIndex)
Expand Down
16 changes: 8 additions & 8 deletions Sources/PureSwiftJSON/Parsing/JSONParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ public struct JSONParser {

mutating func parseNull() throws {
guard self.reader.read()?.0 == UInt8(ascii: "u"),
self.reader.read()?.0 == UInt8(ascii: "l"),
self.reader.read()?.0 == UInt8(ascii: "l")
self.reader.read()?.0 == UInt8(ascii: "l"),
self.reader.read()?.0 == UInt8(ascii: "l")
else {
guard let value = reader.value else {
throw JSONError.unexpectedEndOfFile
Expand All @@ -109,8 +109,8 @@ public struct JSONParser {
switch self.reader.value {
case UInt8(ascii: "t"):
guard self.reader.read()?.0 == UInt8(ascii: "r"),
self.reader.read()?.0 == UInt8(ascii: "u"),
self.reader.read()?.0 == UInt8(ascii: "e")
self.reader.read()?.0 == UInt8(ascii: "u"),
self.reader.read()?.0 == UInt8(ascii: "e")
else {
guard let value = reader.value else {
throw JSONError.unexpectedEndOfFile
Expand All @@ -122,9 +122,9 @@ public struct JSONParser {
return true
case UInt8(ascii: "f"):
guard self.reader.read()?.0 == UInt8(ascii: "a"),
self.reader.read()?.0 == UInt8(ascii: "l"),
self.reader.read()?.0 == UInt8(ascii: "s"),
self.reader.read()?.0 == UInt8(ascii: "e")
self.reader.read()?.0 == UInt8(ascii: "l"),
self.reader.read()?.0 == UInt8(ascii: "s"),
self.reader.read()?.0 == UInt8(ascii: "e")
else {
guard let value = reader.value else {
throw JSONError.unexpectedEndOfFile
Expand Down Expand Up @@ -207,7 +207,7 @@ public struct JSONParser {

case UInt8(ascii: "e"), UInt8(ascii: "E"):
guard numbersSinceControlChar > 0,
pastControlChar == .operand || pastControlChar == .decimalPoint
pastControlChar == .operand || pastControlChar == .decimalPoint
else {
throw JSONError.unexpectedCharacter(ascii: byte, characterIndex: index)
}
Expand Down
70 changes: 26 additions & 44 deletions Tests/LearningTests/FoundationJSONDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@ class FoundationJSONDecoderTests: XCTestCase {
}
}

do {
let json = #"{"hello":"world"}"#
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
XCTFail("Did not expect to get a result: \(result)")
} catch Swift.DecodingError.typeMismatch(let type, let context) {
// expected
let json = #"{"hello":"world"}"#
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
return XCTFail("Unexpected error: \(error)")
}
XCTAssertTrue(type == [Any].self)
print(context)
} catch {
XCTFail("Unexpected error: \(error)")
}
}

Expand All @@ -35,16 +31,12 @@ class FoundationJSONDecoderTests: XCTestCase {
}
}

do {
let json = #"["haha", "hihi"]"#
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
XCTFail("Did not expect to get a result: \(result)")
} catch Swift.DecodingError.typeMismatch(let type, let context) {
// expected
let json = #"["haha", "hihi"]"#
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
return XCTFail("Unexpected error: \(error)")
}
XCTAssertTrue(type == [String: Any].self)
print(context)
} catch {
XCTFail("Unexpected error: \(error)")
}
}

Expand All @@ -62,16 +54,12 @@ class FoundationJSONDecoderTests: XCTestCase {
}
}

do {
let json = #""haha""#
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
XCTFail("Did not expect to get a result: \(result)")
} catch Swift.DecodingError.typeMismatch(let type, let context) {
// expected
let json = #""haha""#
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
return XCTFail("Unexpected error: \(error)")
}
XCTAssertTrue(type == [String: Any].self)
print(context)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
#endif
Expand All @@ -89,16 +77,12 @@ class FoundationJSONDecoderTests: XCTestCase {
}
}

do {
let json = #"{"hello": 12}"#
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
XCTFail("Did not expect to get a result: \(result)")
} catch Swift.DecodingError.typeMismatch(let type, let context) {
// expected
let json = #"{"hello": 12}"#
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
guard case Swift.DecodingError.typeMismatch(let type, _) = error else {
return XCTFail("Unexpected error: \(error)")
}
XCTAssertTrue(type == String.self)
print(context)
} catch {
XCTFail("Unexpected error: \(error)")
}
}

Expand All @@ -115,16 +99,14 @@ class FoundationJSONDecoderTests: XCTestCase {
}
}

do {
let json = "{}"
let result = try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)
XCTFail("Did not expect to get a result: \(result)")
} catch Swift.DecodingError.keyNotFound(let codingKey, let context) {
// expected
let json = "{}"
XCTAssertThrowsError(try JSONDecoder().decode(HelloWorld.self, from: json.data(using: .utf8)!)) { error in
guard case Swift.DecodingError.keyNotFound(let codingKey, let context) = error else {
return XCTFail("Unexpected error: \(error)")
}

XCTAssertEqual(codingKey as? HelloWorld.CodingKeys, .hello)
XCTAssertEqual(context.debugDescription, "No value associated with key CodingKeys(stringValue: \"hello\", intValue: nil) (\"hello\").")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

Expand Down
62 changes: 40 additions & 22 deletions Tests/LearningTests/FoundationJSONEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,44 +22,39 @@ class FoundationJSONEncoderTests: XCTestCase {
]))
}

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

let json = String(data: result, encoding: .utf8)
XCTAssertEqual(json, "null")
}
#endif

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

let json = String(data: result, encoding: .utf8)
XCTAssertEqual(json, #""Hello World""#)
XCTAssertEqual(try String(data: XCTUnwrap(result), encoding: .utf8), #""Hello World""#)
}
#endif

func testEncodeTopLevelDoubleNaN() throws {
do {
_ = try Foundation.JSONEncoder().encode(Double.nan)
} catch Swift.EncodingError.invalidValue(let value as Double, _) {
XCTAssert(value.isNaN) // expected
} catch {
XCTFail("Unexpected error: \(error)")
func testEncodeTopLevelDoubleNaN() {
XCTAssertThrowsError(try Foundation.JSONEncoder().encode(Double.nan)) { error in
guard case Swift.EncodingError.invalidValue(let value as Double, _) = error else {
return XCTFail("Unexpected error: \(error)")
}
XCTAssert(value.isNaN)
}
}

func testEncodeTopLevelDoubleInfinity() throws {
do {
_ = try Foundation.JSONEncoder().encode(Double.infinity)
} catch Swift.EncodingError.invalidValue(let value as Double, let context) {
print(context)
XCTAssert(value.isInfinite) // expected
} catch {
XCTFail("Unexpected error: \(error)")
func testEncodeTopLevelDoubleInfinity() {
XCTAssertThrowsError(try Foundation.JSONEncoder().encode(Double.infinity)) { error in
guard case Swift.EncodingError.invalidValue(let value as Double, _) = error else {
return XCTFail("Unexpected error: \(error)")
}
XCTAssert(value.isInfinite)
}
}

Expand Down Expand Up @@ -100,6 +95,29 @@ class FoundationJSONEncoderTests: XCTestCase {
XCTFail("Unexpected error: \(error)")
}
}

#if swift(>=5.2) || canImport(Darwin)
func testEncodeLineFeed() {
let input = String(decoding: [10], as: Unicode.UTF8.self)
var result: Data?
XCTAssertNoThrow(result = try JSONEncoder().encode(input))
XCTAssertEqual(try Array(XCTUnwrap(result)), [34, 92, 110, 34])
}

func testEncodeBackspace() {
let input = String(decoding: [08], as: Unicode.UTF8.self)
var result: Data?
XCTAssertNoThrow(result = try JSONEncoder().encode(input))
XCTAssertEqual(try Array(XCTUnwrap(result)), [34, 92, 98, 34])
}

func testEncodeCarriageReturn() {
let input = String(decoding: [13], as: Unicode.UTF8.self)
var result: Data?
XCTAssertNoThrow(result = try JSONEncoder().encode(input))
XCTAssertEqual(try Array(XCTUnwrap(result)), [34, 92, 114, 34])
}
#endif
}

extension FoundationJSONEncoderTests.HelloWorld.SubType: Encodable {
Expand Down
Loading

0 comments on commit a5c49df

Please sign in to comment.