Skip to content

Commit ff5f81e

Browse files
committed
JSONSerialization: Improve parsing of numbers
- Check the number looks like a JSON number and exit early if not. - Use the native Int64(), UInt64(), Double() parsers to avoid creating a C string and passing to strtol()/strtod(). This also eliminates a memcpy() and removes the 63 digit restriction which would fail to parse numbers expressible by Double's full exponent. - For numbers with a leading '-' sign, parse using Int64() falling back to Double(), otherwise parse using UInt64() failling back to Double().
1 parent a2b4095 commit ff5f81e

File tree

2 files changed

+151
-34
lines changed

2 files changed

+151
-34
lines changed

Foundation/JSONSerialization.swift

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -822,46 +822,64 @@ private struct JSONReader {
822822
]
823823

824824
func parseNumber(_ input: Index, options opt: JSONSerialization.ReadingOptions) throws -> (Any, Index)? {
825-
func parseTypedNumber(_ address: UnsafePointer<UInt8>, count: Int) -> (Any, IndexDistance)? {
826-
let temp_buffer_size = 64
827-
var temp_buffer = [Int8](repeating: 0, count: temp_buffer_size)
828-
return temp_buffer.withUnsafeMutableBufferPointer { (buffer: inout UnsafeMutableBufferPointer<Int8>) -> (Any, IndexDistance)? in
829-
memcpy(buffer.baseAddress!, address, min(count, temp_buffer_size - 1)) // ensure null termination
830-
831-
let startPointer = buffer.baseAddress!
832-
let intEndPointer = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: 1)
833-
defer { intEndPointer.deallocate() }
834-
let doubleEndPointer = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: 1)
835-
defer { doubleEndPointer.deallocate() }
836-
let intResult = strtol(startPointer, intEndPointer, 10)
837-
let intDistance = startPointer.distance(to: intEndPointer[0]!)
838-
let doubleResult = strtod(startPointer, doubleEndPointer)
839-
let doubleDistance = startPointer.distance(to: doubleEndPointer[0]!)
840-
841-
guard doubleDistance > 0 else { return nil }
842-
if intDistance == doubleDistance {
843-
return (NSNumber(value: intResult), intDistance)
825+
let ZERO = UInt8(ascii: "0")
826+
let ONE = UInt8(ascii: "1")
827+
let NINE = UInt8(ascii: "9")
828+
let MINUS = UInt8(ascii: "-")
829+
830+
var isNegative = false
831+
var string = ""
832+
833+
// Validate the first few characters look like a JSON encoded number:
834+
// Optional '-' sign at start only 1 leading zero if followed by a decimal point.
835+
var index = input
836+
func nextASCII() -> UInt8? {
837+
guard let (ascii, nextIndex) = source.takeASCII(index),
838+
JSONReader.numberCodePoints.contains(ascii) else { return nil }
839+
index = nextIndex
840+
return ascii
841+
}
842+
843+
guard var ascii = nextASCII() else { return nil }
844+
guard ascii == MINUS || (ascii >= ZERO && ascii <= NINE) else { return nil }
845+
if ascii == MINUS {
846+
string = "-"
847+
isNegative = true
848+
guard let d = nextASCII() else { return nil }
849+
ascii = d
850+
}
851+
852+
if ascii == ZERO {
853+
if let ascii2 = nextASCII() {
854+
if ascii2 >= ZERO && ascii2 <= NINE {
855+
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.propertyListReadCorrupt.rawValue,
856+
userInfo: ["NSDebugDescription" : "Leading zeros not allowed at character \(input)." ])
844857
}
845-
return (NSNumber(value: doubleResult), doubleDistance)
858+
string.append("0")
859+
ascii = ascii2
846860
}
861+
} else if ascii < ONE || ascii > NINE {
862+
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.propertyListReadCorrupt.rawValue,
863+
userInfo: ["NSDebugDescription" : "Numbers must start with a 1-9 at character \(input)." ])
847864
}
848-
849-
if source.encoding == .utf8 {
850-
return parseTypedNumber(source.buffer.baseAddress!.advanced(by: input), count: source.buffer.count - input).map { return ($0.0, input + $0.1) }
865+
string.append(Character(UnicodeScalar(ascii)))
866+
while let ascii = nextASCII() {
867+
string.append(Character(UnicodeScalar(ascii)))
851868
}
852-
else {
853-
var numberCharacters = [UInt8]()
854-
var index = input
855-
while let (ascii, nextIndex) = source.takeASCII(index), JSONReader.numberCodePoints.contains(ascii) {
856-
numberCharacters.append(ascii)
857-
index = nextIndex
869+
870+
if isNegative {
871+
if let intValue = Int64(string) {
872+
return (NSNumber(value: intValue), index)
873+
}
874+
} else {
875+
if let uintValue = UInt64(string) {
876+
return (NSNumber(value: uintValue), index)
858877
}
859-
numberCharacters.append(0)
860-
861-
return numberCharacters.withUnsafeBufferPointer {
862-
parseTypedNumber($0.baseAddress!, count: $0.count)
863-
}.map { return ($0.0, index) }
864878
}
879+
if let doubleValue = Double(string) {
880+
return (NSNumber(value: doubleValue), index)
881+
}
882+
return nil
865883
}
866884

867885
//MARK: - Value parsing

TestFoundation/TestJSONEncoder.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,104 @@ class TestJSONEncoder : XCTestCase {
499499
}
500500
}
501501

502+
func test_numericLimits() {
503+
struct DataStruct: Codable {
504+
let int8Value: Int8?
505+
let uint8Value: UInt8?
506+
let int16Value: Int16?
507+
let uint16Value: UInt16?
508+
let int32Value: Int32?
509+
let uint32Value: UInt32?
510+
let int64Value: Int64?
511+
let intValue: Int?
512+
let uintValue: UInt?
513+
let uint64Value: UInt64?
514+
let floatValue: Float?
515+
let doubleValue: Double?
516+
}
517+
518+
func decode(_ type: String, _ value: String) throws {
519+
var key = type.lowercased()
520+
key.append("Value")
521+
_ = try JSONDecoder().decode(DataStruct.self, from: "{ \"\(key)\": \(value) }".data(using: .utf8)!)
522+
}
523+
524+
func testGoodValue(_ type: String, _ value: String) {
525+
do {
526+
try decode(type, value)
527+
} catch {
528+
XCTFail("Unexpected error: \(error) for parsing \(value) to \(type)")
529+
}
530+
}
531+
532+
func testErrorThrown(_ type: String, _ value: String, errorMessage: String) {
533+
do {
534+
try decode(type, value)
535+
XCTFail("Decode of \(value) to \(type) should not succeed")
536+
} catch DecodingError.dataCorrupted(let context) {
537+
XCTAssertEqual(context.debugDescription, errorMessage)
538+
} catch {
539+
XCTAssertEqual(String(describing: error), errorMessage)
540+
}
541+
}
542+
543+
544+
var goodValues = [
545+
("Int8", "0"), ("Int8", "1"), ("Int8", "-1"), ("Int8", "-128"), ("Int8", "127"),
546+
("UInt8", "0"), ("UInt8", "1"), ("UInt8", "255"), ("UInt8", "-0"),
547+
548+
("Int16", "0"), ("Int16", "1"), ("Int16", "-1"), ("Int16", "-32768"), ("Int16", "32767"),
549+
("UInt16", "0"), ("UInt16", "1"), ("UInt16", "65535"), ("UInt16", "34.0"),
550+
551+
("Int32", "0"), ("Int32", "1"), ("Int32", "-1"), ("Int32", "-2147483648"), ("Int32", "2147483647"),
552+
("UInt32", "0"), ("UInt32", "1"), ("UInt32", "4294967295"),
553+
554+
("Int64", "0"), ("Int64", "1"), ("Int64", "-1"), ("Int64", "-9223372036854775808"), ("Int64", "9223372036854775807"),
555+
("UInt64", "0"), ("UInt64", "1"), ("UInt64", "18446744073709551615"),
556+
]
557+
558+
if Int.max == Int64.max {
559+
goodValues += [
560+
("Int", "0"), ("Int", "1"), ("Int", "-1"), ("Int", "-9223372036854775808"), ("Int", "9223372036854775807"),
561+
("UInt", "0"), ("UInt", "1"), ("UInt", "18446744073709551615"),
562+
]
563+
} else {
564+
goodValues += [
565+
("Int", "0"), ("Int", "1"), ("Int", "-1"), ("Int", "-2147483648"), ("Int", "2147483647"),
566+
("UInt", "0"), ("UInt", "1"), ("UInt", "4294967295"),
567+
]
568+
}
569+
570+
let badValues = [
571+
("Int8", "-129"), ("Int8", "128"), ("Int8", "1.2"),
572+
("UInt8", "-1"), ("UInt8", "256"),
573+
574+
("Int16", "-32769"), ("Int16", "32768"),
575+
("UInt16", "-1"), ("UInt16", "65536"),
576+
577+
("Int32", "-2147483649"), ("Int32", "2147483648"),
578+
("UInt32", "-1"), ("UInt32", "4294967296"),
579+
580+
("Int64", "9223372036854775808"), ("Int64", "9223372036854775808"), ("Int64", "-100000000000000000000"),
581+
("UInt64", "-1"), ("UInt64", "18446744073709600000"), ("Int64", "10000000000000000000000000000000000000"),
582+
]
583+
584+
for value in goodValues {
585+
testGoodValue(value.0, value.1)
586+
}
587+
588+
for (type, value) in badValues {
589+
testErrorThrown(type, value, errorMessage: "Parsed JSON number <\(value)> does not fit in \(type).")
590+
}
591+
592+
// Leading zeros are invalid
593+
testErrorThrown("Int8", "0000000000000000000000000000001", errorMessage: "The operation could not be completed")
594+
testErrorThrown("Double", "-.1", errorMessage: "The operation could not be completed")
595+
testErrorThrown("Int32", "+1", errorMessage: "The operation could not be completed")
596+
testErrorThrown("Int", ".012", errorMessage: "The operation could not be completed")
597+
}
598+
599+
502600
// MARK: - Helper Functions
503601
private var _jsonEmptyDictionary: Data {
504602
return "{}".data(using: .utf8)!
@@ -1089,6 +1187,7 @@ extension TestJSONEncoder {
10891187
("test_codingOfDouble", test_codingOfDouble),
10901188
("test_codingOfString", test_codingOfString),
10911189
("test_codingOfURL", test_codingOfURL),
1190+
("test_numericLimits", test_numericLimits),
10921191
]
10931192
}
10941193
}

0 commit comments

Comments
 (0)