From 2ca99ff049e66b46f8d0c25cd2d8ad32377aada6 Mon Sep 17 00:00:00 2001 From: david liu Date: Sat, 13 Aug 2016 13:19:42 -0700 Subject: [PATCH 1/3] [SR-2151]NSJSONSerialization.data produces illegal JSON code NSJSONSerialization.data(withJSONObject:options) produces illegal JSON code https://bugs.swift.org/browse/SR-2151 --- Foundation/NSJSONSerialization.swift | 2 +- Foundation/NSNumber.swift | 16 ++++- TestFoundation/TestNSJSONSerialization.swift | 74 ++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/Foundation/NSJSONSerialization.swift b/Foundation/NSJSONSerialization.swift index d9d5f09bf6..f2d44af439 100644 --- a/Foundation/NSJSONSerialization.swift +++ b/Foundation/NSJSONSerialization.swift @@ -318,7 +318,7 @@ private struct JSONWriter { // Cannot detect type information (e.g. bool) as there is no objCType property on NSNumber in Swift // So, just print the number - writer("\(num)") + writer(num.serializationString) } mutating func serializeArray(_ array: [Any]) throws { diff --git a/Foundation/NSNumber.swift b/Foundation/NSNumber.swift index 4333047a8c..35cf6ae8ef 100644 --- a/Foundation/NSNumber.swift +++ b/Foundation/NSNumber.swift @@ -200,7 +200,6 @@ open class NSNumber : NSValue { // This layout MUST be the same as CFNumber so that they are bridgeable private var _base = _CFInfo(typeID: CFNumberGetTypeID()) private var _pad: UInt64 = 0 - internal var _cfObject: CFType { return unsafeBitCast(self, to: CFType.self) } @@ -512,8 +511,23 @@ open class NSNumber : NSValue { open override var description: String { return description(withLocale: nil) } + + //[SR-2151] https://bugs.swift.org/browse/SR-2151 + internal var serializationString: String { + let formatter: CFNumberFormatter + formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle) + CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObject()) + switch CFNumberGetType(_cfObject as CFNumber){ + case .floatType, .float32Type, .float64Type, .cgFloatType, .doubleType: + CFNumberFormatterSetFormat(formatter, "0.###############"._cfObject); + default:break + } + return CFNumberFormatterCreateStringWithNumber(nil, formatter, self._cfObject)._swiftObject + } } + + extension CFNumber : _NSBridgable { typealias NSType = NSNumber internal var _nsObject: NSType { return unsafeBitCast(self, to: NSType.self) } diff --git a/TestFoundation/TestNSJSONSerialization.swift b/TestFoundation/TestNSJSONSerialization.swift index 243b490dc1..7ce4ac2df3 100644 --- a/TestFoundation/TestNSJSONSerialization.swift +++ b/TestFoundation/TestNSJSONSerialization.swift @@ -117,6 +117,8 @@ extension TestNSJSONSerialization { ("test_deserialize_badlyFormedArray", test_deserialize_badlyFormedArray), ("test_deserialize_invalidEscapeSequence", test_deserialize_invalidEscapeSequence), ("test_deserialize_unicodeMissingTrailingSurrogate", test_deserialize_unicodeMissingTrailingSurrogate), + ("test_serialize_dictionaryWithDecimal", test_serialize_dictionaryWithDecimal), + ] } @@ -624,6 +626,78 @@ extension TestNSJSONSerialization { XCTAssertEqual(try trySerialize(array2), "[]") } + //[SR-2151] https://bugs.swift.org/browse/SR-2151 + //NSJSONSerialization.data(withJSONObject:options) produces illegal JSON code + func test_serialize_dictionaryWithDecimal() { + + //test serialize values less than 1 with maxFractionDigits = 15 + func excecute_testSetLessThanOne() { + //expected : input to be serialized + let params = [ + ("0.1",0.1), + ("0.2",0.2), + ("0.3",0.3), + ("0.4",0.4), + ("0.5",0.5), + ("0.6",0.6), + ("0.7",0.7), + ("0.8",0.8), + ("0.9",0.9), + ("0.23456789012345",0.23456789012345), + + ("-0.1",-0.1), + ("-0.2",-0.2), + ("-0.3",-0.3), + ("-0.4",-0.4), + ("-0.5",-0.5), + ("-0.6",-0.6), + ("-0.7",-0.7), + ("-0.8",-0.8), + ("-0.9",-0.9), + ("-0.23456789012345",-0.23456789012345), + ] + for param in params { + let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] + let str = try? trySerialize(testDict.bridge()) + XCTAssertEqual(str!, "{\"\(param.0)\":\(param.1)}", "serialized value should have a decimal places and leading zero") + } + } + //test serialize values grater than 1 with maxFractionDigits = 15 + func excecute_testSetGraterThanOne() { + let paramsBove1 = [ + ("1.1",1.1), + ("1.2",1.2), + ("1.23456789012345",1.23456789012345), + ("-1.1",-1.1), + ("-1.2",-1.2), + ("-1.23456789012345",-1.23456789012345), + ] + for param in paramsBove1 { + let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] + let str = try? trySerialize(testDict.bridge()) + XCTAssertEqual(str!, "{\"\(param.0)\":\(param.1)}", "serialized Double should have a decimal places and leading value") + } + } + + //test serialize values for whole integer where the input is in Double format + func excecute_testWholeNumbersWithDoubleAsInput() { + + let paramsWholeNumbers = [ + ("-1" ,-1.0), + ("0" ,0.0), + ("1" ,1.0), + ] + for param in paramsWholeNumbers { + let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] + let str = try? trySerialize(testDict.bridge()) + XCTAssertEqual(str!, "{\"\(param.0)\":\(param.0._bridgeToObject().intValue)}", "expect that serialized value should not contain trailing zero or decimal as they are whole numbers ") + } + } + excecute_testSetLessThanOne() + excecute_testSetGraterThanOne() + excecute_testWholeNumbersWithDoubleAsInput() + } + func test_serialize_null() { let arr = [NSNull()] XCTAssertEqual(try trySerialize(arr), "[null]") From bf5fdac9b2ded370a1c364da5c736652efb0c2df Mon Sep 17 00:00:00 2001 From: david liu Date: Sun, 14 Aug 2016 14:19:25 -0700 Subject: [PATCH 2/3] decoupling formatting logic and lazy loading formatter 1. moved format logic out of NSNumber and in to NSJSonSerialization 2. lazy load the formatter to be instantiated if needed 3. create a single format string to work with all formats since we are lazy loading a single formatter --- Foundation/NSJSONSerialization.swift | 19 +++++++++++++++++-- Foundation/NSNumber.swift | 16 +--------------- TestFoundation/TestNSJSONSerialization.swift | 10 ++++++++++ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Foundation/NSJSONSerialization.swift b/Foundation/NSJSONSerialization.swift index f2d44af439..40c43fcece 100644 --- a/Foundation/NSJSONSerialization.swift +++ b/Foundation/NSJSONSerialization.swift @@ -7,6 +7,8 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // +import CoreFoundation + #if os(OSX) || os(iOS) import Darwin #elseif os(Linux) @@ -253,6 +255,14 @@ private struct JSONWriter { let pretty: Bool let writer: (String?) -> Void + private lazy var _numberformatter: CFNumberFormatter = { + let formatter: CFNumberFormatter + formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle) + CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObject()) + CFNumberFormatterSetFormat(formatter, "0.###############"._cfObject) + return formatter + }() + init(pretty: Bool = false, writer: @escaping (String?) -> Void) { self.pretty = pretty self.writer = writer @@ -310,7 +320,7 @@ private struct JSONWriter { writer("\"") } - func serializeNumber(_ num: NSNumber) throws { + mutating func serializeNumber(_ num: NSNumber) throws { if num.doubleValue.isInfinite || num.doubleValue.isNaN { throw NSError(domain: NSCocoaErrorDomain, code: NSCocoaError.PropertyListReadCorruptError.rawValue, userInfo: ["NSDebugDescription" : "Number cannot be infinity or NaN"]) } @@ -318,7 +328,7 @@ private struct JSONWriter { // Cannot detect type information (e.g. bool) as there is no objCType property on NSNumber in Swift // So, just print the number - writer(num.serializationString) + writer(_serializationString(for: num)) } mutating func serializeArray(_ array: [Any]) throws { @@ -402,6 +412,11 @@ private struct JSONWriter { writer(" ") } } + + //[SR-2151] https://bugs.swift.org/browse/SR-2151 + private mutating func _serializationString(for number: NSNumber) -> String { + return CFNumberFormatterCreateStringWithNumber(nil, _numberformatter, number._cfObject)._swiftObject + } } //MARK: - JSONDeserializer diff --git a/Foundation/NSNumber.swift b/Foundation/NSNumber.swift index 35cf6ae8ef..4333047a8c 100644 --- a/Foundation/NSNumber.swift +++ b/Foundation/NSNumber.swift @@ -200,6 +200,7 @@ open class NSNumber : NSValue { // This layout MUST be the same as CFNumber so that they are bridgeable private var _base = _CFInfo(typeID: CFNumberGetTypeID()) private var _pad: UInt64 = 0 + internal var _cfObject: CFType { return unsafeBitCast(self, to: CFType.self) } @@ -511,23 +512,8 @@ open class NSNumber : NSValue { open override var description: String { return description(withLocale: nil) } - - //[SR-2151] https://bugs.swift.org/browse/SR-2151 - internal var serializationString: String { - let formatter: CFNumberFormatter - formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle) - CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObject()) - switch CFNumberGetType(_cfObject as CFNumber){ - case .floatType, .float32Type, .float64Type, .cgFloatType, .doubleType: - CFNumberFormatterSetFormat(formatter, "0.###############"._cfObject); - default:break - } - return CFNumberFormatterCreateStringWithNumber(nil, formatter, self._cfObject)._swiftObject - } } - - extension CFNumber : _NSBridgable { typealias NSType = NSNumber internal var _nsObject: NSType { return unsafeBitCast(self, to: NSType.self) } diff --git a/TestFoundation/TestNSJSONSerialization.swift b/TestFoundation/TestNSJSONSerialization.swift index 7ce4ac2df3..35d08ae2ad 100644 --- a/TestFoundation/TestNSJSONSerialization.swift +++ b/TestFoundation/TestNSJSONSerialization.swift @@ -693,9 +693,19 @@ extension TestNSJSONSerialization { XCTAssertEqual(str!, "{\"\(param.0)\":\(param.0._bridgeToObject().intValue)}", "expect that serialized value should not contain trailing zero or decimal as they are whole numbers ") } } + + func excecute_testWholeNumbersWithIntInput() { + for i in -10..<10 { + let iStr = "\(i)" + let testDict = [iStr : i as AnyObject] as [String : AnyObject] + let str = try? trySerialize(testDict.bridge()) + XCTAssertEqual(str!, "{\"\(iStr)\":\(i)}", "expect that serialized value should not contain trailing zero or decimal as they are whole numbers ") + } + } excecute_testSetLessThanOne() excecute_testSetGraterThanOne() excecute_testWholeNumbersWithDoubleAsInput() + excecute_testWholeNumbersWithIntInput() } func test_serialize_null() { From 5ca161a8812e36a7641cdb99d53d9c633b2ce862 Mon Sep 17 00:00:00 2001 From: david liu Date: Mon, 15 Aug 2016 15:39:34 -0700 Subject: [PATCH 3/3] fix build break update to latest master by removing casting and updating syntax --- Foundation/NSJSONSerialization.swift | 2 +- TestFoundation/TestNSJSONSerialization.swift | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Foundation/NSJSONSerialization.swift b/Foundation/NSJSONSerialization.swift index 40c43fcece..860194206d 100644 --- a/Foundation/NSJSONSerialization.swift +++ b/Foundation/NSJSONSerialization.swift @@ -258,7 +258,7 @@ private struct JSONWriter { private lazy var _numberformatter: CFNumberFormatter = { let formatter: CFNumberFormatter formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle) - CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObject()) + CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, NSNumber(value: 15)) CFNumberFormatterSetFormat(formatter, "0.###############"._cfObject) return formatter }() diff --git a/TestFoundation/TestNSJSONSerialization.swift b/TestFoundation/TestNSJSONSerialization.swift index 35d08ae2ad..de12fa6dc4 100644 --- a/TestFoundation/TestNSJSONSerialization.swift +++ b/TestFoundation/TestNSJSONSerialization.swift @@ -633,7 +633,7 @@ extension TestNSJSONSerialization { //test serialize values less than 1 with maxFractionDigits = 15 func excecute_testSetLessThanOne() { //expected : input to be serialized - let params = [ + let params = [ ("0.1",0.1), ("0.2",0.2), ("0.3",0.3), @@ -657,8 +657,8 @@ extension TestNSJSONSerialization { ("-0.23456789012345",-0.23456789012345), ] for param in params { - let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] - let str = try? trySerialize(testDict.bridge()) + let testDict = [param.0 : param.1] + let str = try? trySerialize(testDict) XCTAssertEqual(str!, "{\"\(param.0)\":\(param.1)}", "serialized value should have a decimal places and leading zero") } } @@ -673,8 +673,8 @@ extension TestNSJSONSerialization { ("-1.23456789012345",-1.23456789012345), ] for param in paramsBove1 { - let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] - let str = try? trySerialize(testDict.bridge()) + let testDict = [param.0 : param.1] + let str = try? trySerialize(testDict) XCTAssertEqual(str!, "{\"\(param.0)\":\(param.1)}", "serialized Double should have a decimal places and leading value") } } @@ -688,17 +688,17 @@ extension TestNSJSONSerialization { ("1" ,1.0), ] for param in paramsWholeNumbers { - let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] - let str = try? trySerialize(testDict.bridge()) - XCTAssertEqual(str!, "{\"\(param.0)\":\(param.0._bridgeToObject().intValue)}", "expect that serialized value should not contain trailing zero or decimal as they are whole numbers ") + let testDict = [param.0 : param.1] + let str = try? trySerialize(testDict) + XCTAssertEqual(str!, "{\"\(param.0)\":\(NSString(string:param.0).intValue)}", "expect that serialized value should not contain trailing zero or decimal as they are whole numbers ") } } func excecute_testWholeNumbersWithIntInput() { for i in -10..<10 { let iStr = "\(i)" - let testDict = [iStr : i as AnyObject] as [String : AnyObject] - let str = try? trySerialize(testDict.bridge()) + let testDict = [iStr : i] + let str = try? trySerialize(testDict) XCTAssertEqual(str!, "{\"\(iStr)\":\(i)}", "expect that serialized value should not contain trailing zero or decimal as they are whole numbers ") } }