From 5878b03966ba528d65e59dbdcf9e26d1f49ff1a0 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:04:22 +0400 Subject: [PATCH 01/12] test: unify web and VM test expectations Remove platform-specific test expectations in preparation for cross-platform fixes. Co-Authored-By: Claude Opus 4.5 --- test/decoder_rfc_test.dart | 7 +- test/encoder_rfc_test.dart | 55 +++------------ test/json_encoder_test.dart | 17 +++-- test/rfc8746_test.dart | 10 +-- test/simple_decoder_rfc_test.dart | 13 +--- test/simple_encoder_rfc_test.dart | 107 ++++++++---------------------- test/supplementary_test.dart | 7 +- 7 files changed, 50 insertions(+), 166 deletions(-) diff --git a/test/decoder_rfc_test.dart b/test/decoder_rfc_test.dart index eb30203..53b5168 100644 --- a/test/decoder_rfc_test.dart +++ b/test/decoder_rfc_test.dart @@ -9,7 +9,6 @@ import 'package:cbor/cbor.dart'; import 'package:test/test.dart'; void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC Appendix A Diagnostics decoder tests -> ', () { test('0', () { final decoded = cbor.decode([0x00]); @@ -232,11 +231,7 @@ void main() { test('-4.0', () { final decoded = cbor.decode([0xf9, 0xc4, 0x00]); - if (kIsWeb) { - expect(decoded, CborFloat(-33554428)); - } else { - expect(decoded, CborFloat(-4.0)); - } + expect(decoded, CborFloat(-4.0)); }); test('-4.1', () { diff --git a/test/encoder_rfc_test.dart b/test/encoder_rfc_test.dart index 1f279d4..fd167d2 100644 --- a/test/encoder_rfc_test.dart +++ b/test/encoder_rfc_test.dart @@ -9,7 +9,6 @@ import 'package:cbor/cbor.dart'; import 'package:test/test.dart'; void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC Appendix A Diagnostics encoder tests -> ', () { test('0', () { final encoded = cbor.encode(CborSmallInt(0)); @@ -91,11 +90,7 @@ void main() { final encoded = cbor.encode( CborInt(BigInt.parse('-18446744073709551616')), ); - if (kIsWeb) { - expect(encoded, [0x3b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } else { - expect(encoded, [0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); - } + expect(encoded, [0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); }); test('-18446744073709551617', () { @@ -149,47 +144,27 @@ void main() { test('1.0', () { final encoded = cbor.encode(CborFloat(1.0)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x3c, 0x00]); - } + expect(encoded, [0xf9, 0x3c, 0x00]); }); test('1.5', () { final encoded = cbor.encode(CborFloat(1.5)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x3e, 0x00]); - } + expect(encoded, [0xf9, 0x3e, 0x00]); }); test('65504.0', () { final encoded = cbor.encode(CborFloat(65504.0)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x7b, 0xff]); - } + expect(encoded, [0xf9, 0x7b, 0xff]); }); test('100000.0', () { final encoded = cbor.encode(CborFloat(100000.0)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xfa, 0x47, 0xc3, 0x50, 0x00]); - } + expect(encoded, [0xfa, 0x47, 0xc3, 0x50, 0x00]); }); test('3.4028234663852886e+38', () { final encoded = cbor.encode(CborFloat(3.4028234663852886e+38)); - if (kIsWeb) { - expect(encoded, [0xfb, 0x47, 0xef, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00]); - } else { - expect(encoded, [0xfa, 0x7f, 0x7f, 0xff, 0xff]); - } + expect(encoded, [0xfa, 0x7f, 0x7f, 0xff, 0xff]); }); test('1.0e+300', () { @@ -199,29 +174,17 @@ void main() { test('5.960464477539063e-8', () { final encoded = cbor.encode(CborFloat(5.960464477539063e-8)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x00, 0x01]); - } + expect(encoded, [0xf9, 0x00, 0x01]); }); test('0.00006103515625', () { final encoded = cbor.encode(CborFloat(0.00006103515625)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x04, 0x00]); - } + expect(encoded, [0xf9, 0x04, 0x00]); }); test('-4.0', () { final encoded = cbor.encode(CborFloat(-4.0)); - if (kIsWeb) { - expect(encoded, [0xf9, 0x80, 0x00]); - } else { - expect(encoded, [0xf9, 0xc4, 0x00]); - } + expect(encoded, [0xf9, 0xc4, 0x00]); }); test('-4.1', () { diff --git a/test/json_encoder_test.dart b/test/json_encoder_test.dart index cccfd6c..c5d7356 100644 --- a/test/json_encoder_test.dart +++ b/test/json_encoder_test.dart @@ -15,7 +15,6 @@ String encode(Object? input) { } void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC Appendix A Diagnostics encoder tests -> ', () { test('0', () { expect(encode(0), '0'); @@ -59,7 +58,7 @@ void main() { test('0.0', () { expect(encode(0.0), '0.0'); - }, skip: kIsWeb); + }); test('-0.0', () { expect(encode(-0.0), '-0.0'); @@ -67,7 +66,7 @@ void main() { test('1.0', () { expect(encode(1.0), '1.0'); - }, skip: kIsWeb); + }); test('1.5', () { expect(encode(1.5), '1.5'); @@ -75,19 +74,19 @@ void main() { test('65504.0', () { expect(encode(65504.0), '65504.0'); - }, skip: kIsWeb); + }); test('100000.0', () { expect(encode(100000.0), '100000.0'); - }, skip: kIsWeb); + }); test('3.4028234663852886e+38', () { expect(encode(3.4028234663852886e+38), '3.4028234663852886e+38'); - }, skip: kIsWeb); + }); test('1.0e+300', () { expect(encode(1.0e+300), '1e+300'); - }, skip: kIsWeb); + }); test('5.960464477539063e-8', () { expect(encode(5.960464477539063e-8), '5.960464477539063e-8'); @@ -99,7 +98,7 @@ void main() { test('Infinity', () { expect(encode(double.infinity), 'null'); - }, skip: kIsWeb); + }); test('Nan', () { expect(encode(double.nan), 'null'); @@ -107,7 +106,7 @@ void main() { test('-Infinity', () { expect(encode(double.negativeInfinity), 'null'); - }, skip: kIsWeb); + }); test('False', () { expect(encode(false), 'false'); diff --git a/test/rfc8746_test.dart b/test/rfc8746_test.dart index 4fd4479..f45290b 100644 --- a/test/rfc8746_test.dart +++ b/test/rfc8746_test.dart @@ -11,7 +11,6 @@ import 'package:cbor/cbor.dart'; import 'package:test/test.dart'; void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC 8746 Typed Arrays -> ', () { test('Tag 64 Uint8Array', () { final encoded = [ @@ -117,13 +116,8 @@ void main() { final decoded = cbor.decode(encoded); expect(decoded, isA()); final array = decoded as CborUint64BigEndianArray; - if (!kIsWeb) { - expect(array.toObject(), isA()); - expect(array.toObject(), [1, 2]); - } else { - expect(array.toObject(), isA()); - expect(array.toObject(), [5e-324, 1e-323]); - } + expect(array.toObject(), isA()); + expect(array.toObject(), [1, 2]); }); test('Tag 72 Int8Array', () { diff --git a/test/simple_decoder_rfc_test.dart b/test/simple_decoder_rfc_test.dart index c88157a..d1a171d 100644 --- a/test/simple_decoder_rfc_test.dart +++ b/test/simple_decoder_rfc_test.dart @@ -9,7 +9,6 @@ import 'package:cbor/simple.dart'; import 'package:test/test.dart'; void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC Appendix A Diagnostics decoder tests -> ', () { test('0', () { final decoded = cbor.decode([0x00]); @@ -115,11 +114,7 @@ void main() { 0xff, 0xff, ]); - if (kIsWeb) { - expect(decoded, int.parse('-18446744073709552000')); - } else { - expect(decoded, BigInt.parse('-18446744073709551616')); - } + expect(decoded, BigInt.parse('-18446744073709551616')); }); test('-18446744073709551617', () { @@ -236,11 +231,7 @@ void main() { test('-4.0', () { final decoded = cbor.decode([0xf9, 0xc4, 0x00]); - if (kIsWeb) { - expect(decoded, -33554428); - } else { - expect(decoded, -4.0); - } + expect(decoded, -4.0); }); test('-4.1', () { diff --git a/test/simple_encoder_rfc_test.dart b/test/simple_encoder_rfc_test.dart index 846d4b5..41ffc78 100644 --- a/test/simple_encoder_rfc_test.dart +++ b/test/simple_encoder_rfc_test.dart @@ -11,7 +11,6 @@ import 'package:cbor/simple.dart'; import 'package:test/test.dart'; void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC Appendix A Diagnostics encoder tests -> ', () { test('0', () { final encoded = cbor.encode(0); @@ -70,32 +69,24 @@ void main() { test('18446744073709551616', () { final encoded = cbor.encode(BigInt.parse('18446744073709551616')); - if (kIsWeb) { - expect(encoded, [0x1b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } else { - expect(encoded, [ - 0xc2, - 0x49, - 0x01, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); - } + expect(encoded, [ + 0xc2, + 0x49, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); }); test('-18446744073709551616', () { final encoded = cbor.encode(BigInt.parse('-18446744073709551616')); - if (kIsWeb) { - expect(encoded, [0x3b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); - } else { - expect(encoded, [0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); - } + expect(encoded, [0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); }); test('-18446744073709551617', () { @@ -137,101 +128,57 @@ void main() { test('0.0', () { final encoded = cbor.encode(0.0); - if (kIsWeb) { - expect(encoded, [0x00]); - } else { - expect(encoded, [0xf9, 0x00, 0x00]); - } + expect(encoded, [0xf9, 0x00, 0x00]); }); test('-0.0', () { final encoded = cbor.encode(-0.0); - if (kIsWeb) { - expect(encoded, [0xff]); - } else { - expect(encoded, [0xf9, 0x80, 0x00]); - } + expect(encoded, [0xf9, 0x80, 0x00]); }); test('1.0', () { final encoded = cbor.encode(1.0); - if (kIsWeb) { - expect(encoded, [0x01]); - } else { - expect(encoded, [0xf9, 0x3c, 0x00]); - } + expect(encoded, [0xf9, 0x3c, 0x00]); }); test('1.5', () { final encoded = cbor.encode(1.5); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x3e, 0x00]); - } + expect(encoded, [0xf9, 0x3e, 0x00]); }); test('65504.0', () { final encoded = cbor.encode(65504.0); - if (kIsWeb) { - expect(encoded, [0x19, 0xff, 0xe0]); - } else { - expect(encoded, [0xf9, 0x7b, 0xff]); - } + expect(encoded, [0xf9, 0x7b, 0xff]); }); test('100000.0', () { final encoded = cbor.encode(100000.0); - if (kIsWeb) { - expect(encoded, [0x1a, 0x00, 0x01, 0x86, 0xa0]); - } else { - expect(encoded, [0xfa, 0x47, 0xc3, 0x50, 0x00]); - } + expect(encoded, [0xfa, 0x47, 0xc3, 0x50, 0x00]); }); test('3.4028234663852886e+38', () { final encoded = cbor.encode(3.4028234663852886e+38); - if (kIsWeb) { - expect(encoded, [0x1b, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00]); - } else { - expect(encoded, [0xfa, 0x7f, 0x7f, 0xff, 0xff]); - } + expect(encoded, [0xfa, 0x7f, 0x7f, 0xff, 0xff]); }); test('1.0e+300', () { final encoded = cbor.encode(1.0e+300); - if (kIsWeb) { - expect(encoded, [0x1b, 0xbf, 0x21, 0xe4, 0x40, 0x03, 0xac, 0xe0, 0x00]); - } else { - expect(encoded, [0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); - } + expect(encoded, [0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); }); test('5.960464477539063e-8', () { final encoded = cbor.encode(5.960464477539063e-8); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x00, 0x01]); - } + expect(encoded, [0xf9, 0x00, 0x01]); }); test('0.00006103515625', () { final encoded = cbor.encode(0.00006103515625); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x04, 0x00]); - } + expect(encoded, [0xf9, 0x04, 0x00]); }); test('-4.0', () { final encoded = cbor.encode(-4.0); - if (kIsWeb) { - expect(encoded, [0x23]); - } else { - expect(encoded, [0xf9, 0xc4, 0x00]); - } + expect(encoded, [0xf9, 0xc4, 0x00]); }); test('-4.1', () { @@ -242,7 +189,7 @@ void main() { test('Infinity', () { final encoded = cbor.encode(double.infinity); expect(encoded, [0xf9, 0x7c, 0x00]); - }, skip: kIsWeb); + }); test('Nan', () { final encoded = cbor.encode(double.nan); @@ -252,7 +199,7 @@ void main() { test('-Infinity', () { final encoded = cbor.encode(double.negativeInfinity); expect(encoded, [0xf9, 0xfc, 0x00]); - }, skip: kIsWeb); + }); test('False', () { final encoded = cbor.encode(false); diff --git a/test/supplementary_test.dart b/test/supplementary_test.dart index 1150453..8732fa7 100644 --- a/test/supplementary_test.dart +++ b/test/supplementary_test.dart @@ -11,7 +11,6 @@ import 'package:cbor/cbor.dart'; import 'package:test/test.dart'; void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('Known patterns', () { test('Pattern 1 -> ', () { final encoded = cbor.encode( @@ -325,11 +324,7 @@ void main() { } on ArgumentError { raised = true; } - if (kIsWeb) { - expect(raised, false); - } else { - expect(raised, true); - } + expect(raised, true); }); test('Float - value to large', () { bool raised = false; From fb68841ad5070de06c31589d1f6a713353c6fc94 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:05:13 +0400 Subject: [PATCH 02/12] fix: implement cross-platform half-precision float encoding The ieee754 package's toFloat16Bytes() uses ByteData.setInt16() which behaves incorrectly on JS where bitwise operators are limited to 32-bit signed integers. This implementation constructs bytes directly. Co-Authored-By: Claude Opus 4.5 --- lib/src/utils/float16.dart | 156 +++++++++++++++++++++++++++++++++++++ lib/src/value/double.dart | 5 +- 2 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 lib/src/utils/float16.dart diff --git a/lib/src/utils/float16.dart b/lib/src/utils/float16.dart new file mode 100644 index 0000000..e0ce757 --- /dev/null +++ b/lib/src/utils/float16.dart @@ -0,0 +1,156 @@ +/* + * Package : Cbor + * Author : S. Hamblett + * Date : 21/01/2026 + * Copyright : S.Hamblett + */ + +import 'dart:typed_data'; + +/// Custom half-precision (float16) encoding that works correctly on all platforms. +/// +/// The ieee754 package's `toFloat16Bytes()` uses `ByteData.setInt16()` which +/// behaves incorrectly on JS where bitwise operators are limited to 32-bit +/// signed integers. This implementation constructs bytes directly. + +/// Converts a double to IEEE 754 half-precision (16-bit) bytes in big-endian order. +/// +/// Returns a list of 2 bytes `[highByte, lowByte]`. +List toFloat16Bytes(double value) { + // Handle special cases + if (value.isNaN) { + // Canonical NaN: 0x7e00 + return [0x7e, 0x00]; + } + + if (value.isInfinite) { + // +Infinity: 0x7c00, -Infinity: 0xfc00 + return value.isNegative ? [0xfc, 0x00] : [0x7c, 0x00]; + } + + // Handle zero (including signed zero) + if (value == 0.0) { + // Check for negative zero + final bytes = Uint8List(8); + ByteData.view(bytes.buffer).setFloat64(0, value, Endian.big); + final isNegativeZero = bytes[0] == 0x80; + return isNegativeZero ? [0x80, 0x00] : [0x00, 0x00]; + } + + // Extract components from double using ByteData + final doubleBytes = Uint8List(8); + ByteData.view(doubleBytes.buffer).setFloat64(0, value, Endian.big); + + // Extract sign, exponent, and mantissa from IEEE 754 double (64-bit) + // Double format: 1 sign + 11 exponent + 52 mantissa + final sign = (doubleBytes[0] >> 7) & 1; + final exponent = + ((doubleBytes[0] & 0x7F) << 4) | ((doubleBytes[1] >> 4) & 0x0F); + // We only need the top 10 bits of the mantissa for half-precision + final mantissaHigh = + ((doubleBytes[1] & 0x0F) << 6) | ((doubleBytes[2] >> 2) & 0x3F); + + // Double exponent bias is 1023, half-precision bias is 15 + // Adjust exponent: half_exp = double_exp - 1023 + 15 = double_exp - 1008 + final adjustedExponent = exponent - 1023 + 15; + + int halfSign = sign; + int halfExponent; + int halfMantissa; + + if (adjustedExponent <= 0) { + // Subnormal or zero in half-precision + if (adjustedExponent < -10) { + // Too small, round to zero + halfExponent = 0; + halfMantissa = 0; + } else { + // Subnormal: shift mantissa right and add implicit leading 1 + halfExponent = 0; + final shift = 1 - adjustedExponent; + // Add implicit leading bit and shift + halfMantissa = (0x400 | mantissaHigh) >> shift; + } + } else if (adjustedExponent >= 31) { + // Overflow to infinity + halfExponent = 31; + halfMantissa = 0; + } else { + // Normal number + halfExponent = adjustedExponent; + halfMantissa = mantissaHigh; + } + + // Construct the 16-bit half-precision value + // Format: 1 sign + 5 exponent + 10 mantissa + final halfValue = (halfSign << 15) | (halfExponent << 10) | halfMantissa; + + // Return as big-endian bytes + return [(halfValue >> 8) & 0xFF, halfValue & 0xFF]; +} + +/// Check if a double can be represented losslessly as a half-precision float. +/// +/// This is a cross-platform implementation that doesn't rely on platform-specific +/// behavior. +bool isFloat16Lossless(double value) { + // Special values are always lossless + if (value.isNaN || value.isInfinite || value == 0.0) { + return true; + } + + // Convert to float16 and back, check if value is preserved + final halfBytes = toFloat16Bytes(value); + final reconstructed = _float16BytesToDouble(halfBytes); + + return reconstructed == value; +} + +/// Converts half-precision bytes back to double (for lossless check). +double _float16BytesToDouble(List bytes) { + final halfValue = (bytes[0] << 8) | bytes[1]; + + final sign = (halfValue >> 15) & 1; + final exponent = (halfValue >> 10) & 0x1F; + final mantissa = halfValue & 0x3FF; + + double result; + + if (exponent == 0) { + if (mantissa == 0) { + // Zero + result = 0.0; + } else { + // Subnormal + result = mantissa / 1024.0 * (1.0 / 16384.0); // 2^-14 + } + } else if (exponent == 31) { + if (mantissa == 0) { + result = double.infinity; + } else { + result = double.nan; + } + } else { + // Normal number + result = (1.0 + mantissa / 1024.0) * _pow2(exponent - 15); + } + + return sign == 1 ? -result : result; +} + +/// Power of 2 helper that avoids dart:math dependency. +double _pow2(int exp) { + if (exp >= 0) { + double result = 1.0; + for (var i = 0; i < exp; i++) { + result *= 2.0; + } + return result; + } else { + double result = 1.0; + for (var i = 0; i > exp; i--) { + result /= 2.0; + } + return result; + } +} diff --git a/lib/src/value/double.dart b/lib/src/value/double.dart index f6808c3..6f363e2 100644 --- a/lib/src/value/double.dart +++ b/lib/src/value/double.dart @@ -11,6 +11,7 @@ import 'package:ieee754/ieee754.dart'; import '../constants.dart'; import '../encoder/sink.dart'; +import '../utils/float16.dart' as float16_utils; import 'internal.dart'; /// Enumeration to allow the user to select which level of precision to @@ -89,7 +90,7 @@ class _CborFloatImpl with CborValueMixin implements CborFloat { CborAdditionalInfo.halfPrecisionFloat, ); - sink.add(parts.toFloat16Bytes()); + sink.add(float16_utils.toFloat16Bytes(value)); } else if (parts.isFloat32Lossless) { sink.addHeader( CborMajorType.simpleFloat, @@ -113,7 +114,7 @@ class _CborFloatImpl with CborValueMixin implements CborFloat { CborMajorType.simpleFloat, CborAdditionalInfo.halfPrecisionFloat, ); - sink.add(parts.toFloat16Bytes()); + sink.add(float16_utils.toFloat16Bytes(value)); } else { // Invalid conversion throw ArgumentError( From 8fab3696388ac5ed54403022b4b39f1b5ab70a95 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:05:33 +0400 Subject: [PATCH 03/12] fix: handle Infinity encoding by checking special doubles first On JS, double.infinity and double.nan might incorrectly match the int type check, leading to BigInt.from(infinity) which throws. This fix checks for NaN/Infinity before the int type check. Co-Authored-By: Claude Opus 4.5 --- lib/src/value/value.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/value/value.dart b/lib/src/value/value.dart index 2a6cd62..e5bde57 100644 --- a/lib/src/value/value.dart +++ b/lib/src/value/value.dart @@ -125,6 +125,10 @@ abstract class CborValue { return CborNull(); } else if (object is CborValue) { return object; + } else if (object is double && (object.isNaN || object.isInfinite)) { + // Handle special double values first: on JS, Infinity/NaN might match + // 'is int', leading to BigInt.from(infinity) throwing. + return CborFloat(object); } else if (object is int) { return CborSmallInt(object); } else if (object is BigInt) { From e7dc722e637265db88da573f583586c21d23b6c4 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:05:39 +0400 Subject: [PATCH 04/12] fix: use explicit safe integer bounds for BigInt handling on JS BigInt.isValidInt is unreliable on JS for values > 2^53-1. This fix adds explicit bitLength checks to ensure safe integer handling across all platforms. - arg.dart: operator ~() now returns ArgBigInt when result exceeds JS safe integer range - int.dart: CborInt factory uses explicit safe integer check Co-Authored-By: Claude Opus 4.5 --- lib/src/utils/arg.dart | 14 +++++++++----- lib/src/value/int.dart | 5 ++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/src/utils/arg.dart b/lib/src/utils/arg.dart index 1765801..887fa7b 100644 --- a/lib/src/utils/arg.dart +++ b/lib/src/utils/arg.dart @@ -5,8 +5,6 @@ * Copyright : S.Hamblett */ -import '../constants.dart'; - /// The information encoded by additional Arg and the following bytes. abstract class Arg { static const Arg indefiniteLength = _ArgIndefiniteLength(); @@ -55,9 +53,15 @@ class _ArgInt implements Arg { const _ArgInt(this.value); @override - _ArgInt operator ~() => _ArgInt( - (~BigInt.from(value)).toSigned(CborConstants.bigIntSlice).toInt(), - ); + Arg operator ~() { + final result = ~BigInt.from(value); + // Check if result fits in JS safe integer range (2^53-1) + // Use bitLength <= 53 as a conservative check + if (result.bitLength <= 53 && result.isValidInt) { + return _ArgInt(result.toInt()); + } + return _ArgBigInt(result); + } @override int toInt() => value; diff --git a/lib/src/value/int.dart b/lib/src/value/int.dart index 927dd5b..d984dd8 100644 --- a/lib/src/value/int.dart +++ b/lib/src/value/int.dart @@ -21,7 +21,10 @@ import 'internal.dart'; /// If the incoming value has less than 53 bits, it is [CborSmallInt]. abstract class CborInt extends CborValue { factory CborInt(BigInt value, {List? tags}) { - if (value.isValidInt) { + // Use explicit check: JS safe integer is 2^53-1 + // bitLength <= 53 is a conservative check that works on all platforms + final isSafeInt = value.bitLength <= 53 && value.isValidInt; + if (isSafeInt) { return CborSmallInt(value.toInt(), tags: tags ?? const []); } From 7a897515723eacc29452afe05b8ac0280ca19997 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:05:45 +0400 Subject: [PATCH 05/12] fix: add web platform support for Uint64/Int64 typed arrays ByteData.getUint64()/setUint64() don't exist on JS/WASM. This fix: - Adds platform detection constants for JS and WASM - Implements 64-bit integer read/write using two 32-bit operations - Uses multiplication instead of bit shifts to avoid JS limitations - Properly handles signed integers via two's complement conversion Co-Authored-By: Claude Opus 4.5 --- lib/src/value/typed_data.dart | 167 +++++++++++++++++++++++++++++----- 1 file changed, 143 insertions(+), 24 deletions(-) diff --git a/lib/src/value/typed_data.dart b/lib/src/value/typed_data.dart index d9b7be6..b6951fa 100644 --- a/lib/src/value/typed_data.dart +++ b/lib/src/value/typed_data.dart @@ -184,6 +184,11 @@ class CborUint32LittleEndianArray extends CborTypedArray { } } +/// Platform detection constants +const bool _kIsJs = bool.fromEnvironment('dart.library.js_interop'); +const bool _kIsWasm = bool.fromEnvironment('dart.tool.dart2wasm'); +const bool _kIsWeb = _kIsJs || _kIsWasm; + /// uint64 big endian class CborUint64BigEndianArray extends CborTypedArray { const CborUint64BigEndianArray(super.bytes, {super.tags}); @@ -192,7 +197,16 @@ class CborUint64BigEndianArray extends CborTypedArray { final bytes = Uint8List(list.length * 8); final data = ByteData.view(bytes.buffer); for (var i = 0; i < list.length; i++) { - data.setUint64(i * 8, list[i], Endian.big); + if (_kIsWeb) { + // Web: write as two 32-bit values (avoid setUint64 which doesn't exist) + final value = list[i]; + // High 32 bits: use integer division to avoid bit shift issues on JS + data.setUint32(i * 8, (value ~/ 0x100000000) & 0xFFFFFFFF, Endian.big); + // Low 32 bits + data.setUint32(i * 8 + 4, value & 0xFFFFFFFF, Endian.big); + } else { + data.setUint64(i * 8, list[i], Endian.big); + } } return CborUint64BigEndianArray( bytes, @@ -202,13 +216,12 @@ class CborUint64BigEndianArray extends CborTypedArray { @override Object? toObjectInternal(Set cyclicCheck, ToObjectOptions o) { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); if (bytes.length % 8 != 0) { throw CborMalformedException( 'Uint64ArrayBE byte length not multiple of 8', ); } - if (!kIsWeb) { + if (!_kIsWeb) { final list = Uint64List(bytes.length ~/ 8); final data = ByteData.sublistView(Uint8List.fromList(bytes)); for (var i = 0; i < list.length; i++) { @@ -216,10 +229,14 @@ class CborUint64BigEndianArray extends CborTypedArray { } return list; } else { - final list = Float64List(bytes.length ~/ 8); + // Web: read as two 32-bit values and combine into int + final list = []; final data = ByteData.sublistView(Uint8List.fromList(bytes)); - for (var i = 0; i < list.length; i++) { - list[i] = data.getFloat64(i * 8, Endian.big); + for (var i = 0; i < bytes.length ~/ 8; i++) { + final high = data.getUint32(i * 8, Endian.big); + final low = data.getUint32(i * 8 + 4, Endian.big); + // Combine: use multiplication to avoid << 32 issues on JS + list.add((high * 0x100000000) + low); } return list; } @@ -237,7 +254,15 @@ class CborUint64LittleEndianArray extends CborTypedArray { final bytes = Uint8List(list.length * 8); final data = ByteData.view(bytes.buffer); for (var i = 0; i < list.length; i++) { - data.setUint64(i * 8, list[i], Endian.little); + if (_kIsWeb) { + // Web: write as two 32-bit values (avoid setUint64 which doesn't exist) + final value = list[i]; + // Little endian: low bytes first + data.setUint32(i * 8, value & 0xFFFFFFFF, Endian.little); + data.setUint32(i * 8 + 4, (value ~/ 0x100000000) & 0xFFFFFFFF, Endian.little); + } else { + data.setUint64(i * 8, list[i], Endian.little); + } } return CborUint64LittleEndianArray( bytes, @@ -252,12 +277,26 @@ class CborUint64LittleEndianArray extends CborTypedArray { 'Uint64ArrayLE byte length not multiple of 8', ); } - final list = Uint64List(bytes.length ~/ 8); - final data = ByteData.sublistView(Uint8List.fromList(bytes)); - for (var i = 0; i < list.length; i++) { - list[i] = data.getUint64(i * 8, Endian.little); + if (!_kIsWeb) { + final list = Uint64List(bytes.length ~/ 8); + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + for (var i = 0; i < list.length; i++) { + list[i] = data.getUint64(i * 8, Endian.little); + } + return list; + } else { + // Web: read as two 32-bit values and combine into int + final list = []; + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + for (var i = 0; i < bytes.length ~/ 8; i++) { + // Little endian: low bytes first + final low = data.getUint32(i * 8, Endian.little); + final high = data.getUint32(i * 8 + 4, Endian.little); + // Combine: use multiplication to avoid << 32 issues on JS + list.add((high * 0x100000000) + low); + } + return list; } - return list; } } @@ -423,7 +462,25 @@ class CborInt64BigEndianArray extends CborTypedArray { final bytes = Uint8List(list.length * 8); final data = ByteData.view(bytes.buffer); for (var i = 0; i < list.length; i++) { - data.setInt64(i * 8, list[i], Endian.big); + if (_kIsWeb) { + // Web: write as two 32-bit values (avoid setInt64 which doesn't exist) + var value = list[i]; + int high, low; + if (value < 0) { + // For negative values, compute two's complement representation + // Negate, subtract 1, then invert each part + final magnitude = -value - 1; + high = (~(magnitude ~/ 0x100000000)) & 0xFFFFFFFF; + low = (~(magnitude & 0xFFFFFFFF)) & 0xFFFFFFFF; + } else { + high = (value ~/ 0x100000000) & 0xFFFFFFFF; + low = value & 0xFFFFFFFF; + } + data.setUint32(i * 8, high, Endian.big); + data.setUint32(i * 8 + 4, low, Endian.big); + } else { + data.setInt64(i * 8, list[i], Endian.big); + } } return CborInt64BigEndianArray( bytes, @@ -438,12 +495,33 @@ class CborInt64BigEndianArray extends CborTypedArray { 'Int64ArrayBE byte length not multiple of 8', ); } - final list = Int64List(bytes.length ~/ 8); - final data = ByteData.sublistView(Uint8List.fromList(bytes)); - for (var i = 0; i < list.length; i++) { - list[i] = data.getInt64(i * 8, Endian.big); + if (!_kIsWeb) { + final list = Int64List(bytes.length ~/ 8); + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + for (var i = 0; i < list.length; i++) { + list[i] = data.getInt64(i * 8, Endian.big); + } + return list; + } else { + // Web: read as two 32-bit values and combine into signed int + final list = []; + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + for (var i = 0; i < bytes.length ~/ 8; i++) { + final high = data.getUint32(i * 8, Endian.big); + final low = data.getUint32(i * 8 + 4, Endian.big); + // Check if negative (high bit set) + if (high >= 0x80000000) { + // Two's complement: invert bits and add 1 to get magnitude + final invHigh = (~high) & 0xFFFFFFFF; + final invLow = (~low) & 0xFFFFFFFF; + final magnitude = (invHigh * 0x100000000) + invLow + 1; + list.add(-magnitude); + } else { + list.add((high * 0x100000000) + low); + } + } + return list; } - return list; } } @@ -458,7 +536,26 @@ class CborInt64LittleEndianArray extends CborTypedArray { final bytes = Uint8List(list.length * 8); final data = ByteData.view(bytes.buffer); for (var i = 0; i < list.length; i++) { - data.setInt64(i * 8, list[i], Endian.little); + if (_kIsWeb) { + // Web: write as two 32-bit values (avoid setInt64 which doesn't exist) + var value = list[i]; + int high, low; + if (value < 0) { + // For negative values, compute two's complement representation + // Negate, subtract 1, then invert each part + final magnitude = -value - 1; + high = (~(magnitude ~/ 0x100000000)) & 0xFFFFFFFF; + low = (~(magnitude & 0xFFFFFFFF)) & 0xFFFFFFFF; + } else { + high = (value ~/ 0x100000000) & 0xFFFFFFFF; + low = value & 0xFFFFFFFF; + } + // Little endian: low bytes first + data.setUint32(i * 8, low, Endian.little); + data.setUint32(i * 8 + 4, high, Endian.little); + } else { + data.setInt64(i * 8, list[i], Endian.little); + } } return CborInt64LittleEndianArray( bytes, @@ -473,12 +570,34 @@ class CborInt64LittleEndianArray extends CborTypedArray { 'Int64ArrayLE byte length not multiple of 8', ); } - final list = Int64List(bytes.length ~/ 8); - final data = ByteData.sublistView(Uint8List.fromList(bytes)); - for (var i = 0; i < list.length; i++) { - list[i] = data.getInt64(i * 8, Endian.little); + if (!_kIsWeb) { + final list = Int64List(bytes.length ~/ 8); + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + for (var i = 0; i < list.length; i++) { + list[i] = data.getInt64(i * 8, Endian.little); + } + return list; + } else { + // Web: read as two 32-bit values and combine into signed int + final list = []; + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + for (var i = 0; i < bytes.length ~/ 8; i++) { + // Little endian: low bytes first + final low = data.getUint32(i * 8, Endian.little); + final high = data.getUint32(i * 8 + 4, Endian.little); + // Check if negative (high bit set) + if (high >= 0x80000000) { + // Two's complement: invert bits and add 1 to get magnitude + final invHigh = (~high) & 0xFFFFFFFF; + final invLow = (~low) & 0xFFFFFFFF; + final magnitude = (invHigh * 0x100000000) + invLow + 1; + list.add(-magnitude); + } else { + list.add((high * 0x100000000) + low); + } + } + return list; } - return list; } } From 0ea1245d14d09a0cf66aa69324216fb885761d56 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:05:50 +0400 Subject: [PATCH 06/12] test: update Uint64 test to work cross-platform Remove Uint64List type expectation since web platforms return List instead. The value check still ensures correctness. Co-Authored-By: Claude Opus 4.5 --- test/rfc8746_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/rfc8746_test.dart b/test/rfc8746_test.dart index f45290b..ada75de 100644 --- a/test/rfc8746_test.dart +++ b/test/rfc8746_test.dart @@ -116,7 +116,7 @@ void main() { final decoded = cbor.decode(encoded); expect(decoded, isA()); final array = decoded as CborUint64BigEndianArray; - expect(array.toObject(), isA()); + // On web, returns List instead of Uint64List expect(array.toObject(), [1, 2]); }); From fa89b049e8db68439bbbbd3a283b1d77d226654d Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:11:06 +0400 Subject: [PATCH 07/12] fix: implement cross-platform half-precision float decoding The ieee754 package's fromFloat16Bytes() uses ByteData.getInt16() which interprets bytes as a signed integer on JS. For values with the high bit set (negative numbers, large exponents), this produces incorrect results. For example, -4.0 (bytes 0xC400) was decoded as -33554428 instead of -4.0. This fix: - Adds fromFloat16Bytes() to custom float16.dart utility - Uses only unsigned byte operations that work consistently across platforms - Updates all decoder call sites to use the custom implementation - Documents the rationale in the utility file Co-Authored-By: Claude Opus 4.5 --- lib/src/decoder/pretty_print.dart | 3 ++- lib/src/decoder/stage3.dart | 3 ++- lib/src/utils/float16.dart | 35 ++++++++++++++++++++++++------- lib/src/value/typed_data.dart | 19 +++++++---------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/lib/src/decoder/pretty_print.dart b/lib/src/decoder/pretty_print.dart index bbe6f65..46c35f9 100644 --- a/lib/src/decoder/pretty_print.dart +++ b/lib/src/decoder/pretty_print.dart @@ -9,6 +9,7 @@ import 'dart:convert'; import 'package:ieee754/ieee754.dart'; +import '../utils/float16.dart' as float16_utils; import 'stage1.dart'; import '../constants.dart'; import '../value/value.dart'; @@ -140,7 +141,7 @@ class _PrettyPrint implements Sink { break; case CborAdditionalInfo.halfPrecisionFloat: writer.write( - '(${FloatParts.fromFloat16Bytes(x.header.dataBytes).toDouble()})', + '(${float16_utils.fromFloat16Bytes(x.header.dataBytes)})', ); break; case CborAdditionalInfo.singlePrecisionFloat: diff --git a/lib/src/decoder/stage3.dart b/lib/src/decoder/stage3.dart index a17c399..3e6c6f3 100644 --- a/lib/src/decoder/stage3.dart +++ b/lib/src/decoder/stage3.dart @@ -10,6 +10,7 @@ import 'package:cbor/src/value/internal.dart'; import 'package:collection/collection.dart'; import 'package:ieee754/ieee754.dart'; +import '../utils/float16.dart' as float16_utils; import '../utils/utils.dart'; import '../constants.dart'; import 'stage2.dart'; @@ -257,7 +258,7 @@ CborFloat _createFloat(RawValueTagged raw) { final double value; switch (raw.header.additionalInfo) { case CborAdditionalInfo.halfPrecisionFloat: - value = FloatParts.fromFloat16Bytes(raw.header.dataBytes).toDouble(); + value = float16_utils.fromFloat16Bytes(raw.header.dataBytes); break; case CborAdditionalInfo.singlePrecisionFloat: value = FloatParts.fromFloat32Bytes(raw.header.dataBytes).toDouble(); diff --git a/lib/src/utils/float16.dart b/lib/src/utils/float16.dart index e0ce757..c68132d 100644 --- a/lib/src/utils/float16.dart +++ b/lib/src/utils/float16.dart @@ -7,11 +7,27 @@ import 'dart:typed_data'; -/// Custom half-precision (float16) encoding that works correctly on all platforms. +/// Custom half-precision (float16) encoding/decoding that works correctly on all platforms. /// -/// The ieee754 package's `toFloat16Bytes()` uses `ByteData.setInt16()` which -/// behaves incorrectly on JS where bitwise operators are limited to 32-bit -/// signed integers. This implementation constructs bytes directly. +/// ## Why custom implementation? +/// +/// The ieee754 package has issues on JavaScript platforms: +/// +/// ### Encoding (`toFloat16Bytes`) +/// The ieee754 package uses `ByteData.setInt16()` which behaves incorrectly on JS +/// where bitwise operators are limited to 32-bit signed integers. When the sign bit +/// is set (negative numbers, negative zero), the value gets sign-extended incorrectly. +/// +/// ### Decoding (`fromFloat16Bytes`) +/// The ieee754 package uses `ByteData.getInt16()` to read the 16-bit value, which +/// on JS interprets the bytes as a signed integer. For values with the high bit set +/// (negative numbers, large positive exponents), this produces incorrect results. +/// For example, -4.0 (bytes 0xC400) gets decoded as -33554428 instead of -4.0. +/// +/// This implementation avoids these issues by: +/// - Constructing bytes directly without using setInt16/getInt16 +/// - Using only unsigned byte operations that work consistently across platforms +/// - Building the 16-bit value from individual bytes using shifts and OR operations /// Converts a double to IEEE 754 half-precision (16-bit) bytes in big-endian order. /// @@ -101,13 +117,18 @@ bool isFloat16Lossless(double value) { // Convert to float16 and back, check if value is preserved final halfBytes = toFloat16Bytes(value); - final reconstructed = _float16BytesToDouble(halfBytes); + final reconstructed = fromFloat16Bytes(halfBytes); return reconstructed == value; } -/// Converts half-precision bytes back to double (for lossless check). -double _float16BytesToDouble(List bytes) { +/// Converts IEEE 754 half-precision (16-bit) bytes to a double. +/// +/// Expects 2 bytes in big-endian order `[highByte, lowByte]`. +/// +/// This is a cross-platform implementation that avoids `ByteData.getInt16()` +/// which behaves incorrectly on JS for values with the high bit set. +double fromFloat16Bytes(List bytes) { final halfValue = (bytes[0] << 8) | bytes[1]; final sign = (halfValue >> 15) & 1; diff --git a/lib/src/value/typed_data.dart b/lib/src/value/typed_data.dart index b6951fa..5e91784 100644 --- a/lib/src/value/typed_data.dart +++ b/lib/src/value/typed_data.dart @@ -9,7 +9,8 @@ import 'dart:typed_data'; import 'package:cbor/cbor.dart'; import 'package:cbor/src/value/internal.dart'; -import 'package:ieee754/ieee754.dart'; + +import '../utils/float16.dart' as float16_utils; /// Base class for all Typed Arrays sealed class CborTypedArray extends CborBytesImpl { @@ -615,11 +616,8 @@ class CborFloat16BigEndianArray extends CborTypedArray { final list = []; final data = ByteData.sublistView(Uint8List.fromList(bytes)); for (var i = 0; i < bytes.length; i += 2) { - final chunk = Uint8List.fromList([ - data.getUint8(i), - data.getUint8(i + 1), - ]); - list.add(FloatParts.fromFloat16Bytes(chunk).toDouble()); + final chunk = [data.getUint8(i), data.getUint8(i + 1)]; + list.add(float16_utils.fromFloat16Bytes(chunk)); } return list; } @@ -639,12 +637,9 @@ class CborFloat16LittleEndianArray extends CborTypedArray { final list = []; final data = ByteData.sublistView(Uint8List.fromList(bytes)); for (var i = 0; i < bytes.length; i += 2) { - // Reverse for Little Endian as library expects Big Endian - final chunk = Uint8List.fromList([ - data.getUint8(i + 1), - data.getUint8(i), - ]); - list.add(FloatParts.fromFloat16Bytes(chunk).toDouble()); + // Reverse for Little Endian as our decoder expects Big Endian + final chunk = [data.getUint8(i + 1), data.getUint8(i)]; + list.add(float16_utils.fromFloat16Bytes(chunk)); } return list; } From ec2090a465a623e89ceaa34c081c3dc72066e0fd Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:42:40 +0400 Subject: [PATCH 08/12] fix: replace ieee754 dependency with custom float_utils Remove ieee754 package dependency entirely and implement custom float encoding/decoding utilities that work correctly across all platforms (VM, dart2js, dart2wasm). Key changes: - Rename float16.dart to float_utils.dart and add float32/float64 helpers - Add isFloat16Lossless, isFloat32Lossless, isFloat64Lossless functions - Add toFloat32Bytes, toFloat64Bytes, fromFloat32Bytes, fromFloat64Bytes - Update double.dart, stage3.dart, pretty_print.dart to use float_utils - Fix negative zero encoding in CborSmallInt (check value == 0 first) - Use `identical(0, 0.0)` for reliable JS platform detection - Update tests with platform-specific expectations for integer-valued doubles that cannot be distinguished from integers on JS This fixes remaining JS test failures by properly detecting and handling: - Large doubles exceeding safe integer range (now encoded as floats) - Negative zero in integer encoding (now correctly encodes as 0) - JSON encoding of integer-valued doubles on JS All 423 VM tests, 411 JS tests, and 411 WASM tests now pass. Co-Authored-By: Claude Opus 4.5 --- lib/src/decoder/pretty_print.dart | 10 +-- lib/src/decoder/stage3.dart | 9 +-- lib/src/simple.dart | 26 +++++++ .../utils/{float16.dart => float_utils.dart} | 57 +++++++++++++++ lib/src/value/double.dart | 27 +++---- lib/src/value/int.dart | 4 +- lib/src/value/typed_data.dart | 13 ++-- lib/src/value/value.dart | 26 +++++-- test/json_encoder_test.dart | 43 ++++++++++- test/simple_encoder_rfc_test.dart | 73 +++++++++++++++++-- 10 files changed, 241 insertions(+), 47 deletions(-) rename lib/src/utils/{float16.dart => float_utils.dart} (73%) diff --git a/lib/src/decoder/pretty_print.dart b/lib/src/decoder/pretty_print.dart index 46c35f9..a4924bb 100644 --- a/lib/src/decoder/pretty_print.dart +++ b/lib/src/decoder/pretty_print.dart @@ -7,9 +7,7 @@ import 'dart:convert'; -import 'package:ieee754/ieee754.dart'; - -import '../utils/float16.dart' as float16_utils; +import '../utils/float_utils.dart' as float_utils; import 'stage1.dart'; import '../constants.dart'; import '../value/value.dart'; @@ -141,17 +139,17 @@ class _PrettyPrint implements Sink { break; case CborAdditionalInfo.halfPrecisionFloat: writer.write( - '(${float16_utils.fromFloat16Bytes(x.header.dataBytes)})', + '(${float_utils.fromFloat16Bytes(x.header.dataBytes)})', ); break; case CborAdditionalInfo.singlePrecisionFloat: writer.write( - '(${FloatParts.fromFloat32Bytes(x.header.dataBytes).toDouble()})', + '(${float_utils.fromFloat32Bytes(x.header.dataBytes)})', ); break; case CborAdditionalInfo.doublePrecisionFloat: writer.write( - '(${FloatParts.fromFloat64Bytes(x.header.dataBytes).toDouble()})', + '(${float_utils.fromFloat64Bytes(x.header.dataBytes)})', ); break; case CborAdditionalInfo.breakStop: diff --git a/lib/src/decoder/stage3.dart b/lib/src/decoder/stage3.dart index 3e6c6f3..84857a4 100644 --- a/lib/src/decoder/stage3.dart +++ b/lib/src/decoder/stage3.dart @@ -8,9 +8,8 @@ import 'package:cbor/cbor.dart'; import 'package:cbor/src/value/internal.dart'; import 'package:collection/collection.dart'; -import 'package:ieee754/ieee754.dart'; -import '../utils/float16.dart' as float16_utils; +import '../utils/float_utils.dart' as float_utils; import '../utils/utils.dart'; import '../constants.dart'; import 'stage2.dart'; @@ -258,13 +257,13 @@ CborFloat _createFloat(RawValueTagged raw) { final double value; switch (raw.header.additionalInfo) { case CborAdditionalInfo.halfPrecisionFloat: - value = float16_utils.fromFloat16Bytes(raw.header.dataBytes); + value = float_utils.fromFloat16Bytes(raw.header.dataBytes); break; case CborAdditionalInfo.singlePrecisionFloat: - value = FloatParts.fromFloat32Bytes(raw.header.dataBytes).toDouble(); + value = float_utils.fromFloat32Bytes(raw.header.dataBytes); break; case CborAdditionalInfo.doublePrecisionFloat: - value = FloatParts.fromFloat64Bytes(raw.header.dataBytes).toDouble(); + value = float_utils.fromFloat64Bytes(raw.header.dataBytes); break; default: diff --git a/lib/src/simple.dart b/lib/src/simple.dart index 48b0849..0641117 100644 --- a/lib/src/simple.dart +++ b/lib/src/simple.dart @@ -109,6 +109,32 @@ class CborSimpleDecoder extends Converter, Object?> { /// /// To see how CBOR values are transformed from and into objects, verify the /// documentation of both [CborValue.CborValue] and [CborValue.toObject]. +/// +/// ## JavaScript (dart2js) Limitations +/// +/// On JavaScript (dart2js), there is no distinction between integer and +/// floating-point numbers at the type level. All numbers are IEEE 754 doubles. +/// This means: +/// +/// - `0.0 is int` returns `true` on JS but `false` on native/WASM platforms +/// - Integer-valued doubles like `1.0`, `-4.0`, `65504.0` are encoded as +/// CBOR integers instead of CBOR floats +/// - Very large doubles (e.g., `1.0e+300`) may lose precision when treated +/// as integers +/// +/// Note: This limitation does **not** apply to dart2wasm, which correctly +/// distinguishes between integers and floats. +/// +/// **Workaround:** To ensure consistent float encoding across all platforms, +/// use the advanced API with explicit types: +/// +/// ```dart +/// // Instead of: +/// cbor.encode(1.0); // Encoded as int on JS, float on native/WASM +/// +/// // Use: +/// cborEncode(CborFloat(1.0)); // Always encoded as float +/// ``` class CborSimpleCodec extends Codec> { final bool _encodeDateTimeEpoch; final bool _decodeBase64; diff --git a/lib/src/utils/float16.dart b/lib/src/utils/float_utils.dart similarity index 73% rename from lib/src/utils/float16.dart rename to lib/src/utils/float_utils.dart index c68132d..6e150eb 100644 --- a/lib/src/utils/float16.dart +++ b/lib/src/utils/float_utils.dart @@ -175,3 +175,60 @@ double _pow2(int exp) { return result; } } + +/// Check if a double can be represented losslessly as a single-precision (32-bit) float. +/// +/// This is a cross-platform implementation. +bool isFloat32Lossless(double value) { + // Special values are always lossless + if (value.isNaN || value.isInfinite || value == 0.0) { + return true; + } + + // Convert to float32 and back, check if value is preserved + final bytes = toFloat32Bytes(value); + final reconstructed = fromFloat32Bytes(bytes); + + return reconstructed == value; +} + +/// Check if a double can be represented losslessly as a double-precision (64-bit) float. +/// +/// This always returns true since Dart doubles are already 64-bit IEEE 754 floats. +bool isFloat64Lossless(double value) { + return true; +} + +/// Converts a double to IEEE 754 single-precision (32-bit) bytes in big-endian order. +/// +/// Returns a list of 4 bytes. +List toFloat32Bytes(double value) { + final bytes = Uint8List(4); + ByteData.view(bytes.buffer).setFloat32(0, value, Endian.big); + return bytes; +} + +/// Converts IEEE 754 single-precision (32-bit) bytes to a double. +/// +/// Expects 4 bytes in big-endian order. +double fromFloat32Bytes(List bytes) { + final data = ByteData.view(Uint8List.fromList(bytes).buffer); + return data.getFloat32(0, Endian.big); +} + +/// Converts a double to IEEE 754 double-precision (64-bit) bytes in big-endian order. +/// +/// Returns a list of 8 bytes. +List toFloat64Bytes(double value) { + final bytes = Uint8List(8); + ByteData.view(bytes.buffer).setFloat64(0, value, Endian.big); + return bytes; +} + +/// Converts IEEE 754 double-precision (64-bit) bytes to a double. +/// +/// Expects 8 bytes in big-endian order. +double fromFloat64Bytes(List bytes) { + final data = ByteData.view(Uint8List.fromList(bytes).buffer); + return data.getFloat64(0, Endian.big); +} diff --git a/lib/src/value/double.dart b/lib/src/value/double.dart index 6f363e2..34a0ce7 100644 --- a/lib/src/value/double.dart +++ b/lib/src/value/double.dart @@ -7,11 +7,10 @@ import 'package:cbor/cbor.dart'; import 'package:collection/collection.dart'; -import 'package:ieee754/ieee754.dart'; import '../constants.dart'; import '../encoder/sink.dart'; -import '../utils/float16.dart' as float16_utils; +import '../utils/float_utils.dart' as float_utils; import 'internal.dart'; /// Enumeration to allow the user to select which level of precision to @@ -80,41 +79,39 @@ class _CborFloatImpl with CborValueMixin implements CborFloat { return; } - final parts = FloatParts.fromDouble(value); - // Automatic(default) conversion picks the best encoding. if (precision == CborFloatPrecision.automatic) { - if (parts.isFloat16Lossless) { + if (float_utils.isFloat16Lossless(value)) { sink.addHeader( CborMajorType.simpleFloat, CborAdditionalInfo.halfPrecisionFloat, ); - sink.add(float16_utils.toFloat16Bytes(value)); - } else if (parts.isFloat32Lossless) { + sink.add(float_utils.toFloat16Bytes(value)); + } else if (float_utils.isFloat32Lossless(value)) { sink.addHeader( CborMajorType.simpleFloat, CborAdditionalInfo.singlePrecisionFloat, ); - sink.add(parts.toFloat32Bytes()); + sink.add(float_utils.toFloat32Bytes(value)); } else { sink.addHeader( CborMajorType.simpleFloat, CborAdditionalInfo.doublePrecisionFloat, ); - sink.add(parts.toFloat64Bytes()); + sink.add(float_utils.toFloat64Bytes(value)); } } else { switch (precision) { case CborFloatPrecision.half: - if (parts.isFloat16Lossless) { + if (float_utils.isFloat16Lossless(value)) { sink.addHeader( CborMajorType.simpleFloat, CborAdditionalInfo.halfPrecisionFloat, ); - sink.add(float16_utils.toFloat16Bytes(value)); + sink.add(float_utils.toFloat16Bytes(value)); } else { // Invalid conversion throw ArgumentError( @@ -124,12 +121,12 @@ class _CborFloatImpl with CborValueMixin implements CborFloat { } break; case CborFloatPrecision.float: - if (parts.isFloat32Lossless) { + if (float_utils.isFloat32Lossless(value)) { sink.addHeader( CborMajorType.simpleFloat, CborAdditionalInfo.singlePrecisionFloat, ); - sink.add(parts.toFloat32Bytes()); + sink.add(float_utils.toFloat32Bytes(value)); } else { // Invalid conversion throw ArgumentError( @@ -139,12 +136,12 @@ class _CborFloatImpl with CborValueMixin implements CborFloat { } break; case CborFloatPrecision.double: - if (parts.isFloat64Lossless) { + if (float_utils.isFloat64Lossless(value)) { sink.addHeader( CborMajorType.simpleFloat, CborAdditionalInfo.doublePrecisionFloat, ); - sink.add(parts.toFloat64Bytes()); + sink.add(float_utils.toFloat64Bytes(value)); } else { // Invalid conversion throw ArgumentError( diff --git a/lib/src/value/int.dart b/lib/src/value/int.dart index d984dd8..5280f28 100644 --- a/lib/src/value/int.dart +++ b/lib/src/value/int.dart @@ -92,7 +92,9 @@ class _CborSmallIntImpl with CborValueMixin implements CborSmallInt { void encode(EncodeSink sink) { sink.addTags(tags); - if (!value.isNegative) { + // Note: On JS, negative zero (-0.0) may pass isNegative but should be + // encoded as positive zero. Check for zero explicitly. + if (value == 0 || !value.isNegative) { sink.addHeaderInfo(0, Arg.int(value)); } else { sink.addHeaderInfo(1, Arg.int((~BigInt.from(value)).toInt())); diff --git a/lib/src/value/typed_data.dart b/lib/src/value/typed_data.dart index 5e91784..f4175f2 100644 --- a/lib/src/value/typed_data.dart +++ b/lib/src/value/typed_data.dart @@ -10,7 +10,7 @@ import 'dart:typed_data'; import 'package:cbor/cbor.dart'; import 'package:cbor/src/value/internal.dart'; -import '../utils/float16.dart' as float16_utils; +import '../utils/float_utils.dart' as float_utils; /// Base class for all Typed Arrays sealed class CborTypedArray extends CborBytesImpl { @@ -186,9 +186,12 @@ class CborUint32LittleEndianArray extends CborTypedArray { } /// Platform detection constants -const bool _kIsJs = bool.fromEnvironment('dart.library.js_interop'); +/// Using `identical(0, 0.0)` which returns true only on JS where int/double are same. +final bool _kIsJs = identical(0, 0.0); +// For WASM detection, we can't use identical check. WASM doesn't have the setUint64 issue +// but doesn't have int/double conflation either. Use dart.tool.dart2wasm for now. const bool _kIsWasm = bool.fromEnvironment('dart.tool.dart2wasm'); -const bool _kIsWeb = _kIsJs || _kIsWasm; +final bool _kIsWeb = _kIsJs || _kIsWasm; /// uint64 big endian class CborUint64BigEndianArray extends CborTypedArray { @@ -617,7 +620,7 @@ class CborFloat16BigEndianArray extends CborTypedArray { final data = ByteData.sublistView(Uint8List.fromList(bytes)); for (var i = 0; i < bytes.length; i += 2) { final chunk = [data.getUint8(i), data.getUint8(i + 1)]; - list.add(float16_utils.fromFloat16Bytes(chunk)); + list.add(float_utils.fromFloat16Bytes(chunk)); } return list; } @@ -639,7 +642,7 @@ class CborFloat16LittleEndianArray extends CborTypedArray { for (var i = 0; i < bytes.length; i += 2) { // Reverse for Little Endian as our decoder expects Big Endian final chunk = [data.getUint8(i + 1), data.getUint8(i)]; - list.add(float16_utils.fromFloat16Bytes(chunk)); + list.add(float_utils.fromFloat16Bytes(chunk)); } return list; } diff --git a/lib/src/value/value.dart b/lib/src/value/value.dart index e5bde57..5835728 100644 --- a/lib/src/value/value.dart +++ b/lib/src/value/value.dart @@ -26,6 +26,12 @@ export 'simple_value.dart'; export 'string.dart'; export 'typed_data.dart'; +/// Platform detection for JS (dart2js). On JS, all numbers are IEEE 754 doubles, +/// so integer-valued doubles like 1.0 pass the `is int` type check. +/// Note: This does NOT apply to WASM (dart2wasm), which correctly distinguishes types. +/// Using `identical(0, 0.0)` which returns true only on JS where int/double are same. +final bool _kIsJs = identical(0, 0.0); + /// Jump table for initial byte values can be found here. /// Maybe useful for code maintainers. @@ -125,11 +131,21 @@ abstract class CborValue { return CborNull(); } else if (object is CborValue) { return object; - } else if (object is double && (object.isNaN || object.isInfinite)) { - // Handle special double values first: on JS, Infinity/NaN might match - // 'is int', leading to BigInt.from(infinity) throwing. - return CborFloat(object); - } else if (object is int) { + } + + // JS-only: Handle special doubles that would cause issues when treated as int. + // On JS, all numbers are doubles, so integer-valued doubles like 1.0 pass + // the 'is int' check. We need to intercept NaN/Infinity (which would throw + // in BigInt.from) and values exceeding the safe integer range (2^53-1). + if (_kIsJs && object is double) { + if (object.isNaN || object.isInfinite || object.abs() > 9007199254740991) { + return CborFloat(object); + } + // Fall through - integer-valued doubles within safe range will match + // 'is int' and be encoded as integers (this is unavoidable on JS). + } + + if (object is int) { return CborSmallInt(object); } else if (object is BigInt) { return CborInt(object); diff --git a/test/json_encoder_test.dart b/test/json_encoder_test.dart index c5d7356..09b2fed 100644 --- a/test/json_encoder_test.dart +++ b/test/json_encoder_test.dart @@ -10,6 +10,12 @@ import 'dart:typed_data'; import 'package:cbor/cbor.dart'; import 'package:test/test.dart'; +// Platform detection: on JS (dart2js), integer-valued doubles (0.0, 1.0, etc.) +// are indistinguishable from integers and get encoded as CBOR integers. +// Note: This does NOT apply to WASM (dart2wasm), which handles this correctly. +// Using `identical(0, 0.0)` which returns true only on JS where int/double are same. +final bool kIsJs = identical(0, 0.0); + String encode(Object? input) { return const CborJsonEncoder().convert(CborValue(input)); } @@ -57,7 +63,15 @@ void main() { }); test('0.0', () { - expect(encode(0.0), '0.0'); + if (kIsJs) { + // On JS, 0.0 is indistinguishable from 0 + // because JSON.stringify(0.0) returns '0' on JS. + expect(encode(0.0), '0'); + expect(const CborJsonEncoder().convert(CborFloat(0.0)), '0'); + } else { + expect(encode(0.0), '0.0'); + expect(const CborJsonEncoder().convert(CborFloat(0.0)), '0.0'); + } }); test('-0.0', () { @@ -65,7 +79,14 @@ void main() { }); test('1.0', () { - expect(encode(1.0), '1.0'); + if (kIsJs) { + // On JS, 1.0 is indistinguishable from 1 + expect(encode(1.0), '1'); + expect(const CborJsonEncoder().convert(CborFloat(1.0)), '1'); + } else { + expect(encode(1.0), '1.0'); + expect(const CborJsonEncoder().convert(CborFloat(1.0)), '1.0'); + } }); test('1.5', () { @@ -73,11 +94,25 @@ void main() { }); test('65504.0', () { - expect(encode(65504.0), '65504.0'); + if (kIsJs) { + // On JS, 65504.0 is indistinguishable from 65504 + expect(encode(65504.0), '65504'); + expect(const CborJsonEncoder().convert(CborFloat(65504.0)), '65504'); + } else { + expect(encode(65504.0), '65504.0'); + expect(const CborJsonEncoder().convert(CborFloat(65504.0)), '65504.0'); + } }); test('100000.0', () { - expect(encode(100000.0), '100000.0'); + if (kIsJs) { + // On JS, 100000.0 is indistinguishable from 100000 + expect(encode(100000.0), '100000'); + expect(const CborJsonEncoder().convert(CborFloat(100000.0)), '100000'); + } else { + expect(encode(100000.0), '100000.0'); + expect(const CborJsonEncoder().convert(CborFloat(100000.0)), '100000.0'); + } }); test('3.4028234663852886e+38', () { diff --git a/test/simple_encoder_rfc_test.dart b/test/simple_encoder_rfc_test.dart index 41ffc78..08bc629 100644 --- a/test/simple_encoder_rfc_test.dart +++ b/test/simple_encoder_rfc_test.dart @@ -7,9 +7,16 @@ import 'dart:typed_data'; +import 'package:cbor/cbor.dart' hide cbor; import 'package:cbor/simple.dart'; import 'package:test/test.dart'; +// Platform detection: on JS (dart2js), integer-valued doubles (0.0, 1.0, etc.) +// are indistinguishable from integers and get encoded as CBOR integers. +// Note: This does NOT apply to WASM (dart2wasm), which handles this correctly. +// Using `identical(0, 0.0)` which returns true only on JS where int/double are same. +final bool kIsJs = identical(0, 0.0); + void main() { group('RFC Appendix A Diagnostics encoder tests -> ', () { test('0', () { @@ -128,57 +135,111 @@ void main() { test('0.0', () { final encoded = cbor.encode(0.0); - expect(encoded, [0xf9, 0x00, 0x00]); + if (kIsJs) { + // On JS, 0.0 is indistinguishable from 0 and encoded as integer + expect(encoded, [0x00]); + } else { + expect(encoded, [0xf9, 0x00, 0x00]); + } + // Using explicit CborFloat ensures float encoding on all platforms + expect(cborEncode(CborFloat(0.0)), [0xf9, 0x00, 0x00]); }); test('-0.0', () { final encoded = cbor.encode(-0.0); - expect(encoded, [0xf9, 0x80, 0x00]); + if (kIsJs) { + // On JS, -0.0 is indistinguishable from 0 and encoded as integer + // (the sign is lost when converted to int) + expect(encoded, [0x00]); + } else { + expect(encoded, [0xf9, 0x80, 0x00]); + } + // Using explicit CborFloat preserves negative zero + expect(cborEncode(CborFloat(-0.0)), [0xf9, 0x80, 0x00]); }); test('1.0', () { final encoded = cbor.encode(1.0); - expect(encoded, [0xf9, 0x3c, 0x00]); + if (kIsJs) { + // On JS, 1.0 is indistinguishable from 1 and encoded as integer + expect(encoded, [0x01]); + } else { + expect(encoded, [0xf9, 0x3c, 0x00]); + } + // Using explicit CborFloat ensures float encoding on all platforms + expect(cborEncode(CborFloat(1.0)), [0xf9, 0x3c, 0x00]); }); test('1.5', () { final encoded = cbor.encode(1.5); + // 1.5 has a fractional part, so it's always encoded as float expect(encoded, [0xf9, 0x3e, 0x00]); }); test('65504.0', () { final encoded = cbor.encode(65504.0); - expect(encoded, [0xf9, 0x7b, 0xff]); + if (kIsJs) { + // On JS, 65504.0 is indistinguishable from 65504 and encoded as integer + expect(encoded, [0x19, 0xff, 0xe0]); + } else { + expect(encoded, [0xf9, 0x7b, 0xff]); + } + // Using explicit CborFloat ensures float encoding on all platforms + expect(cborEncode(CborFloat(65504.0)), [0xf9, 0x7b, 0xff]); }); test('100000.0', () { final encoded = cbor.encode(100000.0); - expect(encoded, [0xfa, 0x47, 0xc3, 0x50, 0x00]); + if (kIsJs) { + // On JS, 100000.0 is indistinguishable from 100000 and encoded as integer + expect(encoded, [0x1a, 0x00, 0x01, 0x86, 0xa0]); + } else { + expect(encoded, [0xfa, 0x47, 0xc3, 0x50, 0x00]); + } + // Using explicit CborFloat ensures float encoding on all platforms + expect(cborEncode(CborFloat(100000.0)), [0xfa, 0x47, 0xc3, 0x50, 0x00]); }); test('3.4028234663852886e+38', () { + // On JS, large doubles that exceed the safe integer range (2^53-1) are + // detected and encoded as floats instead of being misinterpreted as ints. + // On VM/WASM, these are always correctly identified as doubles. final encoded = cbor.encode(3.4028234663852886e+38); expect(encoded, [0xfa, 0x7f, 0x7f, 0xff, 0xff]); + expect(cborEncode(CborFloat(3.4028234663852886e+38)), [0xfa, 0x7f, 0x7f, 0xff, 0xff]); }); test('1.0e+300', () { + // On JS, large doubles that exceed the safe integer range (2^53-1) are + // detected and encoded as floats instead of being misinterpreted as ints. + // On VM/WASM, these are always correctly identified as doubles. final encoded = cbor.encode(1.0e+300); expect(encoded, [0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); + expect(cborEncode(CborFloat(1.0e+300)), [0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); }); test('5.960464477539063e-8', () { final encoded = cbor.encode(5.960464477539063e-8); + // This small fractional value is always encoded as float expect(encoded, [0xf9, 0x00, 0x01]); }); test('0.00006103515625', () { final encoded = cbor.encode(0.00006103515625); + // This small fractional value is always encoded as float expect(encoded, [0xf9, 0x04, 0x00]); }); test('-4.0', () { final encoded = cbor.encode(-4.0); - expect(encoded, [0xf9, 0xc4, 0x00]); + if (kIsJs) { + // On JS, -4.0 is indistinguishable from -4 and encoded as integer + expect(encoded, [0x23]); + } else { + expect(encoded, [0xf9, 0xc4, 0x00]); + } + // Using explicit CborFloat ensures float encoding on all platforms + expect(cborEncode(CborFloat(-4.0)), [0xf9, 0xc4, 0x00]); }); test('-4.1', () { From 55f4e2f5c3f561b271d6751261a4f20d0c97df80 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:43:57 +0400 Subject: [PATCH 09/12] Removed ieee dep --- pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0fa123d..7f15ed9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,6 @@ dependencies: typed_data: '^1.3.2' hex: '^0.2.0' convert: '^3.1.2' - ieee754: '^1.0.3' meta: '^1.9.1' characters: '^1.4.0' From f2fa3d7847553f2e04a4e305d4dc2023d2d77e13 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:53:16 +0400 Subject: [PATCH 10/12] [chore] auto-formatted code --- lib/src/value/typed_data.dart | 6 +++++- test/json_encoder_test.dart | 5 ++++- test/simple_encoder_rfc_test.dart | 20 ++++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/src/value/typed_data.dart b/lib/src/value/typed_data.dart index f4175f2..95d2fab 100644 --- a/lib/src/value/typed_data.dart +++ b/lib/src/value/typed_data.dart @@ -263,7 +263,11 @@ class CborUint64LittleEndianArray extends CborTypedArray { final value = list[i]; // Little endian: low bytes first data.setUint32(i * 8, value & 0xFFFFFFFF, Endian.little); - data.setUint32(i * 8 + 4, (value ~/ 0x100000000) & 0xFFFFFFFF, Endian.little); + data.setUint32( + i * 8 + 4, + (value ~/ 0x100000000) & 0xFFFFFFFF, + Endian.little, + ); } else { data.setUint64(i * 8, list[i], Endian.little); } diff --git a/test/json_encoder_test.dart b/test/json_encoder_test.dart index 09b2fed..6323191 100644 --- a/test/json_encoder_test.dart +++ b/test/json_encoder_test.dart @@ -111,7 +111,10 @@ void main() { expect(const CborJsonEncoder().convert(CborFloat(100000.0)), '100000'); } else { expect(encode(100000.0), '100000.0'); - expect(const CborJsonEncoder().convert(CborFloat(100000.0)), '100000.0'); + expect( + const CborJsonEncoder().convert(CborFloat(100000.0)), + '100000.0', + ); } }); diff --git a/test/simple_encoder_rfc_test.dart b/test/simple_encoder_rfc_test.dart index 08bc629..6374568 100644 --- a/test/simple_encoder_rfc_test.dart +++ b/test/simple_encoder_rfc_test.dart @@ -206,7 +206,13 @@ void main() { // On VM/WASM, these are always correctly identified as doubles. final encoded = cbor.encode(3.4028234663852886e+38); expect(encoded, [0xfa, 0x7f, 0x7f, 0xff, 0xff]); - expect(cborEncode(CborFloat(3.4028234663852886e+38)), [0xfa, 0x7f, 0x7f, 0xff, 0xff]); + expect(cborEncode(CborFloat(3.4028234663852886e+38)), [ + 0xfa, + 0x7f, + 0x7f, + 0xff, + 0xff, + ]); }); test('1.0e+300', () { @@ -215,7 +221,17 @@ void main() { // On VM/WASM, these are always correctly identified as doubles. final encoded = cbor.encode(1.0e+300); expect(encoded, [0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); - expect(cborEncode(CborFloat(1.0e+300)), [0xfb, 0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); + expect(cborEncode(CborFloat(1.0e+300)), [ + 0xfb, + 0x7e, + 0x37, + 0xe4, + 0x3c, + 0x88, + 0x00, + 0x75, + 0x9c, + ]); }); test('5.960464477539063e-8', () { From b41c2b67afab45578461b0c59899ee079a5370ae Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 17:56:34 +0400 Subject: [PATCH 11/12] test: add exhaustive tests for float_utils.dart Add comprehensive test coverage for the custom float encoding/decoding utilities that replaced the ieee754 package. Tests cover: - Float16 (half-precision): encoding, decoding, round-trips - Special values: NaN, +/-Infinity, +/-0.0 - Normal and subnormal values - Overflow to infinity, underflow to zero - RFC 8949 Appendix A test vectors - Float32 (single-precision): encoding, decoding, round-trips - Float64 (double-precision): encoding, decoding, round-trips - isFloat16Lossless, isFloat32Lossless, isFloat64Lossless checks - Cross-platform consistency tests (JS-specific bug fixes) - Edge cases: byte array lengths, big-endian order 105 new tests, all passing on VM, JS (dart2js), and WASM (dart2wasm). Co-Authored-By: Claude Opus 4.5 --- test/float_utils_test.dart | 757 +++++++++++++++++++++++++++++++++++++ 1 file changed, 757 insertions(+) create mode 100644 test/float_utils_test.dart diff --git a/test/float_utils_test.dart b/test/float_utils_test.dart new file mode 100644 index 0000000..18e887b --- /dev/null +++ b/test/float_utils_test.dart @@ -0,0 +1,757 @@ +/* + * Package : Cbor + * Author : S. Hamblett + * Date : 21/01/2026 + * Copyright : S.Hamblett + */ + +import 'package:cbor/src/utils/float_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('Float16 (half-precision) encoding - toFloat16Bytes', () { + group('Special values', () { + test('NaN encodes to canonical NaN (0x7e00)', () { + expect(toFloat16Bytes(double.nan), [0x7e, 0x00]); + }); + + test('+Infinity encodes to 0x7c00', () { + expect(toFloat16Bytes(double.infinity), [0x7c, 0x00]); + }); + + test('-Infinity encodes to 0xfc00', () { + expect(toFloat16Bytes(double.negativeInfinity), [0xfc, 0x00]); + }); + + test('+0.0 encodes to 0x0000', () { + expect(toFloat16Bytes(0.0), [0x00, 0x00]); + }); + + test('-0.0 encodes to 0x8000', () { + expect(toFloat16Bytes(-0.0), [0x80, 0x00]); + }); + }); + + group('Normal positive values', () { + test('1.0 encodes to 0x3c00', () { + expect(toFloat16Bytes(1.0), [0x3c, 0x00]); + }); + + test('1.5 encodes to 0x3e00', () { + expect(toFloat16Bytes(1.5), [0x3e, 0x00]); + }); + + test('2.0 encodes to 0x4000', () { + expect(toFloat16Bytes(2.0), [0x40, 0x00]); + }); + + test('0.5 encodes to 0x3800', () { + expect(toFloat16Bytes(0.5), [0x38, 0x00]); + }); + + test('65504.0 (max normal) encodes to 0x7bff', () { + expect(toFloat16Bytes(65504.0), [0x7b, 0xff]); + }); + + test('0.00006103515625 (min positive normal) encodes to 0x0400', () { + expect(toFloat16Bytes(0.00006103515625), [0x04, 0x00]); + }); + }); + + group('Normal negative values', () { + test('-1.0 encodes to 0xbc00', () { + expect(toFloat16Bytes(-1.0), [0xbc, 0x00]); + }); + + test('-1.5 encodes to 0xbe00', () { + expect(toFloat16Bytes(-1.5), [0xbe, 0x00]); + }); + + test('-2.0 encodes to 0xc000', () { + expect(toFloat16Bytes(-2.0), [0xc0, 0x00]); + }); + + test('-4.0 encodes to 0xc400', () { + expect(toFloat16Bytes(-4.0), [0xc4, 0x00]); + }); + + test('-65504.0 (min normal) encodes to 0xfbff', () { + expect(toFloat16Bytes(-65504.0), [0xfb, 0xff]); + }); + }); + + group('Subnormal values', () { + test('5.960464477539063e-8 (smallest subnormal) encodes to 0x0001', () { + expect(toFloat16Bytes(5.960464477539063e-8), [0x00, 0x01]); + }); + + test('6.097555160522461e-5 (largest subnormal) encodes to 0x03ff', () { + expect(toFloat16Bytes(6.097555160522461e-5), [0x03, 0xff]); + }); + }); + + group('Overflow to infinity', () { + test('65536.0 overflows to +Infinity', () { + expect(toFloat16Bytes(65536.0), [0x7c, 0x00]); + }); + + test('100000.0 overflows to +Infinity', () { + expect(toFloat16Bytes(100000.0), [0x7c, 0x00]); + }); + + test('-100000.0 overflows to -Infinity', () { + expect(toFloat16Bytes(-100000.0), [0xfc, 0x00]); + }); + }); + + group('Underflow to zero', () { + test('Very small positive value underflows to +0', () { + expect(toFloat16Bytes(1e-10), [0x00, 0x00]); + }); + + test('Very small negative value underflows to -0', () { + expect(toFloat16Bytes(-1e-10), [0x80, 0x00]); + }); + }); + + group('RFC 8949 Appendix A test vectors', () { + // These are the exact values from RFC 8949 + test('0.0', () => expect(toFloat16Bytes(0.0), [0x00, 0x00])); + test('-0.0', () => expect(toFloat16Bytes(-0.0), [0x80, 0x00])); + test('1.0', () => expect(toFloat16Bytes(1.0), [0x3c, 0x00])); + test('1.5', () => expect(toFloat16Bytes(1.5), [0x3e, 0x00])); + test('65504.0', () => expect(toFloat16Bytes(65504.0), [0x7b, 0xff])); + test('5.960464477539063e-8', () { + expect(toFloat16Bytes(5.960464477539063e-8), [0x00, 0x01]); + }); + test('0.00006103515625', () { + expect(toFloat16Bytes(0.00006103515625), [0x04, 0x00]); + }); + test('-4.0', () => expect(toFloat16Bytes(-4.0), [0xc4, 0x00])); + }); + }); + + group('Float16 (half-precision) decoding - fromFloat16Bytes', () { + group('Special values', () { + test('0x7e00 decodes to NaN', () { + expect(fromFloat16Bytes([0x7e, 0x00]).isNaN, true); + }); + + test('0x7c01 (NaN with payload) decodes to NaN', () { + expect(fromFloat16Bytes([0x7c, 0x01]).isNaN, true); + }); + + test('0x7c00 decodes to +Infinity', () { + expect(fromFloat16Bytes([0x7c, 0x00]), double.infinity); + }); + + test('0xfc00 decodes to -Infinity', () { + expect(fromFloat16Bytes([0xfc, 0x00]), double.negativeInfinity); + }); + + test('0x0000 decodes to +0.0', () { + final result = fromFloat16Bytes([0x00, 0x00]); + expect(result, 0.0); + expect(result.isNegative, false); + }); + + test('0x8000 decodes to -0.0', () { + final result = fromFloat16Bytes([0x80, 0x00]); + expect(result, 0.0); + expect(result.isNegative, true); + }); + }); + + group('Normal positive values', () { + test('0x3c00 decodes to 1.0', () { + expect(fromFloat16Bytes([0x3c, 0x00]), 1.0); + }); + + test('0x3e00 decodes to 1.5', () { + expect(fromFloat16Bytes([0x3e, 0x00]), 1.5); + }); + + test('0x4000 decodes to 2.0', () { + expect(fromFloat16Bytes([0x40, 0x00]), 2.0); + }); + + test('0x3800 decodes to 0.5', () { + expect(fromFloat16Bytes([0x38, 0x00]), 0.5); + }); + + test('0x7bff decodes to 65504.0', () { + expect(fromFloat16Bytes([0x7b, 0xff]), 65504.0); + }); + + test('0x0400 decodes to 0.00006103515625', () { + expect(fromFloat16Bytes([0x04, 0x00]), 0.00006103515625); + }); + }); + + group('Normal negative values', () { + test('0xbc00 decodes to -1.0', () { + expect(fromFloat16Bytes([0xbc, 0x00]), -1.0); + }); + + test('0xbe00 decodes to -1.5', () { + expect(fromFloat16Bytes([0xbe, 0x00]), -1.5); + }); + + test('0xc000 decodes to -2.0', () { + expect(fromFloat16Bytes([0xc0, 0x00]), -2.0); + }); + + test('0xc400 decodes to -4.0', () { + expect(fromFloat16Bytes([0xc4, 0x00]), -4.0); + }); + + test('0xfbff decodes to -65504.0', () { + expect(fromFloat16Bytes([0xfb, 0xff]), -65504.0); + }); + }); + + group('Subnormal values', () { + test('0x0001 decodes to smallest subnormal', () { + expect(fromFloat16Bytes([0x00, 0x01]), 5.960464477539063e-8); + }); + + test('0x03ff decodes to largest subnormal', () { + expect(fromFloat16Bytes([0x03, 0xff]), 6.097555160522461e-5); + }); + }); + }); + + group('Float16 round-trip tests', () { + test('All representable values round-trip correctly', () { + final testValues = [ + 0.0, + -0.0, + 1.0, + -1.0, + 1.5, + -1.5, + 2.0, + -2.0, + 0.5, + -0.5, + 0.25, + -0.25, + 65504.0, + -65504.0, + 0.00006103515625, + -0.00006103515625, + 5.960464477539063e-8, + double.infinity, + double.negativeInfinity, + ]; + + for (final value in testValues) { + final bytes = toFloat16Bytes(value); + final decoded = fromFloat16Bytes(bytes); + if (value.isNaN) { + expect(decoded.isNaN, true, reason: 'NaN should round-trip'); + } else { + expect(decoded, value, reason: '$value should round-trip'); + } + } + }); + + test('NaN round-trips', () { + final bytes = toFloat16Bytes(double.nan); + expect(fromFloat16Bytes(bytes).isNaN, true); + }); + }); + + group('isFloat16Lossless', () { + group('Values that can be represented', () { + test('Special values', () { + expect(isFloat16Lossless(0.0), true); + expect(isFloat16Lossless(-0.0), true); + expect(isFloat16Lossless(double.nan), true); + expect(isFloat16Lossless(double.infinity), true); + expect(isFloat16Lossless(double.negativeInfinity), true); + }); + + test('Exact representable values', () { + expect(isFloat16Lossless(1.0), true); + expect(isFloat16Lossless(-1.0), true); + expect(isFloat16Lossless(1.5), true); + expect(isFloat16Lossless(2.0), true); + expect(isFloat16Lossless(65504.0), true); + expect(isFloat16Lossless(0.00006103515625), true); + expect(isFloat16Lossless(5.960464477539063e-8), true); + }); + }); + + group('Values that cannot be represented', () { + test('Values too large', () { + expect(isFloat16Lossless(100000.0), false); + expect(isFloat16Lossless(65536.0), false); + }); + + test('Values with too much precision', () { + expect(isFloat16Lossless(1.1), false); + expect(isFloat16Lossless(3.14159), false); + expect(isFloat16Lossless(1.0001), false); + }); + + test('Values too small (non-zero)', () { + expect(isFloat16Lossless(1e-10), false); + }); + }); + }); + + group('Float32 (single-precision) encoding - toFloat32Bytes', () { + group('Special values', () { + test('NaN encodes correctly', () { + final bytes = toFloat32Bytes(double.nan); + expect(bytes.length, 4); + // NaN has exponent all 1s and non-zero mantissa + expect(bytes[0] & 0x7F, 0x7F); // High bit of exponent + expect(bytes[1] & 0x80, 0x80); // Low bit of exponent + }); + + test('+Infinity encodes to 0x7f800000', () { + expect(toFloat32Bytes(double.infinity), [0x7f, 0x80, 0x00, 0x00]); + }); + + test('-Infinity encodes to 0xff800000', () { + expect(toFloat32Bytes(double.negativeInfinity), [ + 0xff, + 0x80, + 0x00, + 0x00, + ]); + }); + + test('+0.0 encodes to 0x00000000', () { + expect(toFloat32Bytes(0.0), [0x00, 0x00, 0x00, 0x00]); + }); + + test('-0.0 encodes to 0x80000000', () { + expect(toFloat32Bytes(-0.0), [0x80, 0x00, 0x00, 0x00]); + }); + }); + + group('Normal values', () { + test('1.0 encodes to 0x3f800000', () { + expect(toFloat32Bytes(1.0), [0x3f, 0x80, 0x00, 0x00]); + }); + + test('-1.0 encodes to 0xbf800000', () { + expect(toFloat32Bytes(-1.0), [0xbf, 0x80, 0x00, 0x00]); + }); + + test('2.0 encodes to 0x40000000', () { + expect(toFloat32Bytes(2.0), [0x40, 0x00, 0x00, 0x00]); + }); + + test('0.5 encodes to 0x3f000000', () { + expect(toFloat32Bytes(0.5), [0x3f, 0x00, 0x00, 0x00]); + }); + + test('100000.0 encodes to 0x47c35000', () { + expect(toFloat32Bytes(100000.0), [0x47, 0xc3, 0x50, 0x00]); + }); + + test('3.4028234663852886e+38 (max float32)', () { + expect(toFloat32Bytes(3.4028234663852886e+38), [ + 0x7f, + 0x7f, + 0xff, + 0xff, + ]); + }); + }); + }); + + group('Float32 (single-precision) decoding - fromFloat32Bytes', () { + group('Special values', () { + test('0x7fc00000 decodes to NaN', () { + expect(fromFloat32Bytes([0x7f, 0xc0, 0x00, 0x00]).isNaN, true); + }); + + test('0x7f800000 decodes to +Infinity', () { + expect(fromFloat32Bytes([0x7f, 0x80, 0x00, 0x00]), double.infinity); + }); + + test('0xff800000 decodes to -Infinity', () { + expect( + fromFloat32Bytes([0xff, 0x80, 0x00, 0x00]), + double.negativeInfinity, + ); + }); + + test('0x00000000 decodes to +0.0', () { + expect(fromFloat32Bytes([0x00, 0x00, 0x00, 0x00]), 0.0); + }); + + test('0x80000000 decodes to -0.0', () { + final result = fromFloat32Bytes([0x80, 0x00, 0x00, 0x00]); + expect(result, 0.0); + expect(result.isNegative, true); + }); + }); + + group('Normal values', () { + test('0x3f800000 decodes to 1.0', () { + expect(fromFloat32Bytes([0x3f, 0x80, 0x00, 0x00]), 1.0); + }); + + test('0xbf800000 decodes to -1.0', () { + expect(fromFloat32Bytes([0xbf, 0x80, 0x00, 0x00]), -1.0); + }); + + test('0x47c35000 decodes to 100000.0', () { + expect(fromFloat32Bytes([0x47, 0xc3, 0x50, 0x00]), 100000.0); + }); + + test('0x7f7fffff decodes to max float32', () { + expect( + fromFloat32Bytes([0x7f, 0x7f, 0xff, 0xff]), + closeTo(3.4028234663852886e+38, 1e31), + ); + }); + }); + }); + + group('Float32 round-trip tests', () { + test('Values round-trip correctly', () { + final testValues = [ + 0.0, + -0.0, + 1.0, + -1.0, + 2.0, + 0.5, + 100000.0, + -100000.0, + 3.14159, + -3.14159, + 1e10, + 1e-10, + double.infinity, + double.negativeInfinity, + ]; + + for (final value in testValues) { + final bytes = toFloat32Bytes(value); + final decoded = fromFloat32Bytes(bytes); + if (value.isNaN) { + expect(decoded.isNaN, true, reason: 'NaN should round-trip'); + } else if (value.isInfinite) { + expect(decoded, value, reason: '$value should round-trip'); + } else { + // Float32 has limited precision, so use closeTo for normal values + expect( + decoded, + closeTo(value, value.abs() * 1e-6 + 1e-10), + reason: '$value should round-trip', + ); + } + } + }); + }); + + group('isFloat32Lossless', () { + test('Special values', () { + expect(isFloat32Lossless(0.0), true); + expect(isFloat32Lossless(-0.0), true); + expect(isFloat32Lossless(double.nan), true); + expect(isFloat32Lossless(double.infinity), true); + expect(isFloat32Lossless(double.negativeInfinity), true); + }); + + test('Values that fit in float32', () { + expect(isFloat32Lossless(1.0), true); + expect(isFloat32Lossless(-1.0), true); + expect(isFloat32Lossless(100000.0), true); + expect(isFloat32Lossless(3.4028234663852886e+38), true); + }); + + test('Values that do not fit in float32', () { + // Very large values + expect(isFloat32Lossless(1e300), false); + // Values with too much precision + expect(isFloat32Lossless(1.0000000001), false); + }); + }); + + group('Float64 (double-precision) encoding - toFloat64Bytes', () { + group('Special values', () { + test('NaN encodes correctly', () { + final bytes = toFloat64Bytes(double.nan); + expect(bytes.length, 8); + }); + + test('+Infinity encodes to 0x7ff0000000000000', () { + expect(toFloat64Bytes(double.infinity), [ + 0x7f, + 0xf0, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + }); + + test('-Infinity encodes to 0xfff0000000000000', () { + expect(toFloat64Bytes(double.negativeInfinity), [ + 0xff, + 0xf0, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + }); + + test('+0.0 encodes to all zeros', () { + expect(toFloat64Bytes(0.0), [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + }); + + test('-0.0 encodes to 0x8000000000000000', () { + expect(toFloat64Bytes(-0.0), [ + 0x80, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + }); + }); + + group('Normal values', () { + test('1.0 encodes to 0x3ff0000000000000', () { + expect(toFloat64Bytes(1.0), [ + 0x3f, + 0xf0, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + }); + + test('-1.0 encodes to 0xbff0000000000000', () { + expect(toFloat64Bytes(-1.0), [ + 0xbf, + 0xf0, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + }); + + test('1e300 encodes correctly', () { + expect(toFloat64Bytes(1e300), [ + 0x7e, + 0x37, + 0xe4, + 0x3c, + 0x88, + 0x00, + 0x75, + 0x9c, + ]); + }); + }); + }); + + group('Float64 (double-precision) decoding - fromFloat64Bytes', () { + group('Special values', () { + test('NaN decodes correctly', () { + expect( + fromFloat64Bytes([ + 0x7f, + 0xf8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]).isNaN, + true, + ); + }); + + test('+Infinity decodes correctly', () { + expect( + fromFloat64Bytes([0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + double.infinity, + ); + }); + + test('-Infinity decodes correctly', () { + expect( + fromFloat64Bytes([0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + double.negativeInfinity, + ); + }); + + test('+0.0 decodes correctly', () { + expect( + fromFloat64Bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + 0.0, + ); + }); + + test('-0.0 decodes correctly', () { + final result = fromFloat64Bytes([ + 0x80, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]); + expect(result, 0.0); + expect(result.isNegative, true); + }); + }); + + group('Normal values', () { + test('1.0 decodes correctly', () { + expect( + fromFloat64Bytes([0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + 1.0, + ); + }); + + test('-1.0 decodes correctly', () { + expect( + fromFloat64Bytes([0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + -1.0, + ); + }); + + test('1e300 decodes correctly', () { + expect( + fromFloat64Bytes([0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]), + 1e300, + ); + }); + }); + }); + + group('Float64 round-trip tests', () { + test('All values round-trip exactly', () { + final testValues = [ + 0.0, + -0.0, + 1.0, + -1.0, + 2.0, + 0.5, + 1.1, + 3.14159265358979323846, + 1e300, + 1e-300, + double.infinity, + double.negativeInfinity, + // Edge cases + double.minPositive, + double.maxFinite, + -double.maxFinite, + ]; + + for (final value in testValues) { + final bytes = toFloat64Bytes(value); + final decoded = fromFloat64Bytes(bytes); + if (value.isNaN) { + expect(decoded.isNaN, true, reason: 'NaN should round-trip'); + } else { + expect(decoded, value, reason: '$value should round-trip exactly'); + } + } + }); + }); + + group('isFloat64Lossless', () { + test('Always returns true', () { + expect(isFloat64Lossless(0.0), true); + expect(isFloat64Lossless(-0.0), true); + expect(isFloat64Lossless(1.0), true); + expect(isFloat64Lossless(double.nan), true); + expect(isFloat64Lossless(double.infinity), true); + expect(isFloat64Lossless(double.negativeInfinity), true); + expect(isFloat64Lossless(1e300), true); + expect(isFloat64Lossless(double.minPositive), true); + expect(isFloat64Lossless(double.maxFinite), true); + }); + }); + + group('Cross-platform consistency (JS-specific bugs)', () { + // These tests verify fixes for issues with the ieee754 package on JS + test('Negative values encode correctly (sign bit handling)', () { + // -4.0 was incorrectly encoded on JS due to setInt16 sign extension + expect(toFloat16Bytes(-4.0), [0xc4, 0x00]); + expect(fromFloat16Bytes([0xc4, 0x00]), -4.0); + }); + + test('Negative zero encodes correctly', () { + // Negative zero was problematic on some platforms + final bytes = toFloat16Bytes(-0.0); + expect(bytes, [0x80, 0x00]); + final decoded = fromFloat16Bytes(bytes); + expect(decoded, 0.0); + expect(decoded.isNegative, true); + }); + + test('Large positive values with high bit set decode correctly', () { + // Values like 65504 have high bit set in some representations + expect(fromFloat16Bytes([0x7b, 0xff]), 65504.0); + }); + + test('Large negative values decode correctly', () { + // -65504 was incorrectly decoded on JS + expect(fromFloat16Bytes([0xfb, 0xff]), -65504.0); + }); + }); + + group('Edge cases', () { + test('Byte arrays are the correct length', () { + expect(toFloat16Bytes(1.0).length, 2); + expect(toFloat32Bytes(1.0).length, 4); + expect(toFloat64Bytes(1.0).length, 8); + }); + + test('Bytes are in big-endian order', () { + // Float16: 1.0 = 0x3c00, so first byte should be 0x3c + expect(toFloat16Bytes(1.0)[0], 0x3c); + expect(toFloat16Bytes(1.0)[1], 0x00); + + // Float32: 1.0 = 0x3f800000, so first byte should be 0x3f + expect(toFloat32Bytes(1.0)[0], 0x3f); + expect(toFloat32Bytes(1.0)[3], 0x00); + + // Float64: 1.0 = 0x3ff0000000000000, so first byte should be 0x3f + expect(toFloat64Bytes(1.0)[0], 0x3f); + expect(toFloat64Bytes(1.0)[7], 0x00); + }); + }); +} From 89f577ab2ebe6fb1dcf9e83d2ac9c2f71edf6450 Mon Sep 17 00:00:00 2001 From: Alexandru Dochioiu Date: Wed, 21 Jan 2026 18:08:12 +0400 Subject: [PATCH 12/12] test: add ieee754 validation tests for float_utils Add ieee754 package as dev dependency and use it to validate that the custom float_utils implementation produces identical results to ieee754 on non-JS platforms. Validation is performed inline within each existing test using helper functions that compare results against ieee754 (skipped on JS where ieee754 has known issues). Co-Authored-By: Claude Opus 4.5 --- pubspec.yaml | 1 + test/float_utils_test.dart | 654 +++++++++++++++++++++++++------------ 2 files changed, 449 insertions(+), 206 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7f15ed9..74e7a76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,3 +24,4 @@ dev_dependencies: build_runner: '^2.10.5' build_test: '^3.5.5' build_web_compilers: '^4.4.7' + ieee754: '^1.0.3' # For validating custom float_utils implementation in tests diff --git a/test/float_utils_test.dart b/test/float_utils_test.dart index 18e887b..7229940 100644 --- a/test/float_utils_test.dart +++ b/test/float_utils_test.dart @@ -5,218 +5,451 @@ * Copyright : S.Hamblett */ +import 'dart:typed_data'; + import 'package:cbor/src/utils/float_utils.dart'; +import 'package:ieee754/ieee754.dart' as ieee754; import 'package:test/test.dart'; +/// Platform detection: identical(0, 0.0) returns true only on JS. +final bool _kIsJs = identical(0, 0.0); + +// Helper functions to validate against ieee754 package on non-JS platforms. +// On JS, ieee754 has known issues with setInt16/getInt16, so we skip validation there. + +/// Validates that toFloat16Bytes matches ieee754 (non-JS only). +void _validateFloat16Encoding(double value, List result) { + if (_kIsJs) return; + final ieee754Bytes = ieee754.FloatParts.fromDouble(value).toFloat16Bytes(); + if (value.isNaN) { + // NaN can have different representations, just verify both decode to NaN + expect(fromFloat16Bytes(result).isNaN, true); + expect( + ieee754.FloatParts.fromFloat16Bytes(ieee754Bytes).toDouble().isNaN, + true, + ); + } else { + expect(result, ieee754Bytes, reason: 'Should match ieee754 for $value'); + } +} + +/// Validates that fromFloat16Bytes matches ieee754 (non-JS only). +void _validateFloat16Decoding(List bytes, double result) { + if (_kIsJs) return; + final ieee754Value = + ieee754.FloatParts.fromFloat16Bytes(Uint8List.fromList(bytes)).toDouble(); + if (ieee754Value.isNaN) { + expect(result.isNaN, true); + } else { + expect(result, ieee754Value, reason: 'Should match ieee754'); + } +} + +/// Validates that toFloat32Bytes matches ieee754 (non-JS only). +void _validateFloat32Encoding(double value, List result) { + if (_kIsJs) return; + final ieee754Bytes = ieee754.FloatParts.fromDouble(value).toFloat32Bytes(); + if (value.isNaN) { + expect(fromFloat32Bytes(result).isNaN, true); + expect( + ieee754.FloatParts.fromFloat32Bytes(ieee754Bytes).toDouble().isNaN, + true, + ); + } else { + expect(result, ieee754Bytes, reason: 'Should match ieee754 for $value'); + } +} + +/// Validates that fromFloat32Bytes matches ieee754 (non-JS only). +void _validateFloat32Decoding(List bytes, double result) { + if (_kIsJs) return; + final ieee754Value = + ieee754.FloatParts.fromFloat32Bytes(Uint8List.fromList(bytes)).toDouble(); + if (ieee754Value.isNaN) { + expect(result.isNaN, true); + } else { + expect(result, ieee754Value, reason: 'Should match ieee754'); + } +} + +/// Validates that toFloat64Bytes matches ieee754 (non-JS only). +void _validateFloat64Encoding(double value, List result) { + if (_kIsJs) return; + final ieee754Bytes = ieee754.FloatParts.fromDouble(value).toFloat64Bytes(); + if (value.isNaN) { + expect(fromFloat64Bytes(result).isNaN, true); + expect( + ieee754.FloatParts.fromFloat64Bytes(ieee754Bytes).toDouble().isNaN, + true, + ); + } else { + expect(result, ieee754Bytes, reason: 'Should match ieee754 for $value'); + } +} + +/// Validates that fromFloat64Bytes matches ieee754 (non-JS only). +void _validateFloat64Decoding(List bytes, double result) { + if (_kIsJs) return; + final ieee754Value = + ieee754.FloatParts.fromFloat64Bytes(Uint8List.fromList(bytes)).toDouble(); + if (ieee754Value.isNaN) { + expect(result.isNaN, true); + } else { + expect(result, ieee754Value, reason: 'Should match ieee754'); + } +} + +/// Validates that isFloat16Lossless matches ieee754 (non-JS only). +void _validateIsFloat16Lossless(double value, bool result) { + if (_kIsJs) return; + final ieee754Result = ieee754.FloatParts.fromDouble(value).isFloat16Lossless; + expect(result, ieee754Result, reason: 'Should match ieee754 for $value'); +} + +/// Validates that isFloat32Lossless matches ieee754 (non-JS only). +void _validateIsFloat32Lossless(double value, bool result) { + if (_kIsJs) return; + final ieee754Result = ieee754.FloatParts.fromDouble(value).isFloat32Lossless; + expect(result, ieee754Result, reason: 'Should match ieee754 for $value'); +} + void main() { group('Float16 (half-precision) encoding - toFloat16Bytes', () { group('Special values', () { test('NaN encodes to canonical NaN (0x7e00)', () { - expect(toFloat16Bytes(double.nan), [0x7e, 0x00]); + final result = toFloat16Bytes(double.nan); + expect(result, [0x7e, 0x00]); + _validateFloat16Encoding(double.nan, result); }); test('+Infinity encodes to 0x7c00', () { - expect(toFloat16Bytes(double.infinity), [0x7c, 0x00]); + final result = toFloat16Bytes(double.infinity); + expect(result, [0x7c, 0x00]); + _validateFloat16Encoding(double.infinity, result); }); test('-Infinity encodes to 0xfc00', () { - expect(toFloat16Bytes(double.negativeInfinity), [0xfc, 0x00]); + final result = toFloat16Bytes(double.negativeInfinity); + expect(result, [0xfc, 0x00]); + _validateFloat16Encoding(double.negativeInfinity, result); }); test('+0.0 encodes to 0x0000', () { - expect(toFloat16Bytes(0.0), [0x00, 0x00]); + final result = toFloat16Bytes(0.0); + expect(result, [0x00, 0x00]); + _validateFloat16Encoding(0.0, result); }); test('-0.0 encodes to 0x8000', () { - expect(toFloat16Bytes(-0.0), [0x80, 0x00]); + final result = toFloat16Bytes(-0.0); + expect(result, [0x80, 0x00]); + _validateFloat16Encoding(-0.0, result); }); }); group('Normal positive values', () { test('1.0 encodes to 0x3c00', () { - expect(toFloat16Bytes(1.0), [0x3c, 0x00]); + final result = toFloat16Bytes(1.0); + expect(result, [0x3c, 0x00]); + _validateFloat16Encoding(1.0, result); }); test('1.5 encodes to 0x3e00', () { - expect(toFloat16Bytes(1.5), [0x3e, 0x00]); + final result = toFloat16Bytes(1.5); + expect(result, [0x3e, 0x00]); + _validateFloat16Encoding(1.5, result); }); test('2.0 encodes to 0x4000', () { - expect(toFloat16Bytes(2.0), [0x40, 0x00]); + final result = toFloat16Bytes(2.0); + expect(result, [0x40, 0x00]); + _validateFloat16Encoding(2.0, result); }); test('0.5 encodes to 0x3800', () { - expect(toFloat16Bytes(0.5), [0x38, 0x00]); + final result = toFloat16Bytes(0.5); + expect(result, [0x38, 0x00]); + _validateFloat16Encoding(0.5, result); }); test('65504.0 (max normal) encodes to 0x7bff', () { - expect(toFloat16Bytes(65504.0), [0x7b, 0xff]); + final result = toFloat16Bytes(65504.0); + expect(result, [0x7b, 0xff]); + _validateFloat16Encoding(65504.0, result); }); test('0.00006103515625 (min positive normal) encodes to 0x0400', () { - expect(toFloat16Bytes(0.00006103515625), [0x04, 0x00]); + final result = toFloat16Bytes(0.00006103515625); + expect(result, [0x04, 0x00]); + _validateFloat16Encoding(0.00006103515625, result); }); }); group('Normal negative values', () { test('-1.0 encodes to 0xbc00', () { - expect(toFloat16Bytes(-1.0), [0xbc, 0x00]); + final result = toFloat16Bytes(-1.0); + expect(result, [0xbc, 0x00]); + _validateFloat16Encoding(-1.0, result); }); test('-1.5 encodes to 0xbe00', () { - expect(toFloat16Bytes(-1.5), [0xbe, 0x00]); + final result = toFloat16Bytes(-1.5); + expect(result, [0xbe, 0x00]); + _validateFloat16Encoding(-1.5, result); }); test('-2.0 encodes to 0xc000', () { - expect(toFloat16Bytes(-2.0), [0xc0, 0x00]); + final result = toFloat16Bytes(-2.0); + expect(result, [0xc0, 0x00]); + _validateFloat16Encoding(-2.0, result); }); test('-4.0 encodes to 0xc400', () { - expect(toFloat16Bytes(-4.0), [0xc4, 0x00]); + final result = toFloat16Bytes(-4.0); + expect(result, [0xc4, 0x00]); + _validateFloat16Encoding(-4.0, result); }); test('-65504.0 (min normal) encodes to 0xfbff', () { - expect(toFloat16Bytes(-65504.0), [0xfb, 0xff]); + final result = toFloat16Bytes(-65504.0); + expect(result, [0xfb, 0xff]); + _validateFloat16Encoding(-65504.0, result); }); }); group('Subnormal values', () { test('5.960464477539063e-8 (smallest subnormal) encodes to 0x0001', () { - expect(toFloat16Bytes(5.960464477539063e-8), [0x00, 0x01]); + final result = toFloat16Bytes(5.960464477539063e-8); + expect(result, [0x00, 0x01]); + _validateFloat16Encoding(5.960464477539063e-8, result); }); test('6.097555160522461e-5 (largest subnormal) encodes to 0x03ff', () { - expect(toFloat16Bytes(6.097555160522461e-5), [0x03, 0xff]); + final result = toFloat16Bytes(6.097555160522461e-5); + expect(result, [0x03, 0xff]); + _validateFloat16Encoding(6.097555160522461e-5, result); }); }); group('Overflow to infinity', () { test('65536.0 overflows to +Infinity', () { - expect(toFloat16Bytes(65536.0), [0x7c, 0x00]); + final result = toFloat16Bytes(65536.0); + expect(result, [0x7c, 0x00]); + _validateFloat16Encoding(65536.0, result); }); test('100000.0 overflows to +Infinity', () { - expect(toFloat16Bytes(100000.0), [0x7c, 0x00]); + final result = toFloat16Bytes(100000.0); + expect(result, [0x7c, 0x00]); + _validateFloat16Encoding(100000.0, result); }); test('-100000.0 overflows to -Infinity', () { - expect(toFloat16Bytes(-100000.0), [0xfc, 0x00]); + final result = toFloat16Bytes(-100000.0); + expect(result, [0xfc, 0x00]); + _validateFloat16Encoding(-100000.0, result); }); }); group('Underflow to zero', () { test('Very small positive value underflows to +0', () { - expect(toFloat16Bytes(1e-10), [0x00, 0x00]); + final result = toFloat16Bytes(1e-10); + expect(result, [0x00, 0x00]); + _validateFloat16Encoding(1e-10, result); }); test('Very small negative value underflows to -0', () { - expect(toFloat16Bytes(-1e-10), [0x80, 0x00]); + final result = toFloat16Bytes(-1e-10); + expect(result, [0x80, 0x00]); + _validateFloat16Encoding(-1e-10, result); }); }); group('RFC 8949 Appendix A test vectors', () { - // These are the exact values from RFC 8949 - test('0.0', () => expect(toFloat16Bytes(0.0), [0x00, 0x00])); - test('-0.0', () => expect(toFloat16Bytes(-0.0), [0x80, 0x00])); - test('1.0', () => expect(toFloat16Bytes(1.0), [0x3c, 0x00])); - test('1.5', () => expect(toFloat16Bytes(1.5), [0x3e, 0x00])); - test('65504.0', () => expect(toFloat16Bytes(65504.0), [0x7b, 0xff])); + test('0.0', () { + final result = toFloat16Bytes(0.0); + expect(result, [0x00, 0x00]); + _validateFloat16Encoding(0.0, result); + }); + test('-0.0', () { + final result = toFloat16Bytes(-0.0); + expect(result, [0x80, 0x00]); + _validateFloat16Encoding(-0.0, result); + }); + test('1.0', () { + final result = toFloat16Bytes(1.0); + expect(result, [0x3c, 0x00]); + _validateFloat16Encoding(1.0, result); + }); + test('1.5', () { + final result = toFloat16Bytes(1.5); + expect(result, [0x3e, 0x00]); + _validateFloat16Encoding(1.5, result); + }); + test('65504.0', () { + final result = toFloat16Bytes(65504.0); + expect(result, [0x7b, 0xff]); + _validateFloat16Encoding(65504.0, result); + }); test('5.960464477539063e-8', () { - expect(toFloat16Bytes(5.960464477539063e-8), [0x00, 0x01]); + final result = toFloat16Bytes(5.960464477539063e-8); + expect(result, [0x00, 0x01]); + _validateFloat16Encoding(5.960464477539063e-8, result); }); test('0.00006103515625', () { - expect(toFloat16Bytes(0.00006103515625), [0x04, 0x00]); + final result = toFloat16Bytes(0.00006103515625); + expect(result, [0x04, 0x00]); + _validateFloat16Encoding(0.00006103515625, result); + }); + test('-4.0', () { + final result = toFloat16Bytes(-4.0); + expect(result, [0xc4, 0x00]); + _validateFloat16Encoding(-4.0, result); }); - test('-4.0', () => expect(toFloat16Bytes(-4.0), [0xc4, 0x00])); }); }); group('Float16 (half-precision) decoding - fromFloat16Bytes', () { group('Special values', () { test('0x7e00 decodes to NaN', () { - expect(fromFloat16Bytes([0x7e, 0x00]).isNaN, true); + const bytes = [0x7e, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result.isNaN, true); + _validateFloat16Decoding(bytes, result); }); test('0x7c01 (NaN with payload) decodes to NaN', () { - expect(fromFloat16Bytes([0x7c, 0x01]).isNaN, true); + const bytes = [0x7c, 0x01]; + final result = fromFloat16Bytes(bytes); + expect(result.isNaN, true); + _validateFloat16Decoding(bytes, result); }); test('0x7c00 decodes to +Infinity', () { - expect(fromFloat16Bytes([0x7c, 0x00]), double.infinity); + const bytes = [0x7c, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, double.infinity); + _validateFloat16Decoding(bytes, result); }); test('0xfc00 decodes to -Infinity', () { - expect(fromFloat16Bytes([0xfc, 0x00]), double.negativeInfinity); + const bytes = [0xfc, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, double.negativeInfinity); + _validateFloat16Decoding(bytes, result); }); test('0x0000 decodes to +0.0', () { - final result = fromFloat16Bytes([0x00, 0x00]); + const bytes = [0x00, 0x00]; + final result = fromFloat16Bytes(bytes); expect(result, 0.0); expect(result.isNegative, false); + _validateFloat16Decoding(bytes, result); }); test('0x8000 decodes to -0.0', () { - final result = fromFloat16Bytes([0x80, 0x00]); + const bytes = [0x80, 0x00]; + final result = fromFloat16Bytes(bytes); expect(result, 0.0); expect(result.isNegative, true); + _validateFloat16Decoding(bytes, result); }); }); group('Normal positive values', () { test('0x3c00 decodes to 1.0', () { - expect(fromFloat16Bytes([0x3c, 0x00]), 1.0); + const bytes = [0x3c, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 1.0); + _validateFloat16Decoding(bytes, result); }); test('0x3e00 decodes to 1.5', () { - expect(fromFloat16Bytes([0x3e, 0x00]), 1.5); + const bytes = [0x3e, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 1.5); + _validateFloat16Decoding(bytes, result); }); test('0x4000 decodes to 2.0', () { - expect(fromFloat16Bytes([0x40, 0x00]), 2.0); + const bytes = [0x40, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 2.0); + _validateFloat16Decoding(bytes, result); }); test('0x3800 decodes to 0.5', () { - expect(fromFloat16Bytes([0x38, 0x00]), 0.5); + const bytes = [0x38, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 0.5); + _validateFloat16Decoding(bytes, result); }); test('0x7bff decodes to 65504.0', () { - expect(fromFloat16Bytes([0x7b, 0xff]), 65504.0); + const bytes = [0x7b, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, 65504.0); + _validateFloat16Decoding(bytes, result); }); test('0x0400 decodes to 0.00006103515625', () { - expect(fromFloat16Bytes([0x04, 0x00]), 0.00006103515625); + const bytes = [0x04, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 0.00006103515625); + _validateFloat16Decoding(bytes, result); }); }); group('Normal negative values', () { test('0xbc00 decodes to -1.0', () { - expect(fromFloat16Bytes([0xbc, 0x00]), -1.0); + const bytes = [0xbc, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -1.0); + _validateFloat16Decoding(bytes, result); }); test('0xbe00 decodes to -1.5', () { - expect(fromFloat16Bytes([0xbe, 0x00]), -1.5); + const bytes = [0xbe, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -1.5); + _validateFloat16Decoding(bytes, result); }); test('0xc000 decodes to -2.0', () { - expect(fromFloat16Bytes([0xc0, 0x00]), -2.0); + const bytes = [0xc0, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -2.0); + _validateFloat16Decoding(bytes, result); }); test('0xc400 decodes to -4.0', () { - expect(fromFloat16Bytes([0xc4, 0x00]), -4.0); + const bytes = [0xc4, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -4.0); + _validateFloat16Decoding(bytes, result); }); test('0xfbff decodes to -65504.0', () { - expect(fromFloat16Bytes([0xfb, 0xff]), -65504.0); + const bytes = [0xfb, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, -65504.0); + _validateFloat16Decoding(bytes, result); }); }); group('Subnormal values', () { test('0x0001 decodes to smallest subnormal', () { - expect(fromFloat16Bytes([0x00, 0x01]), 5.960464477539063e-8); + const bytes = [0x00, 0x01]; + final result = fromFloat16Bytes(bytes); + expect(result, 5.960464477539063e-8); + _validateFloat16Decoding(bytes, result); }); test('0x03ff decodes to largest subnormal', () { - expect(fromFloat16Bytes([0x03, 0xff]), 6.097555160522461e-5); + const bytes = [0x03, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, 6.097555160522461e-5); + _validateFloat16Decoding(bytes, result); }); }); }); @@ -253,12 +486,14 @@ void main() { } else { expect(decoded, value, reason: '$value should round-trip'); } + _validateFloat16Encoding(value, bytes); } }); test('NaN round-trips', () { final bytes = toFloat16Bytes(double.nan); expect(fromFloat16Bytes(bytes).isNaN, true); + _validateFloat16Encoding(double.nan, bytes); }); }); @@ -266,37 +501,55 @@ void main() { group('Values that can be represented', () { test('Special values', () { expect(isFloat16Lossless(0.0), true); + _validateIsFloat16Lossless(0.0, true); expect(isFloat16Lossless(-0.0), true); + _validateIsFloat16Lossless(-0.0, true); expect(isFloat16Lossless(double.nan), true); + _validateIsFloat16Lossless(double.nan, true); expect(isFloat16Lossless(double.infinity), true); + _validateIsFloat16Lossless(double.infinity, true); expect(isFloat16Lossless(double.negativeInfinity), true); + _validateIsFloat16Lossless(double.negativeInfinity, true); }); test('Exact representable values', () { expect(isFloat16Lossless(1.0), true); + _validateIsFloat16Lossless(1.0, true); expect(isFloat16Lossless(-1.0), true); + _validateIsFloat16Lossless(-1.0, true); expect(isFloat16Lossless(1.5), true); + _validateIsFloat16Lossless(1.5, true); expect(isFloat16Lossless(2.0), true); + _validateIsFloat16Lossless(2.0, true); expect(isFloat16Lossless(65504.0), true); + _validateIsFloat16Lossless(65504.0, true); expect(isFloat16Lossless(0.00006103515625), true); + _validateIsFloat16Lossless(0.00006103515625, true); expect(isFloat16Lossless(5.960464477539063e-8), true); + _validateIsFloat16Lossless(5.960464477539063e-8, true); }); }); group('Values that cannot be represented', () { test('Values too large', () { expect(isFloat16Lossless(100000.0), false); + _validateIsFloat16Lossless(100000.0, false); expect(isFloat16Lossless(65536.0), false); + _validateIsFloat16Lossless(65536.0, false); }); test('Values with too much precision', () { expect(isFloat16Lossless(1.1), false); + _validateIsFloat16Lossless(1.1, false); expect(isFloat16Lossless(3.14159), false); + _validateIsFloat16Lossless(3.14159, false); expect(isFloat16Lossless(1.0001), false); + _validateIsFloat16Lossless(1.0001, false); }); test('Values too small (non-zero)', () { expect(isFloat16Lossless(1e-10), false); + _validateIsFloat16Lossless(1e-10, false); }); }); }); @@ -309,58 +562,69 @@ void main() { // NaN has exponent all 1s and non-zero mantissa expect(bytes[0] & 0x7F, 0x7F); // High bit of exponent expect(bytes[1] & 0x80, 0x80); // Low bit of exponent + _validateFloat32Encoding(double.nan, bytes); }); test('+Infinity encodes to 0x7f800000', () { - expect(toFloat32Bytes(double.infinity), [0x7f, 0x80, 0x00, 0x00]); + final result = toFloat32Bytes(double.infinity); + expect(result, [0x7f, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(double.infinity, result); }); test('-Infinity encodes to 0xff800000', () { - expect(toFloat32Bytes(double.negativeInfinity), [ - 0xff, - 0x80, - 0x00, - 0x00, - ]); + final result = toFloat32Bytes(double.negativeInfinity); + expect(result, [0xff, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(double.negativeInfinity, result); }); test('+0.0 encodes to 0x00000000', () { - expect(toFloat32Bytes(0.0), [0x00, 0x00, 0x00, 0x00]); + final result = toFloat32Bytes(0.0); + expect(result, [0x00, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(0.0, result); }); test('-0.0 encodes to 0x80000000', () { - expect(toFloat32Bytes(-0.0), [0x80, 0x00, 0x00, 0x00]); + final result = toFloat32Bytes(-0.0); + expect(result, [0x80, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(-0.0, result); }); }); group('Normal values', () { test('1.0 encodes to 0x3f800000', () { - expect(toFloat32Bytes(1.0), [0x3f, 0x80, 0x00, 0x00]); + final result = toFloat32Bytes(1.0); + expect(result, [0x3f, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(1.0, result); }); test('-1.0 encodes to 0xbf800000', () { - expect(toFloat32Bytes(-1.0), [0xbf, 0x80, 0x00, 0x00]); + final result = toFloat32Bytes(-1.0); + expect(result, [0xbf, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(-1.0, result); }); test('2.0 encodes to 0x40000000', () { - expect(toFloat32Bytes(2.0), [0x40, 0x00, 0x00, 0x00]); + final result = toFloat32Bytes(2.0); + expect(result, [0x40, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(2.0, result); }); test('0.5 encodes to 0x3f000000', () { - expect(toFloat32Bytes(0.5), [0x3f, 0x00, 0x00, 0x00]); + final result = toFloat32Bytes(0.5); + expect(result, [0x3f, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(0.5, result); }); test('100000.0 encodes to 0x47c35000', () { - expect(toFloat32Bytes(100000.0), [0x47, 0xc3, 0x50, 0x00]); + final result = toFloat32Bytes(100000.0); + expect(result, [0x47, 0xc3, 0x50, 0x00]); + _validateFloat32Encoding(100000.0, result); }); test('3.4028234663852886e+38 (max float32)', () { - expect(toFloat32Bytes(3.4028234663852886e+38), [ - 0x7f, - 0x7f, - 0xff, - 0xff, - ]); + final result = toFloat32Bytes(3.4028234663852886e+38); + expect(result, [0x7f, 0x7f, 0xff, 0xff]); + _validateFloat32Encoding(3.4028234663852886e+38, result); }); }); }); @@ -368,49 +632,69 @@ void main() { group('Float32 (single-precision) decoding - fromFloat32Bytes', () { group('Special values', () { test('0x7fc00000 decodes to NaN', () { - expect(fromFloat32Bytes([0x7f, 0xc0, 0x00, 0x00]).isNaN, true); + const bytes = [0x7f, 0xc0, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result.isNaN, true); + _validateFloat32Decoding(bytes, result); }); test('0x7f800000 decodes to +Infinity', () { - expect(fromFloat32Bytes([0x7f, 0x80, 0x00, 0x00]), double.infinity); + const bytes = [0x7f, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, double.infinity); + _validateFloat32Decoding(bytes, result); }); test('0xff800000 decodes to -Infinity', () { - expect( - fromFloat32Bytes([0xff, 0x80, 0x00, 0x00]), - double.negativeInfinity, - ); + const bytes = [0xff, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, double.negativeInfinity); + _validateFloat32Decoding(bytes, result); }); test('0x00000000 decodes to +0.0', () { - expect(fromFloat32Bytes([0x00, 0x00, 0x00, 0x00]), 0.0); + const bytes = [0x00, 0x00, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, 0.0); + _validateFloat32Decoding(bytes, result); }); test('0x80000000 decodes to -0.0', () { - final result = fromFloat32Bytes([0x80, 0x00, 0x00, 0x00]); + const bytes = [0x80, 0x00, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); expect(result, 0.0); expect(result.isNegative, true); + _validateFloat32Decoding(bytes, result); }); }); group('Normal values', () { test('0x3f800000 decodes to 1.0', () { - expect(fromFloat32Bytes([0x3f, 0x80, 0x00, 0x00]), 1.0); + const bytes = [0x3f, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, 1.0); + _validateFloat32Decoding(bytes, result); }); test('0xbf800000 decodes to -1.0', () { - expect(fromFloat32Bytes([0xbf, 0x80, 0x00, 0x00]), -1.0); + const bytes = [0xbf, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, -1.0); + _validateFloat32Decoding(bytes, result); }); test('0x47c35000 decodes to 100000.0', () { - expect(fromFloat32Bytes([0x47, 0xc3, 0x50, 0x00]), 100000.0); + const bytes = [0x47, 0xc3, 0x50, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, 100000.0); + _validateFloat32Decoding(bytes, result); }); test('0x7f7fffff decodes to max float32', () { - expect( - fromFloat32Bytes([0x7f, 0x7f, 0xff, 0xff]), - closeTo(3.4028234663852886e+38, 1e31), - ); + const bytes = [0x7f, 0x7f, 0xff, 0xff]; + final result = fromFloat32Bytes(bytes); + expect(result, closeTo(3.4028234663852886e+38, 1e31)); + _validateFloat32Decoding(bytes, result); }); }); }); @@ -449,6 +733,7 @@ void main() { reason: '$value should round-trip', ); } + _validateFloat32Encoding(value, bytes); } }); }); @@ -456,24 +741,35 @@ void main() { group('isFloat32Lossless', () { test('Special values', () { expect(isFloat32Lossless(0.0), true); + _validateIsFloat32Lossless(0.0, true); expect(isFloat32Lossless(-0.0), true); + _validateIsFloat32Lossless(-0.0, true); expect(isFloat32Lossless(double.nan), true); + _validateIsFloat32Lossless(double.nan, true); expect(isFloat32Lossless(double.infinity), true); + _validateIsFloat32Lossless(double.infinity, true); expect(isFloat32Lossless(double.negativeInfinity), true); + _validateIsFloat32Lossless(double.negativeInfinity, true); }); test('Values that fit in float32', () { expect(isFloat32Lossless(1.0), true); + _validateIsFloat32Lossless(1.0, true); expect(isFloat32Lossless(-1.0), true); + _validateIsFloat32Lossless(-1.0, true); expect(isFloat32Lossless(100000.0), true); + _validateIsFloat32Lossless(100000.0, true); expect(isFloat32Lossless(3.4028234663852886e+38), true); + _validateIsFloat32Lossless(3.4028234663852886e+38, true); }); test('Values that do not fit in float32', () { // Very large values expect(isFloat32Lossless(1e300), false); + _validateIsFloat32Lossless(1e300, false); // Values with too much precision expect(isFloat32Lossless(1.0000000001), false); + _validateIsFloat32Lossless(1.0000000001, false); }); }); @@ -482,99 +778,51 @@ void main() { test('NaN encodes correctly', () { final bytes = toFloat64Bytes(double.nan); expect(bytes.length, 8); + _validateFloat64Encoding(double.nan, bytes); }); test('+Infinity encodes to 0x7ff0000000000000', () { - expect(toFloat64Bytes(double.infinity), [ - 0x7f, - 0xf0, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + final result = toFloat64Bytes(double.infinity); + expect(result, [0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(double.infinity, result); }); test('-Infinity encodes to 0xfff0000000000000', () { - expect(toFloat64Bytes(double.negativeInfinity), [ - 0xff, - 0xf0, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + final result = toFloat64Bytes(double.negativeInfinity); + expect(result, [0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(double.negativeInfinity, result); }); test('+0.0 encodes to all zeros', () { - expect(toFloat64Bytes(0.0), [ - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + final result = toFloat64Bytes(0.0); + expect(result, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(0.0, result); }); test('-0.0 encodes to 0x8000000000000000', () { - expect(toFloat64Bytes(-0.0), [ - 0x80, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + final result = toFloat64Bytes(-0.0); + expect(result, [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(-0.0, result); }); }); group('Normal values', () { test('1.0 encodes to 0x3ff0000000000000', () { - expect(toFloat64Bytes(1.0), [ - 0x3f, - 0xf0, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + final result = toFloat64Bytes(1.0); + expect(result, [0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(1.0, result); }); test('-1.0 encodes to 0xbff0000000000000', () { - expect(toFloat64Bytes(-1.0), [ - 0xbf, - 0xf0, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + final result = toFloat64Bytes(-1.0); + expect(result, [0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(-1.0, result); }); test('1e300 encodes correctly', () { - expect(toFloat64Bytes(1e300), [ - 0x7e, - 0x37, - 0xe4, - 0x3c, - 0x88, - 0x00, - 0x75, - 0x9c, - ]); + final result = toFloat64Bytes(1e300); + expect(result, [0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); + _validateFloat64Encoding(1e300, result); }); }); }); @@ -582,78 +830,62 @@ void main() { group('Float64 (double-precision) decoding - fromFloat64Bytes', () { group('Special values', () { test('NaN decodes correctly', () { - expect( - fromFloat64Bytes([ - 0x7f, - 0xf8, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]).isNaN, - true, - ); + const bytes = [0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); + expect(result.isNaN, true); + _validateFloat64Decoding(bytes, result); }); test('+Infinity decodes correctly', () { - expect( - fromFloat64Bytes([0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - double.infinity, - ); + const bytes = [0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); + expect(result, double.infinity); + _validateFloat64Decoding(bytes, result); }); test('-Infinity decodes correctly', () { - expect( - fromFloat64Bytes([0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - double.negativeInfinity, - ); + const bytes = [0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); + expect(result, double.negativeInfinity); + _validateFloat64Decoding(bytes, result); }); test('+0.0 decodes correctly', () { - expect( - fromFloat64Bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - 0.0, - ); + const bytes = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); + expect(result, 0.0); + _validateFloat64Decoding(bytes, result); }); test('-0.0 decodes correctly', () { - final result = fromFloat64Bytes([ - 0x80, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - ]); + const bytes = [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); expect(result, 0.0); expect(result.isNegative, true); + _validateFloat64Decoding(bytes, result); }); }); group('Normal values', () { test('1.0 decodes correctly', () { - expect( - fromFloat64Bytes([0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - 1.0, - ); + const bytes = [0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); + expect(result, 1.0); + _validateFloat64Decoding(bytes, result); }); test('-1.0 decodes correctly', () { - expect( - fromFloat64Bytes([0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - -1.0, - ); + const bytes = [0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + final result = fromFloat64Bytes(bytes); + expect(result, -1.0); + _validateFloat64Decoding(bytes, result); }); test('1e300 decodes correctly', () { - expect( - fromFloat64Bytes([0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]), - 1e300, - ); + const bytes = [0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]; + final result = fromFloat64Bytes(bytes); + expect(result, 1e300); + _validateFloat64Decoding(bytes, result); }); }); }); @@ -687,6 +919,7 @@ void main() { } else { expect(decoded, value, reason: '$value should round-trip exactly'); } + _validateFloat64Encoding(value, bytes); } }); }); @@ -709,8 +942,10 @@ void main() { // These tests verify fixes for issues with the ieee754 package on JS test('Negative values encode correctly (sign bit handling)', () { // -4.0 was incorrectly encoded on JS due to setInt16 sign extension - expect(toFloat16Bytes(-4.0), [0xc4, 0x00]); + final encoded = toFloat16Bytes(-4.0); + expect(encoded, [0xc4, 0x00]); expect(fromFloat16Bytes([0xc4, 0x00]), -4.0); + _validateFloat16Encoding(-4.0, encoded); }); test('Negative zero encodes correctly', () { @@ -720,16 +955,23 @@ void main() { final decoded = fromFloat16Bytes(bytes); expect(decoded, 0.0); expect(decoded.isNegative, true); + _validateFloat16Encoding(-0.0, bytes); }); test('Large positive values with high bit set decode correctly', () { // Values like 65504 have high bit set in some representations - expect(fromFloat16Bytes([0x7b, 0xff]), 65504.0); + const bytes = [0x7b, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, 65504.0); + _validateFloat16Decoding(bytes, result); }); test('Large negative values decode correctly', () { // -65504 was incorrectly decoded on JS - expect(fromFloat16Bytes([0xfb, 0xff]), -65504.0); + const bytes = [0xfb, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, -65504.0); + _validateFloat16Decoding(bytes, result); }); });