From ca6b4528fddf0f59b411f6b22afa63ea91cc803d Mon Sep 17 00:00:00 2001 From: "K.J. Valencik" Date: Mon, 6 Feb 2023 17:20:31 -0500 Subject: [PATCH] feat(neon): JsBigInt --- Cargo.lock | 115 ++++- crates/neon/src/sys/bindings/functions.rs | 34 ++ crates/neon/src/sys/tag.rs | 5 + crates/neon/src/types_impl/bigint.rs | 377 ++++++++++++++++ crates/neon/src/types_impl/mod.rs | 69 +++ test/napi/Cargo.toml | 1 + test/napi/lib/bigint.js | 9 + test/napi/package.json | 1 + test/napi/src/js/bigint.rs | 514 ++++++++++++++++++++++ test/napi/src/lib.rs | 4 + 10 files changed, 1127 insertions(+), 2 deletions(-) create mode 100644 crates/neon/src/types_impl/bigint.rs create mode 100644 test/napi/lib/bigint.js create mode 100644 test/napi/src/js/bigint.rs diff --git a/Cargo.lock b/Cargo.lock index a397ae268..18d2d5070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "cexpr" version = "0.6.0" @@ -226,6 +232,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -249,6 +258,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + [[package]] name = "linkify" version = "0.9.0" @@ -284,6 +299,7 @@ name = "napi-tests" version = "0.1.0" dependencies = [ "neon", + "num-bigint-dig", "once_cell", "tokio", ] @@ -337,6 +353,53 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.1" @@ -371,6 +434,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -422,6 +491,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.6.0" @@ -451,6 +550,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" + [[package]] name = "shlex" version = "1.1.0" @@ -459,9 +564,15 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "strsim" diff --git a/crates/neon/src/sys/bindings/functions.rs b/crates/neon/src/sys/bindings/functions.rs index dc03b5271..c59ee42df 100644 --- a/crates/neon/src/sys/bindings/functions.rs +++ b/crates/neon/src/sys/bindings/functions.rs @@ -345,6 +345,40 @@ mod napi6 { ) -> Status; fn get_instance_data(env: Env, data: *mut *mut c_void) -> Status; + + fn create_bigint_int64(env: Env, value: i64, result: *mut Value) -> Status; + + fn create_bigint_uint64(env: Env, value: u64, result: *mut Value) -> Status; + + fn create_bigint_words( + env: Env, + sign_bit: i32, + word_count: usize, + words: *const u64, + result: *mut Value, + ) -> Status; + + fn get_value_bigint_int64( + env: Env, + value: Value, + result: *mut i64, + lossless: *mut bool, + ) -> Status; + + fn get_value_bigint_uint64( + env: Env, + value: Value, + result: *mut u64, + lossless: *mut bool, + ) -> Status; + + fn get_value_bigint_words( + env: Env, + value: Value, + sign_bit: *mut i64, + word_count: *mut usize, + words: *mut u64, + ) -> Status; } ); } diff --git a/crates/neon/src/sys/tag.rs b/crates/neon/src/sys/tag.rs index 63153d00c..91b2266a6 100644 --- a/crates/neon/src/sys/tag.rs +++ b/crates/neon/src/sys/tag.rs @@ -132,3 +132,8 @@ pub unsafe fn check_object_type_tag(env: Env, object: Local, tag: &super::TypeTa ); result } + +#[cfg(feature = "napi-6")] +pub unsafe fn is_bigint(env: Env, val: Local) -> bool { + is_type(env, val, napi::ValueType::BigInt) +} diff --git a/crates/neon/src/types_impl/bigint.rs b/crates/neon/src/types_impl/bigint.rs new file mode 100644 index 000000000..e9b81dcfa --- /dev/null +++ b/crates/neon/src/types_impl/bigint.rs @@ -0,0 +1,377 @@ +//! Types for working with [`JsBigInt`]. + +use std::{error, fmt, mem::MaybeUninit}; + +use crate::prelude::NeonResult; +use crate::{ + context::Context, + handle::Handle, + result::ResultExt, + sys::{self}, + types::JsBigInt, +}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Indicates if a `JsBigInt` is positive or negative +pub enum Sign { + Positive, + Negative, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Indicates a lossless conversion from a [`JsBigInt`] to a Rust integer +/// could not be performed. +/// +/// Failures include: +/// * Negative sign on an unsigned int +/// * Overflow of an int +/// * Underflow of a signed int +pub struct Error(T); + +impl Error { + /// Get the lossy value read from a `BigInt`. It may be truncated, + /// sign extended or wrapped. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl fmt::Display for Error +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Loss of precision reading BigInt ({})", self.0) + } +} + +impl error::Error for Error where T: fmt::Display + fmt::Debug {} + +impl ResultExt for Result> +where + E: fmt::Display, +{ + fn or_throw<'a, C: Context<'a>>(self, cx: &mut C) -> NeonResult { + self.or_else(|err| cx.throw_range_error(err.to_string())) + } +} + +impl JsBigInt { + pub const POSITIVE: Sign = Sign::Positive; + pub const NEGATIVE: Sign = Sign::Negative; + + /// Creates a `BigInt` from an [`i64`]. + /// + /// # Example + /// + /// ``` + /// # use neon::{prelude::*, types::JsBigInt}; + /// # fn example(mut cx: FunctionContext) -> JsResult { + /// let value: Handle = JsBigInt::from_i64(&mut cx, 42); + /// # Ok(value) + /// # } + /// ``` + pub fn from_i64<'cx, C>(cx: &mut C, n: i64) -> Handle<'cx, Self> + where + C: Context<'cx>, + { + let mut v = MaybeUninit::uninit(); + let v = unsafe { + assert_eq!( + sys::create_bigint_int64(cx.env().to_raw(), n, v.as_mut_ptr(),), + sys::Status::Ok, + ); + + v.assume_init() + }; + + Handle::new_internal(Self(v)) + } + + /// Creates a `BigInt` from a [`u64`]. + pub fn from_u64<'cx, C>(cx: &mut C, n: u64) -> Handle<'cx, Self> + where + C: Context<'cx>, + { + let mut v = MaybeUninit::uninit(); + let v = unsafe { + assert_eq!( + sys::create_bigint_uint64(cx.env().to_raw(), n, v.as_mut_ptr(),), + sys::Status::Ok, + ); + + v.assume_init() + }; + + Handle::new_internal(Self(v)) + } + + // Internal helper for creating a _signed_ `BigInt` from a [`u128`] magnitude + fn from_u128_sign<'cx, C>(cx: &mut C, sign: Sign, n: u128) -> Handle<'cx, Self> + where + C: Context<'cx>, + { + let n = n.to_le(); + let digits = [n as u64, (n >> 64) as u64]; + + Self::from_digits_le(cx, sign, &digits) + } + + /// Creates a `BigInt` from an [`i128`]. + pub fn from_i128<'cx, C>(cx: &mut C, n: i128) -> Handle<'cx, Self> + where + C: Context<'cx>, + { + if n >= 0 { + return Self::from_u128(cx, n as u128); + } + + // Get the magnitude from a two's compliment negative + let n = u128::MAX - (n as u128) + 1; + + Self::from_u128_sign(cx, Self::NEGATIVE, n) + } + + /// Creates a `BigInt` from a [`u128`]. + pub fn from_u128<'cx, C>(cx: &mut C, n: u128) -> Handle<'cx, Self> + where + C: Context<'cx>, + { + Self::from_u128_sign(cx, Self::POSITIVE, n) + } + + /// Creates a `BigInt` from a signed magnitude (i.e., _not_ two's complement). + /// Digits are little-endian. + /// + /// # Example + /// + /// ``` + /// # use neon::{prelude::*, types::JsBigInt}; + /// # fn example(mut cx: FunctionContext) -> JsResult { + /// // Creates a `BigInt` equal to `2n ** 128n` + /// let value: Handle = JsBigInt::from_digits_le( + /// &mut cx, + /// JsBigInt::POSITIVE, + /// &[0, 0, 1], + /// ); + /// # Ok(value) + /// # } + /// ``` + pub fn from_digits_le<'cx, C>(cx: &mut C, sign: Sign, digits: &[u64]) -> Handle<'cx, Self> + where + C: Context<'cx>, + { + let sign_bit = match sign { + Sign::Positive => 0, + Sign::Negative => 1, + }; + + let mut v = MaybeUninit::uninit(); + let v = unsafe { + assert_eq!( + sys::create_bigint_words( + cx.env().to_raw(), + sign_bit, + digits.len(), + digits.as_ptr(), + v.as_mut_ptr(), + ), + sys::Status::Ok, + ); + + v.assume_init() + }; + + Handle::new_internal(Self(v)) + } + + /// Reads an `i64` from a `BigInt`. + /// + /// Fails on overflow and underflow. + pub fn to_i64<'cx, C>(&self, cx: &mut C) -> Result> + where + C: Context<'cx>, + { + let mut n = 0; + let mut lossless = false; + + unsafe { + assert_eq!( + sys::get_value_bigint_int64(cx.env().to_raw(), self.0, &mut n, &mut lossless), + sys::Status::Ok, + ); + } + + if lossless { + Ok(n) + } else { + Err(Error(n)) + } + } + + /// Reads a `u64` from a `BigInt`. + /// + /// Fails on overflow or a negative sign. + pub fn to_u64<'cx, C>(&self, cx: &mut C) -> Result> + where + C: Context<'cx>, + { + let mut n = 0; + let mut lossless = false; + + unsafe { + assert_eq!( + sys::get_value_bigint_uint64(cx.env().to_raw(), self.0, &mut n, &mut lossless), + sys::Status::Ok, + ); + } + + if lossless { + Ok(n) + } else { + Err(Error(n)) + } + } + + /// Reads an `i128` from a `BigInt`. + /// + /// Fails on overflow and underflow. + pub fn to_i128<'cx, C>(&self, cx: &mut C) -> Result> + where + C: Context<'cx>, + { + let mut digits = [0; 2]; + let (sign, num_digits) = self.read_digits_le(cx, &mut digits); + + // Cast digits into a `u128` magnitude + let n = (digits[0] as u128) | ((digits[1] as u128) << 64); + let n = u128::from_le(n); + + // Verify that the magnitude leaves room for the sign bit + let n = match sign { + Sign::Positive => { + if n > (i128::MAX as u128) { + return Err(Error(i128::MAX)); + } else { + n as i128 + } + } + Sign::Negative => { + if n > (i128::MAX as u128) + 1 { + return Err(Error(i128::MIN)); + } else { + (n as i128).wrapping_neg() + } + } + }; + + // Leading zeroes are truncated and never returned. If there are additional + // digits, the number is out of range. + if num_digits > digits.len() { + Err(Error(n)) + } else { + Ok(n) + } + } + + /// Reads a `u128` from a `BigInt`. + /// + /// Fails on overflow or a negative sign. + pub fn to_u128<'cx, C>(&self, cx: &mut C) -> Result> + where + C: Context<'cx>, + { + let mut digits = [0; 2]; + let (sign, num_digits) = self.read_digits_le(cx, &mut digits); + + // Cast digits into a `u128` magnitude + let n = (digits[0] as u128) | ((digits[1] as u128) << 64); + let n = u128::from_le(n); + + // Leading zeroes are truncated and never returned. If there are additional + // digits, the number is out of range. + if matches!(sign, Sign::Negative) || num_digits > digits.len() { + Err(Error(n)) + } else { + Ok(n) + } + } + + /// Gets a signed magnitude pair from a `BigInt`. Digits are little-endian. + pub fn to_digits_le<'cx, C>(&self, cx: &mut C) -> (Sign, Vec) + where + C: Context<'cx>, + { + let mut v = vec![0; self.len(cx)]; + let (sign, len) = self.read_digits_le(cx, &mut v); + + // It shouldn't be possible for the number of digits to change. If it + // it does, it's a correctness issue and not a soundness bug. + debug_assert_eq!(v.len(), len); + + (sign, v) + } + + /// Gets the sign from a `BigInt` and reads little-endian digits into a buffer. + /// The returned `usize` is the total number of digits in the `BigInt`. + /// + /// # Example + /// + /// Read a `u256` from a `BigInt`. + /// + /// ``` + /// # use std::error::Error; + /// # use neon::{prelude::*, types::JsBigInt}; + /// fn bigint_to_u256(cx: &mut FunctionContext, n: Handle) -> NeonResult<[u64; 4]> { + /// let mut digits = [0; 4]; + /// let (sign, num_digits) = n.read_digits_le(cx, &mut digits); + /// + /// if sign == JsBigInt::NEGATIVE { + /// return cx.throw_error("Underflow reading u256 from BigInt"); + /// } + /// + /// if num_digits > digits.len() { + /// return cx.throw_error("Overflow reading u256 from BigInt"); + /// } + /// + /// Ok(digits) + /// } + /// ``` + pub fn read_digits_le<'cx, C>(&self, cx: &mut C, digits: &mut [u64]) -> (Sign, usize) + where + C: Context<'cx>, + { + let mut sign_bit = 0; + let mut word_count = digits.len(); + + unsafe { + assert_eq!( + sys::get_value_bigint_words( + cx.env().to_raw(), + self.0, + &mut sign_bit, + &mut word_count, + digits.as_mut_ptr(), + ), + sys::Status::Ok, + ); + } + + let sign = if sign_bit == 0 { + Sign::Positive + } else { + Sign::Negative + }; + + (sign, word_count) + } + + /// Gets the number of `u64` digits in a `BigInt` + pub fn len<'cx, C>(&self, cx: &mut C) -> usize + where + C: Context<'cx>, + { + // Get the length by reading into an empty slice and ignoring the sign + self.read_digits_le(cx, &mut []).1 + } +} diff --git a/crates/neon/src/types_impl/mod.rs b/crates/neon/src/types_impl/mod.rs index 161eceba6..a40089007 100644 --- a/crates/neon/src/types_impl/mod.rs +++ b/crates/neon/src/types_impl/mod.rs @@ -1,5 +1,8 @@ // See types_docs.rs for top-level module API docs. +#[cfg(feature = "napi-6")] +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +pub mod bigint; pub(crate) mod boxed; pub mod buffer; #[cfg(feature = "napi-5")] @@ -1243,3 +1246,69 @@ impl private::ValueInternal for JsFunction { unsafe { sys::tag::is_function(env.to_raw(), other.to_raw()) } } } + +#[cfg(feature = "napi-6")] +#[cfg_attr(docsrs, doc(cfg(feature = "napi-6")))] +#[derive(Debug)] +#[repr(transparent)] +/// The type of JavaScript +/// [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) +/// values. +/// +/// # Example +/// +/// The following shows an example of adding two numbers that exceed +/// [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). +/// +/// ``` +/// # use neon::{prelude::*, types::JsBigInt}; +/// +/// fn add_bigint(mut cx: FunctionContext) -> JsResult { +/// // Get references to the `BigInt` arguments +/// let a = cx.argument::(0)?; +/// let b = cx.argument::(1)?; +/// +/// // Convert the `BigInt` to `i64` +/// let a = a.to_i64(&mut cx).or_throw(&mut cx)?; +/// let b = b.to_i64(&mut cx).or_throw(&mut cx)?; +/// let sum = a + b; +/// +/// // Create a `BigInt` from the `i64` sum +/// Ok(JsBigInt::from_i64(&mut cx, sum)) +/// } +/// ``` +pub struct JsBigInt(raw::Local); + +#[cfg(feature = "napi-6")] +impl Value for JsBigInt {} + +#[cfg(feature = "napi-6")] +unsafe impl TransparentNoCopyWrapper for JsBigInt { + type Inner = raw::Local; + + fn into_inner(self) -> Self::Inner { + self.0 + } +} + +#[cfg(feature = "napi-6")] +impl Managed for JsBigInt { + fn to_raw(&self) -> raw::Local { + self.0 + } + + fn from_raw(_: Env, h: raw::Local) -> Self { + Self(h) + } +} + +#[cfg(feature = "napi-6")] +impl private::ValueInternal for JsBigInt { + fn name() -> String { + "BigInt".to_string() + } + + fn is_typeof(env: Env, other: &Other) -> bool { + unsafe { sys::tag::is_bigint(env.to_raw(), other.to_raw()) } + } +} diff --git a/test/napi/Cargo.toml b/test/napi/Cargo.toml index 02aea60eb..99db78d17 100644 --- a/test/napi/Cargo.toml +++ b/test/napi/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] +num-bigint-dig = "0.8" once_cell = "1" tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/test/napi/lib/bigint.js b/test/napi/lib/bigint.js new file mode 100644 index 000000000..ba3c96d0d --- /dev/null +++ b/test/napi/lib/bigint.js @@ -0,0 +1,9 @@ +const addon = require(".."); + +describe("JsBigInt", () => { + const suite = addon.bigint_suite(); + + for (const [k, v] of Object.entries(suite)) { + it(k, v); + } +}); diff --git a/test/napi/package.json b/test/napi/package.json index fa9a6c6d5..692bda2f0 100644 --- a/test/napi/package.json +++ b/test/napi/package.json @@ -6,6 +6,7 @@ "license": "MIT", "scripts": { "install": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "mocha": "mocha", "test": "mocha --v8-expose-gc --timeout 5000 --recursive lib" }, "devDependencies": { diff --git a/test/napi/src/js/bigint.rs b/test/napi/src/js/bigint.rs new file mode 100644 index 000000000..241cb2e9f --- /dev/null +++ b/test/napi/src/js/bigint.rs @@ -0,0 +1,514 @@ +/// Tests for [`JsBigInt`]. All unit tests are prefixed with `test_` and exported by +/// [`bigint_suite`]. +use std::{any, cmp::PartialEq, fmt, panic, str::FromStr}; + +use neon::{ + prelude::*, + types::{ + bigint::{self, Sign}, + JsBigInt, + }, +}; + +use num_bigint_dig::BigInt; + +// Helper that converts panics to exceptions to allow `.unwrap()` usage in unit tests +fn panic_catch<'cx, F, C>(cx: &mut C, f: F) -> JsResult<'cx, JsFunction> +where + F: Fn(&mut FunctionContext) -> NeonResult<()> + 'static, + C: Context<'cx>, +{ + JsFunction::new(cx, move |mut cx| { + panic::catch_unwind(panic::AssertUnwindSafe(|| f(&mut cx))).or_else(|panic| { + if let Some(s) = panic.downcast_ref::<&str>() { + cx.throw_error(s) + } else if let Some(s) = panic.downcast_ref::() { + cx.throw_error(s) + } else { + panic::resume_unwind(panic) + } + })??; + + Ok(cx.undefined()) + }) +} + +// Export a test that is expected not to throw +fn export(cx: &mut FunctionContext, o: &JsObject, f: F) -> NeonResult<()> +where + F: Fn(&mut FunctionContext) -> NeonResult<()> + 'static, +{ + let f = panic_catch(cx, f)?; + + o.set(cx, any::type_name::(), f)?; + + Ok(()) +} + +// Export a test that is expected to return a `bigint::Error` +fn export_lossy(cx: &mut FunctionContext, o: &JsObject, f: F) -> NeonResult<()> +where + F: Fn(&mut FunctionContext) -> NeonResult>> + 'static, +{ + let f = panic_catch(cx, move |cx| { + if f(cx)?.is_err() { + return Ok(()); + } + + cx.throw_error("Expected a lossy error") + })?; + + o.set(cx, any::type_name::(), f)?; + + Ok(()) +} + +// Small helper for `eval` of a script from a Rust string. This is used +// for creating `BigInt` inline from literals (e.g., `0n`). +fn eval<'cx, C>(cx: &mut C, script: &str) -> JsResult<'cx, JsValue> +where + C: Context<'cx>, +{ + let script = cx.string(script); + + neon::reflect::eval(cx, script) +} + +// Throws an exception if `l !== r` where operands are JavaScript values +fn strict_eq<'cx, L, R, C>(l: Handle<'cx, L>, r: Handle<'cx, R>, cx: &mut C) -> NeonResult<()> +where + L: Value, + R: Value, + C: Context<'cx>, +{ + if l.strict_equals(cx, r) { + return Ok(()); + } + + let l = l.to_string(cx)?.value(cx); + let r = r.to_string(cx)?.value(cx); + + cx.throw_error(format!("Expected {l} to equal {r}")) +} + +// Throws an exception if `l != r` where operands are Rust values +fn assert_eq<'cx, L, R, C>(l: L, r: R, cx: &mut C) -> NeonResult<()> +where + L: fmt::Debug + PartialEq, + R: fmt::Debug, + C: Context<'cx>, +{ + if l == r { + return Ok(()); + } + + cx.throw_error(format!("Expected {l:?} to equal {r:?}")) +} + +// Create a `JsBigInt` from a `BigInt` +fn bigint<'cx, C>(cx: &mut C, n: &str) -> JsResult<'cx, JsBigInt> +where + C: Context<'cx>, +{ + let n = BigInt::from_str(n).or_else(|err| cx.throw_error(err.to_string()))?; + let (sign, n) = n.to_bytes_le(); + let n = n + .chunks(8) + .map(|c| { + let mut x = [0; 8]; + + (x[..c.len()]).copy_from_slice(c); + + u64::from_le_bytes(x) + }) + .collect::>(); + + let sign = if matches!(sign, num_bigint_dig::Sign::Minus) { + Sign::Negative + } else { + Sign::Positive + }; + + Ok(JsBigInt::from_digits_le(cx, sign, &n)) +} + +// Convert a `JsBigInt` to a `BigInt` +fn to_bigint<'cx, V, C>(b: Handle, cx: &mut C) -> NeonResult +where + V: Value, + C: Context<'cx>, +{ + let (sign, digits) = b.downcast_or_throw::(cx)?.to_digits_le(cx); + let sign = match sign { + Sign::Positive => num_bigint_dig::Sign::Plus, + Sign::Negative => num_bigint_dig::Sign::Minus, + }; + + Ok(BigInt::from_slice_native(sign, &digits)) +} + +fn test_from_u64(cx: &mut FunctionContext) -> NeonResult<()> { + strict_eq(JsBigInt::from_u64(cx, 0), eval(cx, "0n")?, cx)?; + strict_eq(JsBigInt::from_u64(cx, 42), eval(cx, "42n")?, cx)?; + strict_eq( + JsBigInt::from_u64(cx, u64::MAX), + eval(cx, &(u64::MAX.to_string() + "n"))?, + cx, + )?; + + Ok(()) +} + +fn test_from_i64(cx: &mut FunctionContext) -> NeonResult<()> { + strict_eq(JsBigInt::from_i64(cx, 0), eval(cx, "0n")?, cx)?; + strict_eq(JsBigInt::from_i64(cx, 42), eval(cx, "42n")?, cx)?; + strict_eq(JsBigInt::from_i64(cx, -42), eval(cx, "-42n")?, cx)?; + + strict_eq( + JsBigInt::from_i64(cx, i64::MAX), + eval(cx, &(i64::MAX.to_string() + "n"))?, + cx, + )?; + + strict_eq( + JsBigInt::from_i64(cx, i64::MIN), + eval(cx, &(i64::MIN.to_string() + "n"))?, + cx, + )?; + + Ok(()) +} + +fn test_from_u128(cx: &mut FunctionContext) -> NeonResult<()> { + strict_eq(JsBigInt::from_u128(cx, 0), eval(cx, "0n")?, cx)?; + strict_eq(JsBigInt::from_u128(cx, 42), eval(cx, "42n")?, cx)?; + + strict_eq( + JsBigInt::from_u128(cx, u128::MAX), + eval(cx, "2n ** 128n - 1n")?, + cx, + )?; + + strict_eq( + JsBigInt::from_u128(cx, u128::MAX - 1), + eval(cx, "2n ** 128n - 2n")?, + cx, + )?; + + Ok(()) +} + +fn test_from_i128(cx: &mut FunctionContext) -> NeonResult<()> { + strict_eq(JsBigInt::from_i128(cx, 0), eval(cx, "0n")?, cx)?; + strict_eq(JsBigInt::from_i128(cx, 42), eval(cx, "42n")?, cx)?; + strict_eq(JsBigInt::from_i128(cx, -42), eval(cx, "-42n")?, cx)?; + + strict_eq( + JsBigInt::from_i128(cx, i128::MAX), + eval(cx, "2n ** 127n - 1n")?, + cx, + )?; + + strict_eq( + JsBigInt::from_i128(cx, i128::MAX - 1), + eval(cx, "2n ** 127n - 2n")?, + cx, + )?; + + strict_eq( + JsBigInt::from_i128(cx, i128::MIN), + eval(cx, "-(2n ** 127n)")?, + cx, + )?; + + strict_eq( + JsBigInt::from_i128(cx, i128::MIN + 1), + eval(cx, "-(2n ** 127n - 1n)")?, + cx, + )?; + + Ok(()) +} + +fn test_from_digits_le(cx: &mut FunctionContext) -> NeonResult<()> { + strict_eq(bigint(cx, "0")?, eval(cx, "0n")?, cx)?; + strict_eq(bigint(cx, "42")?, eval(cx, "42n")?, cx)?; + strict_eq(bigint(cx, "-42")?, eval(cx, "-42n")?, cx)?; + + strict_eq( + bigint(cx, "170141183460469231731687303715884105727")?, + eval(cx, "170141183460469231731687303715884105727n")?, + cx, + )?; + + strict_eq( + bigint(cx, "-170141183460469231731687303715884105728")?, + eval(cx, "-170141183460469231731687303715884105728n")?, + cx, + )?; + + strict_eq( + bigint(cx, "10000000000000000000000000000000000000000")?, + eval(cx, "10000000000000000000000000000000000000000n")?, + cx, + )?; + + strict_eq( + bigint(cx, "-10000000000000000000000000000000000000000")?, + eval(cx, "-10000000000000000000000000000000000000000n")?, + cx, + )?; + + Ok(()) +} + +fn test_to_u64(cx: &mut FunctionContext) -> NeonResult<()> { + assert_eq(JsBigInt::from_u64(cx, 0).to_u64(cx).or_throw(cx)?, 0, cx)?; + assert_eq(JsBigInt::from_u64(cx, 42).to_u64(cx).or_throw(cx)?, 42, cx)?; + + assert_eq( + JsBigInt::from_u64(cx, u64::MAX).to_u64(cx).or_throw(cx)?, + u64::MAX, + cx, + )?; + + Ok(()) +} + +fn test_to_i64(cx: &mut FunctionContext) -> NeonResult<()> { + assert_eq(JsBigInt::from_i64(cx, 0).to_i64(cx).or_throw(cx)?, 0, cx)?; + assert_eq(JsBigInt::from_i64(cx, 42).to_i64(cx).or_throw(cx)?, 42, cx)?; + assert_eq( + JsBigInt::from_i64(cx, -42).to_i64(cx).or_throw(cx)?, + -42, + cx, + )?; + + assert_eq( + JsBigInt::from_i64(cx, i64::MAX).to_i64(cx).or_throw(cx)?, + i64::MAX, + cx, + )?; + + assert_eq( + JsBigInt::from_i64(cx, i64::MIN).to_i64(cx).or_throw(cx)?, + i64::MIN, + cx, + )?; + + Ok(()) +} + +fn test_to_u128(cx: &mut FunctionContext) -> NeonResult<()> { + assert_eq(JsBigInt::from_u128(cx, 0).to_u128(cx).or_throw(cx)?, 0, cx)?; + assert_eq( + JsBigInt::from_u128(cx, 42).to_u128(cx).or_throw(cx)?, + 42, + cx, + )?; + + assert_eq( + JsBigInt::from_u128(cx, u128::MAX) + .to_u128(cx) + .or_throw(cx)?, + u128::MAX, + cx, + )?; + + // Extra trailing zeroes + assert_eq( + JsBigInt::from_digits_le(cx, JsBigInt::POSITIVE, &[u64::MAX, u64::MAX, 0, 0, 0, 0]) + .to_u128(cx) + .or_throw(cx)?, + u128::MAX, + cx, + )?; + + Ok(()) +} + +fn test_to_i128(cx: &mut FunctionContext) -> NeonResult<()> { + assert_eq(JsBigInt::from_i128(cx, 0).to_i128(cx).or_throw(cx)?, 0, cx)?; + assert_eq( + JsBigInt::from_i128(cx, 42).to_i128(cx).or_throw(cx)?, + 42, + cx, + )?; + assert_eq( + JsBigInt::from_i128(cx, -42).to_i128(cx).or_throw(cx)?, + -42, + cx, + )?; + + assert_eq( + JsBigInt::from_i128(cx, i128::MAX) + .to_i128(cx) + .or_throw(cx)?, + i128::MAX, + cx, + )?; + + assert_eq( + JsBigInt::from_i128(cx, i128::MIN) + .to_i128(cx) + .or_throw(cx)?, + i128::MIN, + cx, + )?; + + Ok(()) +} + +fn test_to_digits_le(cx: &mut FunctionContext) -> NeonResult<()> { + assert_eq( + to_bigint(eval(cx, "0n")?, cx)?, + BigInt::from_str("0").unwrap(), + cx, + )?; + + assert_eq( + to_bigint(eval(cx, "42n")?, cx)?, + BigInt::from_str("42").unwrap(), + cx, + )?; + + assert_eq( + to_bigint(eval(cx, "-42n")?, cx)?, + BigInt::from_str("-42").unwrap(), + cx, + )?; + + assert_eq( + to_bigint(eval(cx, "170141183460469231731687303715884105727n")?, cx)?, + BigInt::from_str("170141183460469231731687303715884105727").unwrap(), + cx, + )?; + + assert_eq( + to_bigint(eval(cx, "-170141183460469231731687303715884105728n")?, cx)?, + BigInt::from_str("-170141183460469231731687303715884105728").unwrap(), + cx, + )?; + + assert_eq( + to_bigint(eval(cx, "10000000000000000000000000000000000000000n")?, cx)?, + BigInt::from_str("10000000000000000000000000000000000000000").unwrap(), + cx, + )?; + + assert_eq( + to_bigint(eval(cx, "-10000000000000000000000000000000000000000n")?, cx)?, + BigInt::from_str("-10000000000000000000000000000000000000000").unwrap(), + cx, + )?; + + Ok(()) +} + +fn test_very_large_number(cx: &mut FunctionContext) -> NeonResult<()> { + // 2048-bit prime generated with `crypto.generatePrimeSync(2048)` + // Note: Unlike the rest of the tests, this number is big-endian + let n = BigInt::from_bytes_be( + num_bigint_dig::Sign::Plus, + &[ + 228, 178, 58, 23, 125, 164, 107, 153, 254, 98, 85, 252, 29, 61, 8, 237, 212, 36, 173, + 205, 116, 52, 16, 155, 131, 82, 59, 211, 132, 139, 212, 101, 10, 26, 60, 44, 172, 86, + 50, 42, 9, 124, 188, 236, 77, 46, 209, 64, 239, 34, 99, 8, 235, 165, 5, 41, 159, 211, + 186, 197, 140, 111, 43, 15, 111, 132, 255, 148, 36, 12, 25, 221, 208, 162, 234, 45, 22, + 13, 251, 157, 103, 50, 181, 2, 53, 81, 15, 137, 129, 10, 130, 212, 74, 125, 80, 188, + 19, 218, 236, 189, 234, 145, 234, 232, 9, 218, 167, 111, 33, 62, 81, 96, 83, 125, 242, + 217, 179, 211, 109, 16, 210, 250, 133, 130, 86, 182, 110, 213, 74, 78, 34, 210, 88, 3, + 178, 73, 231, 53, 188, 187, 76, 247, 205, 154, 190, 200, 211, 75, 63, 34, 246, 160, + 193, 98, 7, 85, 40, 208, 47, 157, 34, 120, 235, 136, 101, 88, 174, 149, 180, 114, 197, + 230, 116, 47, 152, 253, 212, 191, 90, 151, 204, 6, 51, 179, 73, 128, 141, 192, 107, 74, + 205, 130, 56, 115, 202, 96, 79, 187, 196, 49, 118, 18, 251, 34, 64, 208, 38, 25, 35, + 195, 231, 195, 201, 224, 110, 205, 213, 92, 192, 23, 48, 165, 126, 145, 18, 30, 230, + 83, 229, 187, 138, 177, 74, 15, 209, 151, 83, 160, 246, 77, 59, 228, 57, 112, 165, 4, + 10, 11, 95, 213, 115, 187, 240, 57, 5, 117, + ], + ); + + assert_eq(to_bigint(eval(cx, &(n.to_string() + "n"))?, cx)?, n, cx)?; + + Ok(()) +} + +fn test_i64_out_of_range(cx: &mut FunctionContext) -> NeonResult>> { + Ok(JsBigInt::from_i128(cx, (i64::MIN as i128) - 1).to_i64(cx)) +} + +fn test_u64_out_of_range(cx: &mut FunctionContext) -> NeonResult>> { + Ok(JsBigInt::from_u128(cx, (u64::MAX as u128) + 1).to_u64(cx)) +} + +fn test_i128_extra_digits( + cx: &mut FunctionContext, +) -> NeonResult>> { + let res = eval(cx, "2n ** 128n")? + .downcast_or_throw::(cx)? + .to_i128(cx); + + Ok(res) +} + +fn test_i128_overflow(cx: &mut FunctionContext) -> NeonResult>> { + let res = eval(cx, "2n ** 127n")? + .downcast_or_throw::(cx)? + .to_i128(cx); + + Ok(res) +} + +fn test_i128_underflow(cx: &mut FunctionContext) -> NeonResult>> { + let res = eval(cx, "-(2n ** 127n + 1n)")? + .downcast_or_throw::(cx)? + .to_i128(cx); + + Ok(res) +} + +fn test_u128_overflow(cx: &mut FunctionContext) -> NeonResult>> { + let res = eval(cx, "2n ** 127n")? + .downcast_or_throw::(cx)? + .to_i128(cx); + + Ok(res) +} + +fn test_u128_underflow(cx: &mut FunctionContext) -> NeonResult>> { + let res = eval(cx, "-1n")? + .downcast_or_throw::(cx)? + .to_u128(cx); + + Ok(res) +} + +// Creates a map (object) of test name to functions to be executed by a JavaScript +// test runner. +pub fn bigint_suite(mut cx: FunctionContext) -> JsResult { + let o = cx.empty_object(); + + // `Ok` tests + export(&mut cx, &o, test_from_u64)?; + export(&mut cx, &o, test_from_i64)?; + export(&mut cx, &o, test_from_u128)?; + export(&mut cx, &o, test_from_i128)?; + export(&mut cx, &o, test_from_digits_le)?; + export(&mut cx, &o, test_to_u64)?; + export(&mut cx, &o, test_to_i64)?; + export(&mut cx, &o, test_to_u128)?; + export(&mut cx, &o, test_to_i128)?; + export(&mut cx, &o, test_to_digits_le)?; + export(&mut cx, &o, test_very_large_number)?; + + // `Err` tests + export_lossy(&mut cx, &o, test_i64_out_of_range)?; + export_lossy(&mut cx, &o, test_u64_out_of_range)?; + export_lossy(&mut cx, &o, test_i128_extra_digits)?; + export_lossy(&mut cx, &o, test_i128_overflow)?; + export_lossy(&mut cx, &o, test_i128_underflow)?; + export_lossy(&mut cx, &o, test_u128_overflow)?; + export_lossy(&mut cx, &o, test_u128_underflow)?; + + Ok(o) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index c2bb89204..90ccb7e7f 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -7,6 +7,7 @@ use crate::js::{ mod js { pub mod arrays; + pub mod bigint; pub mod boxed; pub mod coercions; pub mod date; @@ -393,5 +394,8 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("lazy_async_add", js::futures::lazy_async_add)?; cx.export_function("lazy_async_sum", js::futures::lazy_async_sum)?; + // JsBigInt test suite + cx.export_function("bigint_suite", js::bigint::bigint_suite)?; + Ok(()) }