diff --git a/Sources/Codable.swift b/Sources/Codable.swift index b53b30b..2bac869 100644 --- a/Sources/Codable.swift +++ b/Sources/Codable.swift @@ -102,28 +102,42 @@ extension Array where Element: FixedWidthInteger { extension BigInt: Codable { public init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() + if let container = try? decoder.singleValueContainer(), let stringValue = try? container.decode(String.self) { + if stringValue.hasPrefix("0x") || stringValue.hasPrefix("0X") { + guard let bigUInt = BigUInt(stringValue.dropFirst(2), radix: 16) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid hexadecimal BigInt string") + } + self.init(sign: .plus, magnitude: bigUInt) + } else { + guard let bigInt = BigInt(stringValue) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid decimal BigInt string") + } + self = bigInt + } + } else { + var container = try decoder.unkeyedContainer() - // Decode sign - let sign: BigInt.Sign - switch try container.decode(String.self) { - case "+": - sign = .plus - case "-": - sign = .minus - default: - throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, - debugDescription: "Invalid big integer sign")) - } + // Decode sign + let sign: BigInt.Sign + switch try container.decode(String.self) { + case "+": + sign = .plus + case "-": + sign = .minus + default: + throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, + debugDescription: "Invalid big integer sign")) + } - // Decode magnitude - let words = try [UInt](count: container.count?.advanced(by: -1)) { () -> UInt64? in - guard !container.isAtEnd else { return nil } - return try container.decode(UInt64.self) - } - let magnitude = BigUInt(words: words) + // Decode magnitude + let words = try [UInt](count: container.count?.advanced(by: -1)) { () -> UInt64? in + guard !container.isAtEnd else { return nil } + return try container.decode(UInt64.self) + } + let magnitude = BigUInt(words: words) - self.init(sign: sign, magnitude: magnitude) + self.init(sign: sign, magnitude: magnitude) + } } public func encode(to encoder: Encoder) throws { diff --git a/Tests/BigIntTests/BigIntTests.swift b/Tests/BigIntTests/BigIntTests.swift index 76da4a5..e382eac 100644 --- a/Tests/BigIntTests/BigIntTests.swift +++ b/Tests/BigIntTests/BigIntTests.swift @@ -663,6 +663,44 @@ class BigIntTests: XCTestCase { XCTAssertEqual(context.debugDescription, "Invalid big integer sign") } } + + func testDecodableString() { + func test(_ a: BigInt, _ v: String? = nil, file: StaticString = #file, line: UInt = #line) { + do { + let json = try JSONEncoder().encode(v ?? a.description) + let b = try JSONDecoder().decode(BigInt.self, from: json) + XCTAssertEqual(a, b, file: file, line: line) + } catch let error { + XCTFail("Error thrown: \(error.localizedDescription)", file: file, line: line) + } + } + + test(1, "1") + test(1, "+1") + test(-1, "-1") + test(0, "+0") + test(0, "-0") + test(15, "0xf") + test(15, "0Xf") + test(15, "0x0f") + test(BigInt(1) << 64) + test(-BigInt(1) << 64) + } + + func testDecodableStringError() { + func test(_ v: String, _ m: String) { + XCTAssertThrowsError(try JSONDecoder().decode(BigInt.self, from: try! JSONEncoder().encode(v))) { error in + guard let error = error as? DecodingError else { XCTFail("Expected a decoding error"); return } + guard case .dataCorrupted(let context) = error else { XCTFail("Expected a dataCorrupted error"); return } + XCTAssertEqual(m, context.debugDescription) + } + } + + test("124q", "Invalid decimal BigInt string") + test("-124q", "Invalid decimal BigInt string") + test("0xXYZ", "Invalid hexadecimal BigInt string") + } + func testConversionToData() { func test(_ b: BigInt, _ d: Array, file: StaticString = #file, line: UInt = #line) {