-
Notifications
You must be signed in to change notification settings - Fork 1.2k
SR-5640: JSONEncoder misrepresents UInt.max #1173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Foundation/NSNumber.swift
Outdated
@@ -256,6 +256,10 @@ open class NSNumber : NSValue { | |||
fatalError("unsupported CFNumberType: '\(numberType)'") | |||
} | |||
} | |||
|
|||
public var isSInt128Type: Bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't add new public API unfortunately.
Ah, didn't realise that. Ive removed the function and inlined the test instead |
TestFoundation/TestNSNumber.swift
Outdated
func test_stringValue() { | ||
XCTAssertEqual(NSNumber(value: UInt.min).stringValue, "0") | ||
XCTAssertEqual(NSNumber(value: UInt.min + 1).stringValue, "1") | ||
XCTAssertEqual(NSNumber(value: UInt.max).stringValue, "18446744073709551615") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a 32-bit platform UInt.max
is 4294967295 so this will break.
TestFoundation/TestNSNumber.swift
Outdated
XCTAssertEqual(NSNumber(value: UInt64.max).stringValue, "18446744073709551615") | ||
XCTAssertEqual(NSNumber(value: UInt64.max - 1).stringValue, "18446744073709551614") | ||
|
||
XCTAssertEqual(NSNumber(value: Int.min).stringValue, "-9223372036854775808") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here - Int.min
and Int.max
are different on 32-bit platforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any easy test for platform wordsize apart from Int.max == Int32.max etc ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm only aware of testing MemoryLayout<Int>.size == 32
, which isn't much better.
TestFoundation/TestJSONEncoder.swift
Outdated
@@ -401,6 +401,68 @@ class TestJSONEncoder : XCTestCase { | |||
test_codingOf(value: URL(string: "https://swift.org")!, toAndFrom: "\"https://swift.org\"") | |||
} | |||
|
|||
|
|||
// UInt and Int | |||
func test_uintMinMax() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the naming of this be consistent with the other tests, so test_codingOfUIntMinMax()
?
Also we should add this testcase on the Darwin side too: https://github.com/apple/swift/blob/master/test/stdlib/CodableTests.swift
I'm wondering if the upcoming merge of code from Apple might have a better fix for this... |
@swift-ci please test |
I fixed up the test names. I left the CodableTests.swift out for now as I will do a separate JSON PR for some other minor fixes I want to do. I suspect you are correct about there being a better fix in the 10.13 code dump but won't that happen after 4.0 has been released, i.e. when Xcode9 and High Sierra are publicly available? |
Could you please retest, I added another commit after you kicked off the last test, thanks. |
I think the code drop is due pretty soon: after we stop auto-merging from |
@swift-ci please test |
Foundation/JSONSerialization.swift
Outdated
//[SR-2151] https://bugs.swift.org/browse/SR-2151 | ||
private mutating func _serializationString(for number: NSNumber) -> String { | ||
if _CFNumberGetType2(number._cfObject) == kCFNumberSInt128Type { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like an encapsulation violation to have JSONSerialization calling this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@parkera I agree, but this seems like a reasonable fix for the moment until changes come in from the CF side, no? Or is it possible to get CF changes in for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the stringValue
can be used for any integer value, I could replace it with:
if !CFNumberIsFloatType(number._cfObject) {
return number.stringValue
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem I have is the way this ties the implementation detail of how NSNumber stores its value to JSONSerialization - the two shouldn't really be coupled to each other that tightly if we can help it. Maybe the formatter is a better place to encapsulate that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@parkera So ideally you want all references to _cfObject removed from JSONSerialization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No - it's the _CFNumberGetType2
that is the problem. _cfObject
is just our way to get toll-free bridging to work here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so is !CFNumberIsFloatType(number._cfObject)
ok to use instead?
I replaced the |
@swift-ci please test |
Yes, using the public CFNumber API is much better. Thanks. |
Looks like the test failed due to a build timeout, Ive just tested it against swift-DEVELOPMENT-SNAPSHOT-2017-08-14 on Xcode and it worked ok |
@swift-ci please test |
I think there's a performance problem with the latest change - by routing the stringification of all int types through A quick benchmark of JSONEncoder (on Linux) with a struct containing 10 Ints ran 2x slower. |
I guess we could change |
It could just use a fixed one when locale is |
@djones6 I added a cache for the CFNumberFormatter when locale is nil, can you see if that helps with the performance regression? |
@swift-ci please test |
Foundation/NSNumber.swift
Outdated
open func description(withLocale locale: Locale?) -> String { | ||
// CFNumberFormatterCreateStringWithNumber() doesnt like SInt128Type as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive-by nit: can you correct the spelling of "doesn't" and throw in a few punctuation marks (before the first "as" and before "so")?
Foundation/NSNumber.swift
Outdated
// so special case them. | ||
if _CFNumberGetType2(_cfObject) == kCFNumberSInt128Type { | ||
return String(format: "%@", unsafeBitCast(_cfObject, | ||
to: UnsafePointer<CFNumber>.self)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive-by nit no. 2: can you put this on the same line?
@spevans Thanks - this does address the performance regression. In fact, it seems to perform slightly better than the original code. |
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.
Thanks for running those tests @djones6. I have squashed and rebased and tidied up the comment so I think this may be ready to go. |
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please test |
I think this is good for merge - @spevans @djones6 @itaiferber @parkera? |
I think it is. Its fixes the NSNumber bug and I will have a follow up PR soon to hopefully speed up some of the integer serialisation. |
This is a workaround rather than a full fix. However it should make it easier to create the correct 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 an SInt128Type and if so the .stringValue
method is used to create a string.