=> CborHelpers.Int32BitsToSingle((int)(((sign ? 1U : 0U) << FloatSignShift) + ((uint)exp << FloatExponentShift) + sig));
}
+ public static bool HalfIsNaN(ushort value)
+ {
+ return (value & ~((ushort)1 << HalfSignShift)) > HalfPositiveInfinityBits;
+ }
+
private static (int Exp, uint Sig) NormSubnormalF16Sig(uint sig)
{
int shiftDist = LeadingZeroCount(sig) - 16 - 5;
{
public partial class CborWriter
{
+ // CBOR RFC 8949 says: if NaN is an allowed value, and there is no intent to support NaN payloads or signaling NaNs, the protocol needs to pick a single representation, typically 0xf97e00. If that simple choice is not possible, specific attention will be needed for NaN handling.
+ // In this implementation "that simple choice is not possible" for CTAP2 mode (RequiresPreservingFloatPrecision), in which "representations of any floating-point values are not changed".
+ private const ushort PositiveQNaNBitsHalf = 0x7e00;
+
// Implements major type 7 encoding per https://tools.ietf.org/html/rfc7049#section-2.1
/// <summary>Writes a single-precision floating point number (major type 7).</summary>
private static bool TryConvertDoubleToSingle(double value, out float result)
{
result = (float)value;
- return BitConverter.DoubleToInt64Bits(result) == BitConverter.DoubleToInt64Bits(value);
+ return double.IsNaN(value) || BitConverter.DoubleToInt64Bits(result) == BitConverter.DoubleToInt64Bits(value);
}
}
}
{
EnsureWriteCapacity(1 + sizeof(short));
WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional16BitData));
- BinaryPrimitives.WriteHalfBigEndian(_buffer.AsSpan(_offset), value);
+ if (Half.IsNaN(value) && !CborConformanceModeHelpers.RequiresPreservingFloatPrecision(ConformanceMode))
+ {
+ BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_offset), PositiveQNaNBitsHalf);
+ }
+ else
+ {
+ BinaryPrimitives.WriteHalfBigEndian(_buffer.AsSpan(_offset), value);
+ }
_offset += sizeof(short);
AdvanceDataItemCounters();
}
internal static bool TryConvertSingleToHalf(float value, out Half result)
{
result = (Half)value;
- return BitConverter.SingleToInt32Bits((float)result) == BitConverter.SingleToInt32Bits(value);
+ return float.IsNaN(value) || BitConverter.SingleToInt32Bits((float)result) == BitConverter.SingleToInt32Bits(value);
}
}
}
{
EnsureWriteCapacity(1 + sizeof(ushort));
WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional16BitData));
- CborHelpers.WriteHalfBigEndian(_buffer.AsSpan(_offset), value);
+ if (HalfHelpers.HalfIsNaN(value) && !CborConformanceModeHelpers.RequiresPreservingFloatPrecision(ConformanceMode))
+ {
+ BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_offset), PositiveQNaNBitsHalf);
+ }
+ else
+ {
+ CborHelpers.WriteHalfBigEndian(_buffer.AsSpan(_offset), value);
+ }
_offset += sizeof(ushort);
AdvanceDataItemCounters();
}
internal static bool TryConvertSingleToHalf(float value, out ushort result)
{
result = HalfHelpers.FloatToHalf(value);
- return CborHelpers.SingleToInt32Bits(HalfHelpers.HalfToFloat(result)) == CborHelpers.SingleToInt32Bits(value);
+ return float.IsNaN(value) || CborHelpers.SingleToInt32Bits(HalfHelpers.HalfToFloat(result)) == CborHelpers.SingleToInt32Bits(value);
}
}
}
internal static class CborTestHelpers
{
public static readonly DateTimeOffset UnixEpoch = DateTimeOffset.UnixEpoch;
+
+ public static int SingleToInt32Bits(float value)
+ => BitConverter.SingleToInt32Bits(value);
}
}
{
private const long UnixEpochTicks = 719162L /*Number of days from 1/1/0001 to 12/31/1969*/ * 10000 * 1000 * 60 * 60 * 24; /* Ticks per day.*/
public static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(UnixEpochTicks, TimeSpan.Zero);
+
+ public static unsafe int SingleToInt32Bits(float value)
+ => *((int*)&value);
}
}
[InlineData(float.PositiveInfinity, "fa7f800000")]
[InlineData(float.NegativeInfinity, "faff800000")]
[InlineData(float.NaN, "fa7fc00000")]
+ [InlineData(float.NaN, "faffc00000")]
public static void ReadSingle_SingleValue_HappyPath(float expectedResult, string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
[InlineData(double.PositiveInfinity, "fb7ff0000000000000")]
[InlineData(double.NegativeInfinity, "fbfff0000000000000")]
[InlineData(double.NaN, "fb7ff8000000000000")]
+ [InlineData(double.NaN, "fbfff8000000000000")]
public static void ReadDouble_SingleValue_HappyPath(double expectedResult, string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
[InlineData(double.PositiveInfinity, "fa7f800000")]
[InlineData(double.NegativeInfinity, "faff800000")]
[InlineData(double.NaN, "fa7fc00000")]
+ [InlineData(double.NaN, "faffc00000")]
public static void ReadDouble_SinglePrecisionValue_ShouldCoerceToDouble(double expectedResult, string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
[InlineData(-4.0, "f9c400")]
[InlineData(double.PositiveInfinity, "f97c00")]
[InlineData(double.NaN, "f97e00")]
+ [InlineData(double.NaN, "f9fe00")]
[InlineData(double.NegativeInfinity, "f9fc00")]
public static void ReadDouble_HalfPrecisionValue_ShouldCoerceToDouble(double expectedResult, string hexEncoding)
{
[InlineData(-4.0, "f9c400")]
[InlineData(float.PositiveInfinity, "f97c00")]
[InlineData(float.NaN, "f97e00")]
+ [InlineData(float.NaN, "f9fe00")]
[InlineData(float.NegativeInfinity, "f9fc00")]
public static void ReadSingle_HalfPrecisionValue_ShouldCoerceToSingle(float expectedResult, string hexEncoding)
{
<NoWarn>$(NoWarn);CS8002</NoWarn>
<!-- FSharp.Core: Could not find embedded resource 'FSharpOptimizationCompressedData.FSharp.Core' to remove in assembly 'FSharp.Core'. See https://github.com/dotnet/fsharp/pull/14395 -->
<NoWarn>$(NoWarn);IL2040</NoWarn>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(CommonTestPath)System\Security\Cryptography\ByteUtils.cs">
[InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "98190102030405060708090a0b0c0d0e0f101112131415161718181819")]
[InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "840120604107")]
[InlineData(new object[] { "lorem", "ipsum", "dolor" }, "83656c6f72656d65697073756d65646f6c6f72")]
- [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6f9fe00f97c00")]
+ [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6f97e00f97c00")]
public static void WriteArray_SimpleValues_HappyPath(object[] values, string expectedHexEncoding)
{
byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
[InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff")]
[InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "9f0120604107ff")]
[InlineData(new object[] { "lorem", "ipsum", "dolor" }, "9f656c6f72656d65697073756d65646f6c6f72ff")]
- [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "9ff4f6f9fe00f97c00ff")]
+ [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "9ff4f6f97e00f97c00ff")]
public static void WriteArray_IndefiniteLength_NoPatching_HappyPath(object[] values, string expectedHexEncoding)
{
byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
[InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "98190102030405060708090a0b0c0d0e0f101112131415161718181819")]
[InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "840120604107")]
[InlineData(new object[] { "lorem", "ipsum", "dolor" }, "83656c6f72656d65697073756d65646f6c6f72")]
- [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6f9fe00f97c00")]
+ [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6f97e00f97c00")]
public static void WriteArray_IndefiniteLength_WithPatching_HappyPath(object[] values, string expectedHexEncoding)
{
byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
[InlineData(3.4028234663852886e+38, "fa7f7fffff")]
[InlineData(float.PositiveInfinity, "f97c00")]
[InlineData(float.NegativeInfinity, "f9fc00")]
- [InlineData(float.NaN, "f9fe00")]
+ [InlineData(float.NaN, "f97e00")]
public static void WriteSingle_SingleValue_HappyPath(float input, string hexExpectedEncoding)
{
byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
}
[Theory]
- [InlineData(float.NaN, "f9fe00", CborConformanceMode.Lax)]
- [InlineData(float.NaN, "f9fe00", CborConformanceMode.Strict)]
- [InlineData(float.NaN, "f9fe00", CborConformanceMode.Canonical)]
+ [InlineData(float.NaN, "f97e00", CborConformanceMode.Lax)]
+ [InlineData(float.NaN, "f97e00", CborConformanceMode.Strict)]
+ [InlineData(float.NaN, "f97e00", CborConformanceMode.Canonical)]
public static void WriteSingle_NonCtapConformance_ShouldMinimizePrecision(float input, string hexExpectedEncoding, CborConformanceMode mode)
{
byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
[InlineData(3.4028234663852886e+38, "fa7f7fffff")]
[InlineData(float.PositiveInfinity, "fa7f800000")]
[InlineData(float.NegativeInfinity, "faff800000")]
- [InlineData(float.NaN, "faffc00000")]
public static void WriteSingle_Ctap2Conformance_ShouldPreservePrecision(float input, string hexExpectedEncoding)
{
byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
AssertHelper.HexEqual(expectedEncoding, writer.Encode());
}
+ [Fact]
+ public static void WriteSingle_Ctap2Conformance_ShouldPreservePrecision_NaN()
+ {
+ // float.NaN may differ across architectures, in particular it's negative on x86 and positive elsewhere
+ byte[] expectedEncoding = ("fa" + CborTestHelpers.SingleToInt32Bits(float.NaN).ToString("x4")).HexToByteArray();
+ var writer = new CborWriter(CborConformanceMode.Ctap2Canonical);
+ writer.WriteSingle(float.NaN);
+ AssertHelper.HexEqual(expectedEncoding, writer.Encode());
+ }
+
[Theory]
[InlineData(1.1, "fb3ff199999999999a")]
[InlineData(1.0e+300, "fb7e37e43c8800759c")]
[InlineData(3.1415926, "fb400921fb4d12d84a")]
[InlineData(double.PositiveInfinity, "f97c00")]
[InlineData(double.NegativeInfinity, "f9fc00")]
- [InlineData(double.NaN, "f9fe00")]
+ [InlineData(double.NaN, "f97e00")]
public static void WriteDouble_SingleValue_HappyPath(double input, string hexExpectedEncoding)
{
byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
}
[Theory]
- [InlineData(double.NaN, "f9fe00", CborConformanceMode.Lax)]
- [InlineData(double.NaN, "f9fe00", CborConformanceMode.Strict)]
- [InlineData(double.NaN, "f9fe00", CborConformanceMode.Canonical)]
+ [InlineData(double.NaN, "f97e00", CborConformanceMode.Lax)]
+ [InlineData(double.NaN, "f97e00", CborConformanceMode.Strict)]
+ [InlineData(double.NaN, "f97e00", CborConformanceMode.Canonical)]
[InlineData(65505, "fa477fe100", CborConformanceMode.Lax)]
[InlineData(65505, "fa477fe100", CborConformanceMode.Strict)]
[InlineData(65505, "fa477fe100", CborConformanceMode.Canonical)]
[InlineData(3.1415926, "fb400921fb4d12d84a")]
[InlineData(double.PositiveInfinity, "fb7ff0000000000000")]
[InlineData(double.NegativeInfinity, "fbfff0000000000000")]
- [InlineData(double.NaN, "fbfff8000000000000")]
public static void WriteDouble_Ctap2Conformance_ShouldPreservePrecision(double input, string hexExpectedEncoding)
{
byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
}
[Fact]
+ public static void WriteDouble_Ctap2Conformance_ShouldPreservePrecision_NaN()
+ {
+ // double.NaN may differ across architectures, in particular it's negative on x86 and positive elsewhere
+ byte[] expectedEncoding = ("fb" + BitConverter.DoubleToInt64Bits(double.NaN).ToString("x8")).HexToByteArray();
+ var writer = new CborWriter(CborConformanceMode.Ctap2Canonical);
+ writer.WriteDouble(double.NaN);
+ AssertHelper.HexEqual(expectedEncoding, writer.Encode());
+ }
+
+ [Fact]
public static void WriteNull_SingleValue_HappyPath()
{
byte[] expectedEncoding = "f6".HexToByteArray();
[InlineData(0.00006103515625, "f90400")]
[InlineData(-4.0, "f9c400")]
[InlineData(float.PositiveInfinity, "f97c00")]
- [InlineData(float.NaN, "f9fe00")]
+ [InlineData(float.NaN, "f97e00")]
[InlineData(float.NegativeInfinity, "f9fc00")]
public static void WriteHalf_SingleValue_HappyPath(float input, string hexExpectedEncoding)
{