Skip to content

Commit 8d05411

Browse files
committed
SR-5640: JSONEncoder misrepresents UInt.max
This is a workaround rather than a full fix. - NSNumber(value: UInt.max).stringValue was returning "-1" instead of "18446744073709551615" because NSNumber holds any value > Int64.max as a 128Bit quantity using a high Int and low UInt. It marks this type as an 'SInt128Type'. - CFNumberFormatterCreateStringWithNumber() uses CFNumberGetType() to get this type however CoreFoundation wants to hide SInt128Type (probably for backwards compatibilty) and instead returns it as an SInt64Type. Thus only the low part of the NSNumber is used for the number and this is interpreted as an Int64. - The workaround is simply in NSNumber.description(withLocale:) to test to see if the value is of type SInt128Type and if so use String(format: "%@") to convert it to a string instead of using CFNumberFormatterCreateStringWithNumber(). This should be ok for most situations since it is only used for positive integers and there are no issues with formatting leading zeros or decimal points. - For JSONWriter._serializationString(for: NSNumber) the value is tested to see if it is not a floating point value and if so the .stringValue method is used to create a string. - NSNumber.description(withLocale:) - cache the CFNumberFormatter when locale is nil as a small speedup.
1 parent 2c5d0af commit 8d05411

File tree

4 files changed

+151
-5
lines changed

4 files changed

+151
-5
lines changed

Foundation/JSONSerialization.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,9 +577,12 @@ private struct JSONWriter {
577577
writer(" ")
578578
}
579579
}
580-
580+
581581
//[SR-2151] https://bugs.swift.org/browse/SR-2151
582582
private mutating func _serializationString(for number: NSNumber) -> String {
583+
if !CFNumberIsFloatType(number._cfObject) {
584+
return number.stringValue
585+
}
583586
return CFNumberFormatterCreateStringWithNumber(nil, _numberformatter, number._cfObject)._swiftObject
584587
}
585588
}

Foundation/NSNumber.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ open class NSNumber : NSValue {
256256
fatalError("unsupported CFNumberType: '\(numberType)'")
257257
}
258258
}
259-
259+
260260
deinit {
261261
_CFDeinit(self)
262262
}
@@ -536,13 +536,25 @@ open class NSNumber : NSValue {
536536
}
537537
}
538538

