From 353d76249ae46a4630c3ce04454475a211b0e171 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 98cd8458cd..ccf9db7f15 100644 --- a/Foundation/NSJSONSerialization.swift +++ b/Foundation/NSJSONSerialization.swift @@ -306,7 +306,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: NSArray) throws { diff --git a/Foundation/NSNumber.swift b/Foundation/NSNumber.swift index 967a2a64bf..8746658306 100644 --- a/Foundation/NSNumber.swift +++ b/Foundation/NSNumber.swift @@ -139,7 +139,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) } @@ -451,8 +450,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 3ba499cd7a..1fa670afd2 100644 --- a/TestFoundation/TestNSJSONSerialization.swift +++ b/TestFoundation/TestNSJSONSerialization.swift @@ -114,6 +114,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), + ] } @@ -622,6 +624,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()].bridge() XCTAssertEqual(try trySerialize(arr), "[null]") From 35e5d0fdc73215b9e5e9d4537daf76288cd16192 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 ccf9db7f15..ca15d65967 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) @@ -240,6 +242,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 @@ -298,7 +308,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"]) } @@ -306,7 +316,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: NSArray) throws { @@ -389,6 +399,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 8746658306..967a2a64bf 100644 --- a/Foundation/NSNumber.swift +++ b/Foundation/NSNumber.swift @@ -139,6 +139,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) } @@ -450,23 +451,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 1fa670afd2..6907b2a357 100644 --- a/TestFoundation/TestNSJSONSerialization.swift +++ b/TestFoundation/TestNSJSONSerialization.swift @@ -691,9 +691,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 44e20c762eff17c99a1aaedcc970f3a7f4cc099c 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 by removing casting update to latest master --- TestFoundation/TestNSJSONSerialization.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TestFoundation/TestNSJSONSerialization.swift b/TestFoundation/TestNSJSONSerialization.swift index 6907b2a357..da382886b9 100644 --- a/TestFoundation/TestNSJSONSerialization.swift +++ b/TestFoundation/TestNSJSONSerialization.swift @@ -655,7 +655,7 @@ extension TestNSJSONSerialization { ("-0.23456789012345",-0.23456789012345), ] for param in params { - let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] + let testDict = [param.0 : param.1] let str = try? trySerialize(testDict.bridge()) XCTAssertEqual(str!, "{\"\(param.0)\":\(param.1)}", "serialized value should have a decimal places and leading zero") } @@ -671,7 +671,7 @@ extension TestNSJSONSerialization { ("-1.23456789012345",-1.23456789012345), ] for param in paramsBove1 { - let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] + let testDict = [param.0 : param.1] let str = try? trySerialize(testDict.bridge()) XCTAssertEqual(str!, "{\"\(param.0)\":\(param.1)}", "serialized Double should have a decimal places and leading value") } @@ -686,7 +686,7 @@ extension TestNSJSONSerialization { ("1" ,1.0), ] for param in paramsWholeNumbers { - let testDict = [param.0 : param.1 as AnyObject] as [String : AnyObject] + let testDict = [param.0 : param.1] 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 ") } @@ -695,7 +695,7 @@ extension TestNSJSONSerialization { func excecute_testWholeNumbersWithIntInput() { for i in -10..<10 { let iStr = "\(i)" - let testDict = [iStr : i as AnyObject] as [String : AnyObject] + let testDict = [iStr : i] 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 ") }