diff --git a/lib/src/decoder/pretty_print.dart b/lib/src/decoder/pretty_print.dart index bbe6f65..a4924bb 100644 --- a/lib/src/decoder/pretty_print.dart +++ b/lib/src/decoder/pretty_print.dart @@ -7,8 +7,7 @@ import 'dart:convert'; -import 'package:ieee754/ieee754.dart'; - +import '../utils/float_utils.dart' as float_utils; import 'stage1.dart'; import '../constants.dart'; import '../value/value.dart'; @@ -140,17 +139,17 @@ class _PrettyPrint implements Sink { break; case CborAdditionalInfo.halfPrecisionFloat: writer.write( - '(${FloatParts.fromFloat16Bytes(x.header.dataBytes).toDouble()})', + '(${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 a17c399..84857a4 100644 --- a/lib/src/decoder/stage3.dart +++ b/lib/src/decoder/stage3.dart @@ -8,8 +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/float_utils.dart' as float_utils; import '../utils/utils.dart'; import '../constants.dart'; import 'stage2.dart'; @@ -257,13 +257,13 @@ CborFloat _createFloat(RawValueTagged raw) { final double value; switch (raw.header.additionalInfo) { case CborAdditionalInfo.halfPrecisionFloat: - value = FloatParts.fromFloat16Bytes(raw.header.dataBytes).toDouble(); + 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/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/utils/float_utils.dart b/lib/src/utils/float_utils.dart new file mode 100644 index 0000000..e969cba --- /dev/null +++ b/lib/src/utils/float_utils.dart @@ -0,0 +1,225 @@ +/* + * Package : Cbor + * Author : S. Hamblett + * Date : 21/01/2026 + * Copyright : S.Hamblett + */ + +import 'dart:typed_data'; + +/// Custom half-precision (float16) encoding/decoding that works correctly on all platforms. +/// +/// ## 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. +/// +/// 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.first == 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.first >> 7) & 1; + final exponent = + ((doubleBytes.first & 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 = (mantissaHigh | 0x400) >> 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 = fromFloat16Bytes(halfBytes); + + return reconstructed == value; +} + +/// 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.first << 8) | bytes[1]; + + final sign = (halfValue >> 15) & 1; + final exponent = (halfValue >> 10) & 0x1F; + final mantissa = halfValue & 0x3FF; + + double result; + + if (exponent == 0) { + result = mantissa == 0 ? 0.0 : mantissa / 1024.0 * (1.0 / 16384.0); + } else if (exponent == 31) { + result = mantissa == 0 ? double.infinity : double.nan; + } else { + // Normal number + // ignore: binary-expression-operand-order + 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; + } +} + +/// 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 _) { + 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 f6808c3..34a0ce7 100644 --- a/lib/src/value/double.dart +++ b/lib/src/value/double.dart @@ -7,10 +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/float_utils.dart' as float_utils; import 'internal.dart'; /// Enumeration to allow the user to select which level of precision to @@ -79,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(parts.toFloat16Bytes()); - } 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(parts.toFloat16Bytes()); + sink.add(float_utils.toFloat16Bytes(value)); } else { // Invalid conversion throw ArgumentError( @@ -123,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( @@ -138,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 927dd5b..5280f28 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 []); } @@ -89,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 d9b7be6..95d2fab 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/float_utils.dart' as float_utils; /// Base class for all Typed Arrays sealed class CborTypedArray extends CborBytesImpl { @@ -184,6 +185,14 @@ class CborUint32LittleEndianArray extends CborTypedArray { } } +/// Platform detection constants +/// 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'); +final bool _kIsWeb = _kIsJs || _kIsWasm; + /// uint64 big endian class CborUint64BigEndianArray extends CborTypedArray { const CborUint64BigEndianArray(super.bytes, {super.tags}); @@ -192,7 +201,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 +220,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 +233,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 +258,19 @@ 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 +285,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 +470,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 +503,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 +544,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 +578,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; } } @@ -496,11 +623,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(float_utils.fromFloat16Bytes(chunk)); } return list; } @@ -520,12 +644,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(float_utils.fromFloat16Bytes(chunk)); } return list; } diff --git a/lib/src/value/value.dart b/lib/src/value/value.dart index 2a6cd62..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,7 +131,21 @@ abstract class CborValue { return CborNull(); } else if (object is CborValue) { return 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/pubspec.yaml b/pubspec.yaml index 0fa123d..74e7a76 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' @@ -25,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/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/float_utils_test.dart b/test/float_utils_test.dart new file mode 100644 index 0000000..7229940 --- /dev/null +++ b/test/float_utils_test.dart @@ -0,0 +1,999 @@ +/* + * Package : Cbor + * Author : S. Hamblett + * Date : 21/01/2026 + * 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)', () { + final result = toFloat16Bytes(double.nan); + expect(result, [0x7e, 0x00]); + _validateFloat16Encoding(double.nan, result); + }); + + test('+Infinity encodes to 0x7c00', () { + final result = toFloat16Bytes(double.infinity); + expect(result, [0x7c, 0x00]); + _validateFloat16Encoding(double.infinity, result); + }); + + test('-Infinity encodes to 0xfc00', () { + final result = toFloat16Bytes(double.negativeInfinity); + expect(result, [0xfc, 0x00]); + _validateFloat16Encoding(double.negativeInfinity, result); + }); + + test('+0.0 encodes to 0x0000', () { + final result = toFloat16Bytes(0.0); + expect(result, [0x00, 0x00]); + _validateFloat16Encoding(0.0, result); + }); + + test('-0.0 encodes to 0x8000', () { + final result = toFloat16Bytes(-0.0); + expect(result, [0x80, 0x00]); + _validateFloat16Encoding(-0.0, result); + }); + }); + + group('Normal positive values', () { + test('1.0 encodes to 0x3c00', () { + final result = toFloat16Bytes(1.0); + expect(result, [0x3c, 0x00]); + _validateFloat16Encoding(1.0, result); + }); + + test('1.5 encodes to 0x3e00', () { + final result = toFloat16Bytes(1.5); + expect(result, [0x3e, 0x00]); + _validateFloat16Encoding(1.5, result); + }); + + test('2.0 encodes to 0x4000', () { + final result = toFloat16Bytes(2.0); + expect(result, [0x40, 0x00]); + _validateFloat16Encoding(2.0, result); + }); + + test('0.5 encodes to 0x3800', () { + final result = toFloat16Bytes(0.5); + expect(result, [0x38, 0x00]); + _validateFloat16Encoding(0.5, result); + }); + + test('65504.0 (max normal) encodes to 0x7bff', () { + final result = toFloat16Bytes(65504.0); + expect(result, [0x7b, 0xff]); + _validateFloat16Encoding(65504.0, result); + }); + + test('0.00006103515625 (min positive normal) encodes to 0x0400', () { + final result = toFloat16Bytes(0.00006103515625); + expect(result, [0x04, 0x00]); + _validateFloat16Encoding(0.00006103515625, result); + }); + }); + + group('Normal negative values', () { + test('-1.0 encodes to 0xbc00', () { + final result = toFloat16Bytes(-1.0); + expect(result, [0xbc, 0x00]); + _validateFloat16Encoding(-1.0, result); + }); + + test('-1.5 encodes to 0xbe00', () { + final result = toFloat16Bytes(-1.5); + expect(result, [0xbe, 0x00]); + _validateFloat16Encoding(-1.5, result); + }); + + test('-2.0 encodes to 0xc000', () { + final result = toFloat16Bytes(-2.0); + expect(result, [0xc0, 0x00]); + _validateFloat16Encoding(-2.0, result); + }); + + test('-4.0 encodes to 0xc400', () { + final result = toFloat16Bytes(-4.0); + expect(result, [0xc4, 0x00]); + _validateFloat16Encoding(-4.0, result); + }); + + test('-65504.0 (min normal) encodes to 0xfbff', () { + 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', () { + final result = toFloat16Bytes(5.960464477539063e-8); + expect(result, [0x00, 0x01]); + _validateFloat16Encoding(5.960464477539063e-8, result); + }); + + test('6.097555160522461e-5 (largest subnormal) encodes to 0x03ff', () { + 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', () { + final result = toFloat16Bytes(65536.0); + expect(result, [0x7c, 0x00]); + _validateFloat16Encoding(65536.0, result); + }); + + test('100000.0 overflows to +Infinity', () { + final result = toFloat16Bytes(100000.0); + expect(result, [0x7c, 0x00]); + _validateFloat16Encoding(100000.0, result); + }); + + test('-100000.0 overflows to -Infinity', () { + 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', () { + final result = toFloat16Bytes(1e-10); + expect(result, [0x00, 0x00]); + _validateFloat16Encoding(1e-10, result); + }); + + test('Very small negative value underflows to -0', () { + final result = toFloat16Bytes(-1e-10); + expect(result, [0x80, 0x00]); + _validateFloat16Encoding(-1e-10, result); + }); + }); + + group('RFC 8949 Appendix A test vectors', () { + 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', () { + final result = toFloat16Bytes(5.960464477539063e-8); + expect(result, [0x00, 0x01]); + _validateFloat16Encoding(5.960464477539063e-8, result); + }); + test('0.00006103515625', () { + 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); + }); + }); + }); + + group('Float16 (half-precision) decoding - fromFloat16Bytes', () { + group('Special values', () { + test('0x7e00 decodes to NaN', () { + const bytes = [0x7e, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result.isNaN, true); + _validateFloat16Decoding(bytes, result); + }); + + test('0x7c01 (NaN with payload) decodes to NaN', () { + const bytes = [0x7c, 0x01]; + final result = fromFloat16Bytes(bytes); + expect(result.isNaN, true); + _validateFloat16Decoding(bytes, result); + }); + + test('0x7c00 decodes to +Infinity', () { + const bytes = [0x7c, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, double.infinity); + _validateFloat16Decoding(bytes, result); + }); + + test('0xfc00 decodes to -Infinity', () { + const bytes = [0xfc, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, double.negativeInfinity); + _validateFloat16Decoding(bytes, result); + }); + + test('0x0000 decodes to +0.0', () { + 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', () { + 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', () { + const bytes = [0x3c, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 1.0); + _validateFloat16Decoding(bytes, result); + }); + + test('0x3e00 decodes to 1.5', () { + const bytes = [0x3e, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 1.5); + _validateFloat16Decoding(bytes, result); + }); + + test('0x4000 decodes to 2.0', () { + const bytes = [0x40, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 2.0); + _validateFloat16Decoding(bytes, result); + }); + + test('0x3800 decodes to 0.5', () { + const bytes = [0x38, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, 0.5); + _validateFloat16Decoding(bytes, result); + }); + + test('0x7bff decodes to 65504.0', () { + const bytes = [0x7b, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, 65504.0); + _validateFloat16Decoding(bytes, result); + }); + + test('0x0400 decodes to 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', () { + const bytes = [0xbc, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -1.0); + _validateFloat16Decoding(bytes, result); + }); + + test('0xbe00 decodes to -1.5', () { + const bytes = [0xbe, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -1.5); + _validateFloat16Decoding(bytes, result); + }); + + test('0xc000 decodes to -2.0', () { + const bytes = [0xc0, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -2.0); + _validateFloat16Decoding(bytes, result); + }); + + test('0xc400 decodes to -4.0', () { + const bytes = [0xc4, 0x00]; + final result = fromFloat16Bytes(bytes); + expect(result, -4.0); + _validateFloat16Decoding(bytes, result); + }); + + test('0xfbff decodes to -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', () { + const bytes = [0x00, 0x01]; + final result = fromFloat16Bytes(bytes); + expect(result, 5.960464477539063e-8); + _validateFloat16Decoding(bytes, result); + }); + + test('0x03ff decodes to largest subnormal', () { + const bytes = [0x03, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, 6.097555160522461e-5); + _validateFloat16Decoding(bytes, result); + }); + }); + }); + + 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'); + } + _validateFloat16Encoding(value, bytes); + } + }); + + test('NaN round-trips', () { + final bytes = toFloat16Bytes(double.nan); + expect(fromFloat16Bytes(bytes).isNaN, true); + _validateFloat16Encoding(double.nan, bytes); + }); + }); + + group('isFloat16Lossless', () { + 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); + }); + }); + }); + + 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 + _validateFloat32Encoding(double.nan, bytes); + }); + + test('+Infinity encodes to 0x7f800000', () { + final result = toFloat32Bytes(double.infinity); + expect(result, [0x7f, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(double.infinity, result); + }); + + test('-Infinity encodes to 0xff800000', () { + final result = toFloat32Bytes(double.negativeInfinity); + expect(result, [0xff, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(double.negativeInfinity, result); + }); + + test('+0.0 encodes to 0x00000000', () { + final result = toFloat32Bytes(0.0); + expect(result, [0x00, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(0.0, result); + }); + + test('-0.0 encodes to 0x80000000', () { + final result = toFloat32Bytes(-0.0); + expect(result, [0x80, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(-0.0, result); + }); + }); + + group('Normal values', () { + test('1.0 encodes to 0x3f800000', () { + final result = toFloat32Bytes(1.0); + expect(result, [0x3f, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(1.0, result); + }); + + test('-1.0 encodes to 0xbf800000', () { + final result = toFloat32Bytes(-1.0); + expect(result, [0xbf, 0x80, 0x00, 0x00]); + _validateFloat32Encoding(-1.0, result); + }); + + test('2.0 encodes to 0x40000000', () { + final result = toFloat32Bytes(2.0); + expect(result, [0x40, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(2.0, result); + }); + + test('0.5 encodes to 0x3f000000', () { + final result = toFloat32Bytes(0.5); + expect(result, [0x3f, 0x00, 0x00, 0x00]); + _validateFloat32Encoding(0.5, result); + }); + + test('100000.0 encodes to 0x47c35000', () { + final result = toFloat32Bytes(100000.0); + expect(result, [0x47, 0xc3, 0x50, 0x00]); + _validateFloat32Encoding(100000.0, result); + }); + + test('3.4028234663852886e+38 (max float32)', () { + final result = toFloat32Bytes(3.4028234663852886e+38); + expect(result, [0x7f, 0x7f, 0xff, 0xff]); + _validateFloat32Encoding(3.4028234663852886e+38, result); + }); + }); + }); + + group('Float32 (single-precision) decoding - fromFloat32Bytes', () { + group('Special values', () { + test('0x7fc00000 decodes to NaN', () { + const bytes = [0x7f, 0xc0, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result.isNaN, true); + _validateFloat32Decoding(bytes, result); + }); + + test('0x7f800000 decodes to +Infinity', () { + const bytes = [0x7f, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, double.infinity); + _validateFloat32Decoding(bytes, result); + }); + + test('0xff800000 decodes to -Infinity', () { + const bytes = [0xff, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, double.negativeInfinity); + _validateFloat32Decoding(bytes, result); + }); + + test('0x00000000 decodes to +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', () { + 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', () { + const bytes = [0x3f, 0x80, 0x00, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, 1.0); + _validateFloat32Decoding(bytes, result); + }); + + test('0xbf800000 decodes to -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', () { + const bytes = [0x47, 0xc3, 0x50, 0x00]; + final result = fromFloat32Bytes(bytes); + expect(result, 100000.0); + _validateFloat32Decoding(bytes, result); + }); + + test('0x7f7fffff decodes to max float32', () { + const bytes = [0x7f, 0x7f, 0xff, 0xff]; + final result = fromFloat32Bytes(bytes); + expect(result, closeTo(3.4028234663852886e+38, 1e31)); + _validateFloat32Decoding(bytes, result); + }); + }); + }); + + 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', + ); + } + _validateFloat32Encoding(value, bytes); + } + }); + }); + + 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); + }); + }); + + group('Float64 (double-precision) encoding - toFloat64Bytes', () { + group('Special values', () { + test('NaN encodes correctly', () { + final bytes = toFloat64Bytes(double.nan); + expect(bytes.length, 8); + _validateFloat64Encoding(double.nan, bytes); + }); + + test('+Infinity encodes to 0x7ff0000000000000', () { + final result = toFloat64Bytes(double.infinity); + expect(result, [0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(double.infinity, result); + }); + + test('-Infinity encodes to 0xfff0000000000000', () { + 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', () { + 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', () { + 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', () { + 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', () { + final result = toFloat64Bytes(-1.0); + expect(result, [0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + _validateFloat64Encoding(-1.0, result); + }); + + test('1e300 encodes correctly', () { + final result = toFloat64Bytes(1e300); + expect(result, [0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]); + _validateFloat64Encoding(1e300, result); + }); + }); + }); + + group('Float64 (double-precision) decoding - fromFloat64Bytes', () { + group('Special values', () { + test('NaN decodes correctly', () { + 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', () { + 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', () { + 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', () { + 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', () { + 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', () { + 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', () { + 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', () { + const bytes = [0x7e, 0x37, 0xe4, 0x3c, 0x88, 0x00, 0x75, 0x9c]; + final result = fromFloat64Bytes(bytes); + expect(result, 1e300); + _validateFloat64Decoding(bytes, result); + }); + }); + }); + + 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'); + } + _validateFloat64Encoding(value, bytes); + } + }); + }); + + 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 + final encoded = toFloat16Bytes(-4.0); + expect(encoded, [0xc4, 0x00]); + expect(fromFloat16Bytes([0xc4, 0x00]), -4.0); + _validateFloat16Encoding(-4.0, encoded); + }); + + 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); + _validateFloat16Encoding(-0.0, bytes); + }); + + test('Large positive values with high bit set decode correctly', () { + // Values like 65504 have high bit set in some representations + 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 + const bytes = [0xfb, 0xff]; + final result = fromFloat16Bytes(bytes); + expect(result, -65504.0); + _validateFloat16Decoding(bytes, result); + }); + }); + + 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); + }); + }); +} diff --git a/test/json_encoder_test.dart b/test/json_encoder_test.dart index cccfd6c..6323191 100644 --- a/test/json_encoder_test.dart +++ b/test/json_encoder_test.dart @@ -10,12 +10,17 @@ 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)); } void main() { - const bool kIsWeb = bool.fromEnvironment('dart.library.js_interop'); group('RFC Appendix A Diagnostics encoder tests -> ', () { test('0', () { expect(encode(0), '0'); @@ -58,36 +63,68 @@ void main() { }); test('0.0', () { - expect(encode(0.0), '0.0'); - }, skip: kIsWeb); + 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', () { expect(encode(-0.0), '-0.0'); }); test('1.0', () { - expect(encode(1.0), '1.0'); - }, skip: kIsWeb); + 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', () { expect(encode(1.5), '1.5'); }); test('65504.0', () { - expect(encode(65504.0), '65504.0'); - }, skip: kIsWeb); + 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'); - }, skip: kIsWeb); + 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', () { 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 +136,7 @@ void main() { test('Infinity', () { expect(encode(double.infinity), 'null'); - }, skip: kIsWeb); + }); test('Nan', () { expect(encode(double.nan), 'null'); @@ -107,7 +144,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..ada75de 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]); - } + // On web, returns List instead of Uint64List + 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..6374568 100644 --- a/test/simple_encoder_rfc_test.dart +++ b/test/simple_encoder_rfc_test.dart @@ -7,11 +7,17 @@ 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() { - 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 +76,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 +135,127 @@ void main() { test('0.0', () { final encoded = cbor.encode(0.0); - if (kIsWeb) { + 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); - if (kIsWeb) { - expect(encoded, [0xff]); + 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); - if (kIsWeb) { + 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); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x3e, 0x00]); - } + // 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); - if (kIsWeb) { + 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); - if (kIsWeb) { + 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); - 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]); + 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); - 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]); + 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); - if (kIsWeb) { - expect(encoded, [0xf9, 0x00, 0x00]); - } else { - expect(encoded, [0xf9, 0x00, 0x01]); - } + // This small fractional value is always encoded as float + 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]); - } + // This small fractional value is always encoded as float + expect(encoded, [0xf9, 0x04, 0x00]); }); test('-4.0', () { final encoded = cbor.encode(-4.0); - if (kIsWeb) { + 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', () { @@ -242,7 +266,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 +276,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;