From 6bb465d1d5d8084e1a5c23985cdca9aedecd1705 Mon Sep 17 00:00:00 2001 From: deadcafe Date: Wed, 14 Oct 2026 19:56:58 +0530 Subject: [PATCH] Encode Decimals with positive exponents instead of trapping Motivation: Foundation stores trailing zeros in a Decimal as a positive exponent, so even Decimal(100) has significand 1 and exponent 2. The encoder passed that exponent straight to PseudoDecimal, which only accepts exponents from -3 to 0, so encoding it hit a precondition failure. Any other out-of-range Decimal trapped the same way rather than throwing. Modifications: Move the three copies of the Decimal to PseudoDecimal conversion into a single throwing initializer. It folds a positive exponent into the significand and validates the result, throwing invalidIntegerOrDecimal when the value cannot be represented. Expose PseudoDecimal.validate at package level so the encoder can use it. Add an encoder test covering items, lists and out-of-range values. Result: Decimals with positive exponents encode correctly, and unrepresentable Decimals throw instead of crashing. --- .../PseudoDecimal.swift | 2 +- .../Encoder/StructuredFieldValueEncoder.swift | 46 +++++++++++-------- .../StructuredFieldEncoderTests.swift | 10 ++++ 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Sources/RawStructuredFieldValues/PseudoDecimal.swift b/Sources/RawStructuredFieldValues/PseudoDecimal.swift index e5cf92c..7687cb0 100644 --- a/Sources/RawStructuredFieldValues/PseudoDecimal.swift +++ b/Sources/RawStructuredFieldValues/PseudoDecimal.swift @@ -115,7 +115,7 @@ public struct PseudoDecimal: Hashable, Sendable { } } - private static func validate(mantissa: Int64, exponent: Int8) throws { + package static func validate(mantissa: Int64, exponent: Int8) throws { // We compare against our upper and lower bounds. The maximum allowable magnitude of the mantissa is contingent // on the exponent. switch exponent { diff --git a/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift b/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift index ac8530f..7859852 100644 --- a/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift +++ b/Sources/StructuredFieldValues/Encoder/StructuredFieldValueEncoder.swift @@ -299,12 +299,7 @@ extension _StructuredFieldEncoder: SingleValueEncodingContainer { } func encode(_ data: Decimal) throws { - let significand = (data.significand.magnitude as NSNumber).intValue // Yes, really. - guard let exponent = Int8(exactly: data.exponent) else { - throw StructuredHeaderError.invalidIntegerOrDecimal - } - - let pd = PseudoDecimal(mantissa: significand * (data.isSignMinus ? -1 : 1), exponent: Int(exponent)) + let pd = try PseudoDecimal(data) try self.currentStackEntry.storage.insertBareItem(.decimal(pd)) } @@ -469,12 +464,7 @@ extension _StructuredFieldEncoder { } func append(_ value: Decimal) throws { - let significand = (value.significand.magnitude as NSNumber).intValue // Yes, really. - guard let exponent = Int8(exactly: value.exponent) else { - throw StructuredHeaderError.invalidIntegerOrDecimal - } - - let pd = PseudoDecimal(mantissa: significand * (value.isSignMinus ? -1 : 1), exponent: Int(exponent)) + let pd = try PseudoDecimal(value) try self.currentStackEntry.storage.appendBareItem(.decimal(pd)) } @@ -652,12 +642,7 @@ extension _StructuredFieldEncoder { } func encode(_ value: Decimal, forKey key: String) throws { - let significand = (value.significand.magnitude as NSNumber).intValue // Yes, really. - guard let exponent = Int8(exactly: value.exponent) else { - throw StructuredHeaderError.invalidIntegerOrDecimal - } - - let pd = PseudoDecimal(mantissa: significand * (value.isSignMinus ? -1 : 1), exponent: Int(exponent)) + let pd = try PseudoDecimal(value) try self.currentStackEntry.storage.insertBareItem(.decimal(pd), atKey: key) } @@ -994,3 +979,28 @@ extension Item { self.init(bareItem: partialItem.bareItem!, parameters: partialItem.parameters) } } + +extension PseudoDecimal { + fileprivate init(_ decimal: Decimal) throws { + var significand = (decimal.significand.magnitude as NSNumber).intValue // Yes, really. + var decimalExponent = decimal.exponent + + // Foundation stores trailing zeros as a positive exponent (100 is 1e2), so fold them into the significand. + while decimalExponent > 0 { + let (scaled, overflow) = significand.multipliedReportingOverflow(by: 10) + guard !overflow else { + throw StructuredHeaderError.invalidIntegerOrDecimal + } + significand = scaled + decimalExponent -= 1 + } + + guard let exponent = Int8(exactly: decimalExponent) else { + throw StructuredHeaderError.invalidIntegerOrDecimal + } + + let mantissa = significand * (decimal.isSignMinus ? -1 : 1) + try Self.validate(mantissa: Int64(mantissa), exponent: exponent) + self.init(mantissa: mantissa, exponent: Int(exponent)) + } +} diff --git a/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift b/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift index a6ce1b8..0392b7f 100644 --- a/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift +++ b/Tests/StructuredFieldValuesTests/StructuredFieldEncoderTests.swift @@ -61,6 +61,16 @@ final class StructuredFieldEncoderTests: XCTestCase { ) } + func testEncodeDecimalWithPositiveExponent() throws { + // Foundation stores trailing zeros as a positive exponent, so 100 is 1e2. + let encoder = StructuredFieldValueEncoder() + XCTAssertEqual(Array("100.0".utf8), try encoder.encode(ItemField(Decimal(100)))) + XCTAssertEqual(Array("900000000000.0".utf8), try encoder.encode(ItemField(Decimal(string: "9e11")!))) + XCTAssertEqual(Array("-100.0, 100.0".utf8), try encoder.encode(List([Decimal(-100), Decimal(100)]))) + XCTAssertThrowsError(try encoder.encode(ItemField(Decimal(string: "1e12")!))) + XCTAssertThrowsError(try encoder.encode(List([Decimal(string: "1e12")!]))) + } + func testEncodeKeyedItemHeader() throws { struct KeyedItem: Equatable, StructuredFieldValue { static var structuredFieldType: StructuredFieldType {