diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4f2cd62..c2c5356 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Unreleased +----- + +* Added bigi.from_bytes() and bigi.to_bytes() functions. + 3.0.0 ----- diff --git a/src/bigi.gleam b/src/bigi.gleam index 578c9fb..2d0439a 100644 --- a/src/bigi.gleam +++ b/src/bigi.gleam @@ -1,9 +1,23 @@ -import gleam/order import gleam/dynamic +import gleam/order /// A big integer. pub type BigInt +/// Endianness specifier used when converting between a big integer and +/// raw bytes. +pub type Endianness { + LittleEndian + BigEndian +} + +/// Signedness specifier used when converting between a big integer and +/// raw bytes. +pub type Signedness { + Signed + Unsigned +} + /// Create a big integer representing zero. @external(erlang, "bigi_ffi", "zero") @external(javascript, "./bigi_ffi.mjs", "zero") @@ -30,6 +44,18 @@ pub fn from_int(int: Int) -> BigInt @external(javascript, "./bigi_ffi.mjs", "from_string") pub fn from_string(str: String) -> Result(BigInt, Nil) +/// Convert raw bytes into a big integer. +/// +/// If the bit array does not contain a whole number of bytes then an error is +/// returned. +@external(erlang, "bigi_ffi", "from_bytes") +@external(javascript, "./bigi_ffi.mjs", "from_bytes") +pub fn from_bytes( + bytes: BitArray, + endianness: Endianness, + signedness: Signedness, +) -> Result(BigInt, Nil) + /// Convert a big integer to a regular integer. /// /// In Erlang, this cannot fail, as all Erlang integers are big integers. In the @@ -46,6 +72,20 @@ pub fn to_int(bigint: BigInt) -> Result(Int, Nil) @external(javascript, "./bigi_ffi.mjs", "to_string") pub fn to_string(bigint: BigInt) -> String +/// Convert a big integer to raw bytes. +/// +/// The size of the returned bit array is specified by `byte_count`, e.g. 8 will +/// return a bit array containing 8 bytes (64 bits). If the big integer doesn't +/// fit in the specified number of bytes then an error is returned. +@external(erlang, "bigi_ffi", "to_bytes") +@external(javascript, "./bigi_ffi.mjs", "to_bytes") +pub fn to_bytes( + bigint: BigInt, + endianness: Endianness, + signedness: Signedness, + byte_count: Int, +) -> Result(BitArray, Nil) + /// Compare two big integers, returning an order that denotes if `a` is lower, /// bigger than, or equal to `b`. @external(erlang, "bigi_ffi", "compare") diff --git a/src/bigi_ffi.erl b/src/bigi_ffi.erl index b4da818..13355b9 100644 --- a/src/bigi_ffi.erl +++ b/src/bigi_ffi.erl @@ -3,7 +3,9 @@ -export([ from/1, from_string/1, + from_bytes/3, to/1, + to_bytes/4, zero/0, compare/2, add/2, @@ -27,8 +29,60 @@ from_string(Str) -> {Int, _} -> {ok, Int} end. +from_bytes(Bytes, Endianness, Signedness) -> + BitSize = erlang:bit_size(Bytes), + + case BitSize rem 8 of + 0 -> case Endianness of + little_endian -> case Signedness of + signed -> + <> = Bytes, + {ok, Int}; + unsigned -> + <> = Bytes, + {ok, Int} + end; + big_endian -> case Signedness of + signed -> + <> = Bytes, + {ok, Int}; + unsigned -> + <> = Bytes, + {ok, Int} + end + end; + + _ -> {error, nil} + end. + to(BigInt) -> {ok, BigInt}. +to_bytes(BigInt, Endianness, Signedness, ByteCount) -> + case ByteCount * 8 of + BitCount when BitCount >= 8 -> + RangeMin = case Signedness of + signed -> -(1 bsl (BitCount - 1)); + unsigned -> 0 + end, + + RangeMax = case Signedness of + signed -> (1 bsl (BitCount - 1)) - 1; + unsigned -> (1 bsl BitCount) - 1 + end, + + % Error if the value is out of range for the available bits + case BigInt >= RangeMin andalso BigInt =< RangeMax of + true -> + case Endianness of + little_endian -> {ok, <> }; + big_endian -> {ok, <>} + end; + + false -> {error, nil} + end; + + _ -> {error, nil} + end. zero() -> 0. compare(A, B) when A < B -> lt; diff --git a/src/bigi_ffi.mjs b/src/bigi_ffi.mjs index 3b381de..31e5d13 100644 --- a/src/bigi_ffi.mjs +++ b/src/bigi_ffi.mjs @@ -1,6 +1,7 @@ -import { Ok, Error, toList } from "./gleam.mjs"; +import { Ok, Error, toList, BitArray } from "./gleam.mjs"; import { Lt, Eq, Gt } from "../gleam_stdlib/gleam/order.mjs"; import { DecodeError } from "../gleam_stdlib/gleam/dynamic.mjs"; +import { BigEndian, Signed } from "./bigi.mjs"; export function from(int) { return BigInt(int); @@ -14,6 +15,35 @@ export function from_string(string) { } } +export function from_bytes(bit_array, endianness, signedness) { + let value = 0n; + + // Read bytes as an unsigned integer value + if (endianness instanceof BigEndian) { + for (let i = 0; i < bit_array.length; i++) { + value = value * 256n + BigInt(bit_array.byteAt(i)); + } + } else { + for (let i = bit_array.length - 1; i >= 0; i--) { + value = value * 256n + BigInt(bit_array.byteAt(i)); + } + } + + if (signedness instanceof Signed) { + const byteSize = BigInt(bit_array.length); + + const highBit = 2n ** (byteSize * 8n - 1n); + + // If the high bit is set and this is a signed integer, reinterpret as + // two's complement + if (value >= highBit) { + value -= highBit * 2n; + } + } + + return new Ok(value); +} + export function to(bigint) { if (bigint > Number.MAX_SAFE_INTEGER || bigint < Number.MIN_SAFE_INTEGER) { return new Error(undefined); @@ -26,6 +56,52 @@ export function to_string(bigint) { return bigint.toString(); } +export function to_bytes(bigint, endianness, signedness, byte_count) { + const bit_count = BigInt(byte_count * 8); + + if (bit_count < 8n) { + return new Error(undefined); + } + + let range_min = 0n; + let range_max = 0n; + + // Error if the value is out of range for the available bits + if (signedness instanceof Signed) { + range_min = -(2n ** (bit_count - 1n)); + range_max = -range_min - 1n; + } else { + range_max = 2n ** bit_count - 1n; + } + + if (bigint < range_min || bigint > range_max) { + return new Error(undefined); + } + + // Convert negative number to two's complement representation + if (bigint < 0) { + bigint = (1n << bit_count) + bigint; + } + + const byteArray = new Uint8Array(byte_count); + + if (endianness instanceof BigEndian) { + for (let i = byteArray.length - 1; i >= 0; i--) { + const byte = bigint % 256n; + byteArray[i] = Number(byte); + bigint = (bigint - byte) / 256n; + } + } else { + for (let i = 0; i < byteArray.length; i++) { + const byte = bigint % 256n; + byteArray[i] = Number(byte); + bigint = (bigint - byte) / 256n; + } + } + + return new Ok(new BitArray(byteArray)); +} + export function zero() { return 0n; } diff --git a/test/bigi_test.gleam b/test/bigi_test.gleam index 7ec4a76..d1f56b4 100644 --- a/test/bigi_test.gleam +++ b/test/bigi_test.gleam @@ -1,8 +1,10 @@ -import gleam/order +import bigi +import gleam/bit_array import gleam/dynamic +import gleam/list +import gleam/order import gleeunit import gleeunit/should -import bigi const js_max_safe_int = 9_007_199_254_740_991 @@ -16,7 +18,7 @@ pub fn main() { pub fn zero_test() { should.equal( bigi.zero() - |> bigi.to_int(), + |> bigi.to_int(), Ok(0), ) } @@ -35,7 +37,7 @@ pub fn absolute_test() { pub fn add_test() { should.equal( bigi.add(bigi.from_int(js_max_safe_int), bigi.from_int(2)) - |> bigi.to_string(), + |> bigi.to_string(), "9007199254740993", ) } @@ -43,7 +45,7 @@ pub fn add_test() { pub fn subtract_test() { should.equal( bigi.subtract(bigi.from_int(js_min_safe_int), bigi.from_int(2)) - |> bigi.to_string(), + |> bigi.to_string(), "-9007199254740993", ) } @@ -54,7 +56,7 @@ pub fn multiply_test() { bigi.from_int(js_max_safe_int), bigi.from_int(js_max_safe_int), ) - |> bigi.to_string(), + |> bigi.to_string(), "81129638414606663681390495662081", ) } @@ -174,6 +176,59 @@ pub fn from_bad_string_test() { should.be_error(parsed) } +pub fn from_bytes_test() { + [ + #("4328719365", <<1, 2, 3, 4, 5>>, bigi.BigEndian, bigi.Unsigned), + #( + "-4685255690516759574262", + <<255, 2, 3, 4, 5, 6, 7, 8, 9, 10>>, + bigi.BigEndian, + bigi.Signed, + ), + #("21542142465", <<1, 2, 3, 4, 5>>, bigi.LittleEndian, bigi.Unsigned), + #( + "-4555767348510506941951", + <<1, 2, 3, 4, 5, 6, 7, 8, 9, 255>>, + bigi.LittleEndian, + bigi.Signed, + ), + ] + |> list.each(fn(x) { + let #(bigi_string, bytes, endianness, signedness) = x + + let assert Ok(i) = bigi.from_string(bigi_string) + + bigi.from_bytes(bytes, endianness, signedness) + |> should.equal(Ok(i)) + + bigi.to_bytes(i, endianness, signedness, bit_array.byte_size(bytes)) + |> should.equal(Ok(bytes)) + }) + + // Check error is returned when the integer is out of range + bigi.to_bytes( + bigi.from_int(4_294_967_296), + bigi.LittleEndian, + bigi.Unsigned, + 4, + ) + |> should.equal(Error(Nil)) + + bigi.to_bytes(bigi.from_int(-1), bigi.LittleEndian, bigi.Unsigned, 4) + |> should.equal(Error(Nil)) + + bigi.to_bytes(bigi.from_int(2_147_483_648), bigi.LittleEndian, bigi.Signed, 4) + |> should.equal(Error(Nil)) + + bigi.to_bytes( + bigi.from_int(-2_147_483_649), + bigi.LittleEndian, + bigi.Signed, + 4, + ) + |> should.equal(Error(Nil)) +} + pub fn power_test() { let assert Ok(bigint) = bigi.power(bigi.from_int(2), bigi.from_int(65_535))