Skip to content

Commit 944aead

Browse files
authored
rdar://131793235 (Invalid JSON string causes precondition failure) (#754)
1 parent c0a485e commit 944aead

File tree

5 files changed

+86
-72
lines changed

5 files changed

+86
-72
lines changed

Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ internal func __NSStringToDecimal(
435435
from: string.utf8,
436436
decimalSeparator: ".".utf8,
437437
matchEntireString: false
438-
)
438+
).asOptional
439439
processedLength.pointee = parsed.processedLength
440440
if let parsedResult = parsed.result {
441441
result.pointee = parsedResult

Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ internal import _ForSwiftFoundation
1818
extension Decimal : CustomStringConvertible {
1919
public init?(string: __shared String, locale: __shared Locale? = nil) {
2020
let decimalSeparator = locale?.decimalSeparator ?? "."
21-
guard let value = Decimal._decimal(
21+
guard case let .success(value, _) = Decimal._decimal(
2222
from: string.utf8,
2323
decimalSeparator: decimalSeparator.utf8,
2424
matchEntireString: false
25-
).result else {
25+
) else {
2626
return nil
2727
}
2828
self = value

Sources/FoundationEssentials/Decimal/Decimal.swift

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ extension Decimal {
196196
decimalSeparator: String.UTF8View,
197197
matchEntireString: Bool
198198
) -> (result: Decimal?, processedLength: Int) {
199-
_decimal(from: stringView, decimalSeparator: decimalSeparator, matchEntireString: matchEntireString)
199+
_decimal(from: stringView, decimalSeparator: decimalSeparator, matchEntireString: matchEntireString).asOptional
200200
}
201201
#endif
202202
internal func _toString(with locale: Locale? = nil) -> String {
@@ -250,11 +250,26 @@ extension Decimal {
250250
return String(buffer.reversed())
251251
}
252252

253-
internal static func _decimal(
254-
from stringView: String.UTF8View,
255-
decimalSeparator: String.UTF8View,
253+
internal enum DecimalParseResult {
254+
case success(Decimal, processedLength: Int)
255+
case parseFailure
256+
case overlargeValue
257+
258+
var asOptional: (result: Decimal?, processedLength: Int) {
259+
switch self {
260+
case let .success(decimal, processedLength): (decimal, processedLength: processedLength)
261+
default: (nil, processedLength: 0)
262+
}
263+
}
264+
}
265+
266+
@_specialize(where UTF8Collection == String.UTF8View)
267+
@_specialize(where UTF8Collection == BufferView<UInt8>)
268+
internal static func _decimal<UTF8Collection: Collection>(
269+
from utf8View: UTF8Collection,
270+
decimalSeparator: String.UTF8View = ".".utf8,
256271
matchEntireString: Bool
257-
) -> (result: Decimal?, processedLength: Int) {
272+
) -> DecimalParseResult where UTF8Collection.Element == UTF8.CodeUnit {
258273
func multiplyBy10AndAdd(
259274
_ decimal: Decimal,
260275
number: UInt16
@@ -268,47 +283,47 @@ extension Decimal {
268283
}
269284
}
270285

271-
func skipWhiteSpaces(from index: String.UTF8View.Index) -> String.UTF8View.Index {
286+
func skipWhiteSpaces(from index: UTF8Collection.Index) -> UTF8Collection.Index {
272287
var i = index
273-
while i != stringView.endIndex &&
274-
Character(utf8Scalar: stringView[i]).isWhitespace {
275-
stringView.formIndex(after: &i)
288+
while i != utf8View.endIndex &&
289+
Character(utf8Scalar: utf8View[i]).isWhitespace {
290+
utf8View.formIndex(after: &i)
276291
}
277292
return i
278293
}
279294

280-
func stringViewContainsDecimalSeparator(at index: String.UTF8View.Index) -> Bool {
295+
func stringViewContainsDecimalSeparator(at index: UTF8Collection.Index) -> Bool {
281296
for indexOffset in 0 ..< decimalSeparator.count {
282-
let stringIndex = stringView.index(index, offsetBy: indexOffset)
297+
let stringIndex = utf8View.index(index, offsetBy: indexOffset)
283298
let decimalIndex = decimalSeparator.index(
284299
decimalSeparator.startIndex,
285300
offsetBy: indexOffset
286301
)
287-
if stringView[stringIndex] != decimalSeparator[decimalIndex] {
302+
if utf8View[stringIndex] != decimalSeparator[decimalIndex] {
288303
return false
289304
}
290305
}
291306
return true
292307
}
293308

294309
var result = Decimal()
295-
var index = stringView.startIndex
310+
var index = utf8View.startIndex
296311
index = skipWhiteSpaces(from: index)
297312
// Get the sign
298-
if index != stringView.endIndex &&
299-
(stringView[index] == UInt8._plus ||
300-
stringView[index] == UInt8._minus) {
301-
result._isNegative = (stringView[index] == UInt8._minus) ? 1 : 0
313+
if index != utf8View.endIndex &&
314+
(utf8View[index] == UInt8._plus ||
315+
utf8View[index] == UInt8._minus) {
316+
result._isNegative = (utf8View[index] == UInt8._minus) ? 1 : 0
302317
// Advance over the sign
303-
stringView.formIndex(after: &index)
318+
utf8View.formIndex(after: &index)
304319
}
305320
// Build mantissa
306321
var tooBigToFit = false
307322

308-
while index != stringView.endIndex,
309-
let digitValue = stringView[index].digitValue {
323+
while index != utf8View.endIndex,
324+
let digitValue = utf8View[index].digitValue {
310325
defer {
311-
stringView.formIndex(after: &index)
326+
utf8View.formIndex(after: &index)
312327
}
313328
// Multiply the value by 10 and add the current digit
314329
func incrementExponent(_ decimal: inout Decimal) {
@@ -324,7 +339,7 @@ extension Decimal {
324339
if tooBigToFit {
325340
incrementExponent(&result)
326341
if result.isNaN {
327-
return (result: nil, processedLength: 0)
342+
return .overlargeValue
328343
}
329344
continue
330345
}
@@ -333,20 +348,20 @@ extension Decimal {
333348
tooBigToFit = true
334349
incrementExponent(&result)
335350
if result.isNaN {
336-
return (result: nil, processedLength: 0)
351+
return .overlargeValue
337352
}
338353
continue
339354
}
340355
result = product
341356
}
342357
// Get the decimal point
343-
if index != stringView.endIndex && stringViewContainsDecimalSeparator(at: index) {
344-
stringView.formIndex(&index, offsetBy: decimalSeparator.count)
358+
if index != utf8View.endIndex && stringViewContainsDecimalSeparator(at: index) {
359+
utf8View.formIndex(&index, offsetBy: decimalSeparator.count)
345360
// Continue to build the mantissa
346-
while index != stringView.endIndex,
347-
let digitValue = stringView[index].digitValue {
361+
while index != utf8View.endIndex,
362+
let digitValue = utf8View[index].digitValue {
348363
defer {
349-
stringView.formIndex(after: &index)
364+
utf8View.formIndex(after: &index)
350365
}
351366
guard !tooBigToFit else {
352367
continue
@@ -360,38 +375,38 @@ extension Decimal {
360375
// Before decrementing the exponent, we need to check
361376
// if it's still possible to decrement.
362377
if result._exponent == Int8.min {
363-
return (result: nil, processedLength: 0)
378+
return .overlargeValue
364379
}
365380
result._exponent -= 1
366381
}
367382
}
368383
// Get the exponent if any
369-
if index != stringView.endIndex && (stringView[index] == UInt8._E || stringView[index] == UInt8._e) {
370-
stringView.formIndex(after: &index)
384+
if index != utf8View.endIndex && (utf8View[index] == UInt8._E || utf8View[index] == UInt8._e) {
385+
utf8View.formIndex(after: &index)
371386
var exponentIsNegative = false
372387
var exponent = 0
373388
// Get the exponent sign
374-
if stringView[index] == UInt8._minus || stringView[index] == UInt8._plus {
375-
exponentIsNegative = stringView[index] == UInt8._minus
376-
stringView.formIndex(after: &index)
389+
if utf8View[index] == UInt8._minus || utf8View[index] == UInt8._plus {
390+
exponentIsNegative = utf8View[index] == UInt8._minus
391+
utf8View.formIndex(after: &index)
377392
}
378393
// Build the exponent
379-
while index != stringView.endIndex,
380-
let digitValue = stringView[index].digitValue {
394+
while index != utf8View.endIndex,
395+
let digitValue = utf8View[index].digitValue {
381396
exponent = 10 * exponent + digitValue
382397
if exponent > 2 * Int(Int8.max) {
383398
// Too big to fit
384-
return (result: nil, processedLength: 0)
399+
return .overlargeValue
385400
}
386-
stringView.formIndex(after: &index)
401+
utf8View.formIndex(after: &index)
387402
}
388403
if exponentIsNegative {
389404
exponent = -exponent
390405
}
391406
// Check to see if it will fit into the exponent field
392407
exponent += Int(result._exponent)
393408
if exponent > Int8.max || exponent < Int8.min {
394-
return (result: nil, processedLength: 0)
409+
return .overlargeValue
395410
}
396411
result._exponent = Int32(exponent)
397412
}
@@ -401,27 +416,27 @@ extension Decimal {
401416
if matchEntireString {
402417
// Trim end spaces
403418
index = skipWhiteSpaces(from: index)
404-
guard index == stringView.endIndex else {
419+
guard index == utf8View.endIndex else {
405420
// Any unprocessed content means the string
406421
// contains something not valid
407-
return (result: nil, processedLength: 0)
422+
return .parseFailure
408423
}
409424
}
410-
if index == stringView.startIndex {
425+
if index == utf8View.startIndex {
411426
// If we weren't able to process any character
412427
// the entire string isn't a valid decimal
413-
return (result: nil, processedLength: 0)
428+
return .parseFailure
414429
}
415430
result.compact()
416-
let processedLength = stringView.distance(from: stringView.startIndex, to: index)
431+
let processedLength = utf8View.distance(from: utf8View.startIndex, to: index)
417432
// if we get to this point, and have NaN,
418433
// then the input string was probably "-0"
419434
// or some variation on that, and
420435
// normalize that to zero.
421436
if result.isNaN {
422-
return (result: Decimal(0), processedLength: processedLength)
437+
return .success(Decimal(0), processedLength: processedLength)
423438
}
424-
return (result: result, processedLength: processedLength)
439+
return .success(result, processedLength: processedLength)
425440
}
426441
}
427442

Sources/FoundationEssentials/JSON/JSONDecoder.swift

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -727,20 +727,27 @@ extension JSONDecoderImpl: Decoder {
727727
// TODO: Proper handling of Infinity and NaN Decimal values.
728728
return Decimal.quietNaN
729729
} else {
730-
let numberString = String(decoding: numberBuffer, as: UTF8.self)
731-
if let decimal = Decimal(entire: numberString) {
732-
return decimal
730+
switch Decimal._decimal(from: numberBuffer, matchEntireString: true) {
731+
case .success(let result, _):
732+
return result
733+
case .overlargeValue:
734+
throw JSONError.numberIsNotRepresentableInSwift(parsed: String(decoding: numberBuffer, as: UTF8.self))
735+
case .parseFailure:
736+
throw JSON5Scanner.validateNumber(from: numberBuffer.suffix(from: digitsStartPtr), fullSource: fullSource)
733737
}
734-
throw JSON5Scanner.validateNumber(from: numberBuffer.suffix(from: digitsStartPtr), fullSource: fullSource)
738+
735739
}
736740

737741
} else {
738742
let digitsStartPtr = try JSONScanner.prevalidateJSONNumber(from: numberBuffer, hasExponent: hasExponent, fullSource: fullSource)
739-
let numberString = String(decoding: numberBuffer, as: UTF8.self)
740-
if let decimal = Decimal(entire: numberString) {
741-
return decimal
743+
switch Decimal._decimal(from: numberBuffer, matchEntireString: true) {
744+
case .success(let result, _):
745+
return result
746+
case .overlargeValue:
747+
throw JSONError.numberIsNotRepresentableInSwift(parsed: String(decoding: numberBuffer, as: UTF8.self))
748+
case .parseFailure:
749+
throw JSONScanner.validateNumber(from: numberBuffer.suffix(from: digitsStartPtr), fullSource: fullSource)
742750
}
743-
throw JSONScanner.validateNumber(from: numberBuffer.suffix(from: digitsStartPtr), fullSource: fullSource)
744751
}
745752
}
746753
}
@@ -996,8 +1003,8 @@ extension JSONDecoderImpl: Decoder {
9961003
}
9971004
}
9981005

999-
let number = String(decoding: numberBuffer, as: Unicode.ASCII.self)
1000-
if let decimal = Decimal(entire: number) {
1006+
let decimalParseResult = Decimal._decimal(from: numberBuffer, matchEntireString: true).asOptional
1007+
if let decimal = decimalParseResult.result {
10011008
guard let value = T(decimal) else {
10021009
throw JSONError.numberIsNotRepresentableInSwift(parsed: String(decoding: numberBuffer, as: UTF8.self))
10031010
}
@@ -1072,19 +1079,6 @@ extension FixedWidthInteger {
10721079
}
10731080
}
10741081

1075-
extension Decimal {
1076-
init?(entire string: String) {
1077-
guard let value = Decimal._decimal(
1078-
from: string.utf8,
1079-
decimalSeparator: ".".utf8,
1080-
matchEntireString: true
1081-
).result else {
1082-
return nil
1083-
}
1084-
self = value
1085-
}
1086-
}
1087-
10881082
extension JSONDecoderImpl : SingleValueDecodingContainer {
10891083
func decodeNil() -> Bool {
10901084
switch topValue {

Tests/FoundationEssentialsTests/JSONEncoderTests.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2613,6 +2613,11 @@ extension JSONEncoderTests {
26132613
let testBigDecimal = TestBigDecimal()
26142614
_testRoundTrip(of: testBigDecimal)
26152615
}
2616+
2617+
func testOverlargeDecimal() {
2618+
// Check value too large fails to decode.
2619+
XCTAssertThrowsError(try JSONDecoder().decode(Decimal.self, from: "100e200".data(using: .utf8)!))
2620+
}
26162621
}
26172622

26182623
// MARK: - Framework-only tests

0 commit comments

Comments
 (0)