539+
private static let _numberFormatterForNilLocale: CFNumberFormatter = {
540+
let formatter: CFNumberFormatter
541+
formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle)
542+
CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObjectiveC())
543+
return formatter
544+
}()
545+
539546
open func description(withLocale locale: Locale?) -> String {
547+
// CFNumberFormatterCreateStringWithNumber() does not like numbers of type
548+
// SInt128Type, as it loses the type when looking it up and treats it as
549+
// an SInt64Type, so special case them.
550+
if _CFNumberGetType2(_cfObject) == kCFNumberSInt128Type {
551+
return String(format: "%@", unsafeBitCast(_cfObject, to: UnsafePointer<CFNumber>.self))
552+
}
553+
540554
let aLocale = locale
541555
let formatter: CFNumberFormatter
542556
if (aLocale == nil) {
543-
formatter = CFNumberFormatterCreate(nil, CFLocaleCopyCurrent(), kCFNumberFormatterNoStyle)
544-
CFNumberFormatterSetProperty(formatter, kCFNumberFormatterMaxFractionDigits, 15._bridgeToObjectiveC())
545-
557+
formatter = NSNumber._numberFormatterForNilLocale
546558
} else {
547559
formatter = CFNumberFormatterCreate(nil, aLocale?._cfObject, kCFNumberFormatterDecimalStyle)
548560
}

TestFoundation/TestJSONEncoder.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,68 @@ class TestJSONEncoder : XCTestCase {
401401
test_codingOf(value: URL(string: "https://swift.org")!, toAndFrom: "\"https://swift.org\"")
402402
}
403403

404+
405+
// UInt and Int
406+
func test_codingOfUIntMinMax() {
407+
408+
let encoder = JSONEncoder()
409+
410+
struct MyValue: Codable {
411+
let intMin:Int = Int.min
412+
let intMax:Int = Int.max
413+
let uintMin:UInt = UInt.min
414+
let uintMax:UInt = UInt.max
415+
}
416+
417+
let myValue = MyValue()
418+
let myDictI: [String:Any] = ["intMin": myValue.intMin, "intMax": myValue.intMax]
419+
let myDictU: [String:Any] = ["uintMin": myValue.uintMin, "uintMax": myValue.uintMax]
420+
let myDict1: [String:Any] = ["intMin": myValue.intMin]
421+
let myDict2: [String:Any] = ["intMax": myValue.intMax]
422+
let myDict3: [String:Any] = ["uintMin": myValue.uintMin]
423+
let myDict4: [String:Any] = ["uintMax": myValue.uintMax]
424+
425+
func compareJSON(_ s1: String, _ s2: String) {
426+
let ss1 = s1.trimmingCharacters(in: CharacterSet(charactersIn: "{}")).split(separator: Character(",")).sorted()
427+
let ss2 = s2.trimmingCharacters(in: CharacterSet(charactersIn: "{}")).split(separator: Character(",")).sorted()
428+
XCTAssertEqual(ss1, ss2)
429+
}
430+
431+
do {
432+
let result = try encoder.encode(myValue)
433+
let r = String(data: result, encoding: .utf8) ?? "nil"
434+
compareJSON(r, "{\"uintMin\":0,\"uintMax\":18446744073709551615,\"intMin\":-9223372036854775808,\"intMax\":9223372036854775807}")
435+
436+
let resultI = try JSONSerialization.data(withJSONObject: myDictI)
437+
let rI = String(data: resultI, encoding: .utf8) ?? "nil"
438+
compareJSON(rI, "{\"intMin\":-9223372036854775808,\"intMax\":9223372036854775807}")
439+
440+
let resultU = try JSONSerialization.data(withJSONObject: myDictU)
441+
let rU = String(data: resultU, encoding: .utf8) ?? "nil"
442+
compareJSON(rU, "{\"uintMax\":18446744073709551615,\"uintMin\":0}")
443+
444+
let result1 = try JSONSerialization.data(withJSONObject: myDict1)
445+
let r1 = String(data: result1, encoding: .utf8) ?? "nil"
446+
XCTAssertEqual(r1, "{\"intMin\":-9223372036854775808}")
447+
448+
let result2 = try JSONSerialization.data(withJSONObject: myDict2)
449+
let r2 = String(data: result2, encoding: .utf8) ?? "nil"
450+
XCTAssertEqual(r2, "{\"intMax\":9223372036854775807}")
451+
452+
let result3 = try JSONSerialization.data(withJSONObject: myDict3)
453+
let r3 = String(data: result3, encoding: .utf8) ?? "nil"
454+
XCTAssertEqual(r3, "{\"uintMin\":0}")
455+
456+
let result4 = try JSONSerialization.data(withJSONObject: myDict4)
457+
let r4 = String(data: result4, encoding: .utf8) ?? "nil"
458+
XCTAssertEqual(r4, "{\"uintMax\":18446744073709551615}")
459+
} catch {
460+
XCTFail(String(describing: error))
461+
}
462+
}
463+
464+
465+
404466
// MARK: - Helper Functions
405467
private var _jsonEmptyDictionary: Data {
406468
return "{}".data(using: .utf8)!
@@ -986,6 +1048,7 @@ extension TestJSONEncoder {
9861048
("test_codingOfUInt64", test_codingOfUInt64),
9871049
("test_codingOfInt", test_codingOfInt),
9881050
("test_codingOfUInt", test_codingOfUInt),
1051+
("test_codingOfUIntMinMax", test_codingOfUIntMinMax),
9891052
("test_codingOfFloat", test_codingOfFloat),
9901053
("test_codingOfDouble", test_codingOfDouble),
9911054
("test_codingOfString", test_codingOfString),

TestFoundation/TestNSNumber.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class TestNSNumber : XCTestCase {
4343
("test_description", test_description ),
4444
("test_descriptionWithLocale", test_descriptionWithLocale ),
4545
("test_objCType", test_objCType ),
46+
("test_stringValue", test_stringValue),
4647
]
4748
}
4849

@@ -1019,4 +1020,71 @@ class TestNSNumber : XCTestCase {
10191020
XCTAssertEqual("f" /* 0x66 */, objCType(NSNumber(value: Float.greatestFiniteMagnitude)))
10201021
XCTAssertEqual("d" /* 0x64 */, objCType(NSNumber(value: Double.greatestFiniteMagnitude)))
10211022
}
1023+
1024+
func test_stringValue() {
1025+
1026+
if UInt.max == UInt32.max {
1027+
XCTAssertEqual(NSNumber(value: UInt.min).stringValue, "0")
1028+
XCTAssertEqual(NSNumber(value: UInt.min + 1).stringValue, "1")
1029+
XCTAssertEqual(NSNumber(value: UInt.max).stringValue, "4294967295")
1030+
XCTAssertEqual(NSNumber(value: UInt.max - 1).stringValue, "4294967294")
1031+
} else if UInt.max == UInt64.max {
1032+
XCTAssertEqual(NSNumber(value: UInt.min).stringValue, "0")
1033+
XCTAssertEqual(NSNumber(value: UInt.min + 1).stringValue, "1")
1034+
XCTAssertEqual(NSNumber(value: UInt.max).stringValue, "18446744073709551615")
1035+
XCTAssertEqual(NSNumber(value: UInt.max - 1).stringValue, "18446744073709551614")
1036+
}
1037+
1038+
XCTAssertEqual(NSNumber(value: UInt8.min).stringValue, "0")
1039+
XCTAssertEqual(NSNumber(value: UInt8.min + 1).stringValue, "1")
1040+
XCTAssertEqual(NSNumber(value: UInt8.max).stringValue, "255")
1041+
XCTAssertEqual(NSNumber(value: UInt8.max - 1).stringValue, "254")
1042+
1043+
XCTAssertEqual(NSNumber(value: UInt16.min).stringValue, "0")
1044+
XCTAssertEqual(NSNumber(value: UInt16.min + 1).stringValue, "1")
1045+
XCTAssertEqual(NSNumber(value: UInt16.max).stringValue, "65535")
1046+
XCTAssertEqual(NSNumber(value: UInt16.max - 1).stringValue, "65534")
1047+
1048+
XCTAssertEqual(NSNumber(value: UInt32.min).stringValue, "0")
1049+
XCTAssertEqual(NSNumber(value: UInt32.min + 1).stringValue, "1")
1050+
XCTAssertEqual(NSNumber(value: UInt32.max).stringValue, "4294967295")
1051+
XCTAssertEqual(NSNumber(value: UInt32.max - 1).stringValue, "4294967294")
1052+
1053+
XCTAssertEqual(NSNumber(value: UInt64.min).stringValue, "0")
1054+
XCTAssertEqual(NSNumber(value: UInt64.min + 1).stringValue, "1")
1055+
XCTAssertEqual(NSNumber(value: UInt64.max).stringValue, "18446744073709551615")
1056+
XCTAssertEqual(NSNumber(value: UInt64.max - 1).stringValue, "18446744073709551614")
1057+
1058+
if Int.max == Int32.max {
1059+
XCTAssertEqual(NSNumber(value: Int.min).stringValue, "-2147483648")
1060+
XCTAssertEqual(NSNumber(value: Int.min + 1).stringValue, "-2147483647")
1061+
XCTAssertEqual(NSNumber(value: Int.max).stringValue, "2147483647")
1062+
XCTAssertEqual(NSNumber(value: Int.max - 1).stringValue, "2147483646")
1063+
} else if Int.max == Int64.max {
1064+
XCTAssertEqual(NSNumber(value: Int.min).stringValue, "-9223372036854775808")
1065+
XCTAssertEqual(NSNumber(value: Int.min + 1).stringValue, "-9223372036854775807")
1066+
XCTAssertEqual(NSNumber(value: Int.max).stringValue, "9223372036854775807")
1067+
XCTAssertEqual(NSNumber(value: Int.max - 1).stringValue, "9223372036854775806")
1068+
}
1069+
1070+
XCTAssertEqual(NSNumber(value: Int8.min).stringValue, "-128")
1071+
XCTAssertEqual(NSNumber(value: Int8.min + 1).stringValue, "-127")
1072+
XCTAssertEqual(NSNumber(value: Int8.max).stringValue, "127")
1073+
XCTAssertEqual(NSNumber(value: Int8.max - 1).stringValue, "126")
1074+
1075+
XCTAssertEqual(NSNumber(value: Int16.min).stringValue, "-32768")
1076+
XCTAssertEqual(NSNumber(value: Int16.min + 1).stringValue, "-32767")
1077+
XCTAssertEqual(NSNumber(value: Int16.max).stringValue, "32767")
1078+
XCTAssertEqual(NSNumber(value: Int16.max - 1).stringValue, "32766")
1079+
1080+
XCTAssertEqual(NSNumber(value: Int32.min).stringValue, "-2147483648")
1081+
XCTAssertEqual(NSNumber(value: Int32.min + 1).stringValue, "-2147483647")
1082+
XCTAssertEqual(NSNumber(value: Int32.max).stringValue, "2147483647")
1083+
XCTAssertEqual(NSNumber(value: Int32.max - 1).stringValue, "2147483646")
1084+
1085+
XCTAssertEqual(NSNumber(value: Int64.min).stringValue, "-9223372036854775808")
1086+
XCTAssertEqual(NSNumber(value: Int64.min + 1).stringValue, "-9223372036854775807")
1087+
XCTAssertEqual(NSNumber(value: Int64.max).stringValue, "9223372036854775807")
1088+
XCTAssertEqual(NSNumber(value: Int64.max - 1).stringValue, "9223372036854775806")
1089+
}
10221090
}

0 commit comments

Comments
 (0)