Skip to content
Merged

Pr84 #85

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions lib/src/decoder/pretty_print.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -140,17 +139,17 @@ class _PrettyPrint implements Sink<RawValue> {
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:
Expand Down
8 changes: 4 additions & 4 deletions lib/src/decoder/stage3.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions lib/src/simple.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ class CborSimpleDecoder extends Converter<List<int>, 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<Object?, List<int>> {
final bool _encodeDateTimeEpoch;
final bool _decodeBase64;
Expand Down
14 changes: 9 additions & 5 deletions lib/src/utils/arg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
225 changes: 225 additions & 0 deletions lib/src/utils/float_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Package : Cbor
* Author : S. Hamblett <steve.hamblett@linux.com>
* 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<int> 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<int> 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<int> 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<int> 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<int> 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<int> bytes) {
final data = ByteData.view(Uint8List.fromList(bytes).buffer);
return data.getFloat64(0, Endian.big);
}
Loading