diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 75b3fe5c4..51d45348d 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -56,10 +56,11 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = + def bigInt(num: String, max_bits: Int = 128): Option[BigInt] = + try Some(UnsafeNumbers.bigInt(num, max_bits)) + catch { case _: UnexpectedEnd | UnsafeNumber => None } + + def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } @@ -71,10 +72,7 @@ object SafeNumbers { try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 4c8e7fd29..a6a1a26c5 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -107,6 +107,50 @@ object UnsafeNumbers { else throw UnsafeNumber } + def bigInt(num: String, max_bits: Int): BigInt = + bigInt_(new FastStringReader(num), true, max_bits) + + def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 + } + } + if (consume && current != -1) throw UnsafeNumber + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return BigInt(loM10) + } + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + new BigInt(hiM10.unscaledValue) + } + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = bigInteger_(new FastStringReader(num), true, max_bits) diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 230225737..eba34303b 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -56,10 +56,11 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = + def bigInt(num: String, max_bits: Int = 128): Option[BigInt] = + try Some(UnsafeNumbers.bigInt(num, max_bits)) + catch { case _: UnexpectedEnd | UnsafeNumber => None } + + def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } @@ -71,10 +72,7 @@ object SafeNumbers { try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 4d9b34375..4691351e6 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -107,6 +107,50 @@ object UnsafeNumbers { else throw UnsafeNumber } + def bigInt(num: String, max_bits: Int): BigInt = + bigInt_(new FastStringReader(num), true, max_bits) + + def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 + } + } + if (consume && current != -1) throw UnsafeNumber + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return BigInt(loM10) + } + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + new BigInt(hiM10.unscaledValue) + } + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = bigInteger_(new FastStringReader(num), true, max_bits) diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 168369326..6f541e6f8 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -56,10 +56,11 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = + def bigInt(num: String, max_bits: Int = 128): Option[BigInt] = + try Some(UnsafeNumbers.bigInt(num, max_bits)) + catch { case _: UnexpectedEnd | UnsafeNumber => None } + + def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } @@ -71,10 +72,7 @@ object SafeNumbers { try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 74250d593..f01124bf0 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -107,6 +107,50 @@ object UnsafeNumbers { else throw UnsafeNumber } + def bigInt(num: String, max_bits: Int): BigInt = + bigInt_(new FastStringReader(num), true, max_bits) + + def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 + } + } + if (consume && current != -1) throw UnsafeNumber + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return BigInt(loM10) + } + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + new BigInt(hiM10.unscaledValue) + } + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = bigInteger_(new FastStringReader(num), true, max_bits) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index f03f30bf4..f26aedc3d 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -325,7 +325,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val int: JsonDecoder[Int] = number(Lexer.int, _.intValueExact()) implicit val long: JsonDecoder[Long] = number(Lexer.long, _.longValueExact()) implicit val bigInteger: JsonDecoder[java.math.BigInteger] = number(Lexer.bigInteger, _.toBigIntegerExact) - implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInteger, _.toBigIntegerExact) + implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInt, _.toBigIntegerExact) implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue()) implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue()) implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index b411a052f..13e0f33e2 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -78,11 +78,7 @@ object Lexer { // messages) by only checking for what we expect to see (Jon Pretty's idea). // // returns the index of the matched field, or -1 - def field( - trace: List[JsonError], - in: OneCharReader, - matrix: StringMatrix - ): Int = { + def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { val f = enumeration(trace, in, matrix) char(trace, in, ':') f @@ -181,10 +177,7 @@ object Lexer { } // useful for embedded documents, e.g. CSV contained inside JSON - def streamingString( - trace: List[JsonError], - in: OneCharReader - ): java.io.Reader = { + def streamingString(trace: List[JsonError], in: OneCharReader): java.io.Reader = { char(trace, in, '"') new OneCharReader { def close(): Unit = in.close() @@ -346,10 +339,16 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace) } - def bigInteger( - trace: List[JsonError], - in: RetractReader - ): java.math.BigInteger = + def bigInt(trace: List[JsonError], in: RetractReader): BigInt = + try { + val i = UnsafeNumbers.bigInt_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits bit BigInteger", trace) + } + + def bigInteger(trace: List[JsonError], in: RetractReader): java.math.BigInteger = try { val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) in.retract() @@ -376,10 +375,7 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace) } - def bigDecimal( - trace: List[JsonError], - in: RetractReader - ): java.math.BigDecimal = + def bigDecimal(trace: List[JsonError], in: RetractReader): java.math.BigDecimal = try { val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) in.retract() @@ -394,11 +390,7 @@ object Lexer { if (got != c) error(s"'$c'", got, trace) } - @inline def charOnly( - trace: List[JsonError], - in: OneCharReader, - c: Char - ): Unit = { + @inline def charOnly(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.readChar() if (got != c) error(s"'$c'", got, trace) } @@ -411,12 +403,7 @@ object Lexer { case _ => false } - def readChars( - trace: List[JsonError], - in: OneCharReader, - expect: Array[Char], - errMsg: String - ): Unit = { + def readChars(trace: List[JsonError], in: OneCharReader, expect: Array[Char], errMsg: String): Unit = { var i: Int = 0 while (i < expect.length) { if (in.readChar() != expect(i)) error(s"expected '$errMsg'", trace) diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index 149668481..e77ddc22c 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -54,6 +54,34 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(genAlphaLowerString)(s => assert(SafeNumbers.bigDecimal(s))(isNone)) } ), + suite("BigInt")( + test("valid BigInt edge cases") { + val inputs = List( + "0", + "0123", + "-123", + "-9223372036854775807", + "9223372036854775806", + "-9223372036854775809", + "9223372036854775808" + ) + + check(Gen.fromIterable(inputs)) { s => + assert(SafeNumbers.bigInt(s))(isSome(equalTo(BigInt(s)))) + } + }, + test("invalid BigInt edge cases") { + val inputs = List("0e+1", "01E-1", "0.1", "", "1 ") + + check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInt(s))(isNone)) + }, + test("valid BigInt") { + check(genBigInteger)(i => assert(SafeNumbers.bigInt(i.toString, 2048))(isSome(equalTo(BigInt(i))))) + }, + test("invalid BigInt") { + check(genAlphaLowerString)(s => assert(SafeNumbers.bigInt(s))(isNone)) + } + ), suite("BigInteger")( test("valid BigInteger edge cases") { val inputs = List( @@ -67,11 +95,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { ) check(Gen.fromIterable(inputs)) { s => - assert(SafeNumbers.bigInteger(s))( - isSome( - equalTo(new java.math.BigInteger(s)) - ) - ) + assert(SafeNumbers.bigInteger(s))(isSome(equalTo(new java.math.BigInteger(s)))) } }, test("invalid BigInteger edge cases") { @@ -79,7 +103,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone)) }, - test("valid big Integer") { + test("valid BigInteger") { check(genBigInteger)(i => assert(SafeNumbers.bigInteger(i.toString, 2048))(isSome(equalTo(i)))) }, test("invalid BigInteger") {