mirror of
https://github.com/claunia/plist-cil.git
synced 2025-12-16 11:04:26 +00:00
Merge pull request #56 from quamotion/features/bigint
NSNumber: support some exotic formats
This commit is contained in:
@@ -16,9 +16,16 @@ namespace plistcil.test
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] {0x57}, 0x57)]
|
||||
[InlineData(new byte[] {0x12, 0x34}, 0x1234)]
|
||||
[InlineData(new byte[] {0x12, 0x34, 0x56}, 0x123456)]
|
||||
[InlineData(new byte[] {0x40, 0x2d, 0xf8, 0x4d}, 0x402df84d)]
|
||||
[InlineData(new byte[] {0x12, 0x34, 0x56, 0x78, 0x9a }, 0x123456789a)]
|
||||
[InlineData(new byte[] {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc }, 0x123456789abc)]
|
||||
[InlineData(new byte[] {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde }, 0x123456789abcde)]
|
||||
[InlineData(new byte[] {0x41, 0xb4, 0x83, 0x98, 0x2a, 0x00, 0x00, 0x00}, 0x41b483982a000000)]
|
||||
[InlineData(new byte[] {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x19}, unchecked((long)0xfffffffffffffc19))]
|
||||
[InlineData(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x19 },
|
||||
unchecked((long)0xfffffffffffffc19))]
|
||||
public void ParseLongTest(byte[] binaryValue, long expectedValue)
|
||||
{
|
||||
Assert.Equal(expectedValue, BinaryPropertyListParser.ParseLong(binaryValue));
|
||||
|
||||
@@ -1,10 +1,95 @@
|
||||
using Claunia.PropertyList;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace plistcil.test
|
||||
{
|
||||
public class NSNumberTests
|
||||
{
|
||||
public static IEnumerable<object[]> SpanConstructorTestData()
|
||||
{
|
||||
return new List<object[]>
|
||||
{
|
||||
// INTEGER values
|
||||
// 0
|
||||
new object[] { new byte[] { 0x00 }, NSNumber.INTEGER, false, 0, 0.0 },
|
||||
|
||||
// 1-byte value < sbyte.maxValue
|
||||
new object[] { new byte[] { 0x10 }, NSNumber.INTEGER, true, 16, 16.0 },
|
||||
|
||||
// 1-byte value > sbyte.MaxValue
|
||||
new object[] { new byte[] { 0xFF }, NSNumber.INTEGER, true, byte.MaxValue, (double)byte.MaxValue},
|
||||
|
||||
// 2-byte value < short.maxValue
|
||||
new object[] { new byte[] { 0x10, 0x00 }, NSNumber.INTEGER, true, 4096, 4096.0 },
|
||||
|
||||
// 2-byte value > short.maxValue
|
||||
new object[] { new byte[] { 0xFF, 0xFF }, NSNumber.INTEGER, true, ushort.MaxValue, (double)ushort.MaxValue},
|
||||
|
||||
// 4-byte value < int.maxValue
|
||||
new object[] { new byte[] { 0x10, 0x00, 0x00, 0x00 }, NSNumber.INTEGER, true, 0x10000000, 1.0 * 0x10000000 },
|
||||
|
||||
// 4-bit value > int.MaxValue
|
||||
new object[] { new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }, NSNumber.INTEGER, true, uint.MaxValue, (double)uint.MaxValue },
|
||||
|
||||
// 64-bit value < long.MaxValue
|
||||
new object[] { new byte[] { 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, NSNumber.INTEGER, true, 0x1000000000000000, 1.0 * 0x1000000000000000 },
|
||||
|
||||
// 64-bit value > long.MaxValue
|
||||
new object[] { new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, NSNumber.INTEGER, true, -1, -1.0 },
|
||||
|
||||
// 128-bit positive value
|
||||
new object[] { new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x00 }, NSNumber.INTEGER, true, unchecked((long)0xffffffffffffa000), 1.0 * unchecked((long)0xffffffffffffa000) },
|
||||
|
||||
// 128-bit negative value
|
||||
new object[] { new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, NSNumber.INTEGER, true, -1, -1.0 },
|
||||
|
||||
// REAL values
|
||||
// 4-byte value (float)
|
||||
new object[] { new byte[] { 0x00, 0x00, 0x00, 0x00 }, NSNumber.REAL, false, 0, 0.0 },
|
||||
|
||||
new object[] { new byte[] { 0x41, 0x20, 0x00, 0x00 }, NSNumber.REAL, true, 10, 10.0 },
|
||||
|
||||
new object[] { new byte[] { 0x3d, 0xcc, 0xcc, 0xcd }, NSNumber.REAL, false, 0, 0.1 },
|
||||
|
||||
// 8-byte value (double)
|
||||
new object[] { new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, NSNumber.REAL, false, 0, 0.0 },
|
||||
|
||||
new object[] { new byte[] { 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, NSNumber.REAL, true, 10, 10.0 },
|
||||
|
||||
new object[] { new byte[] { 0x3f, 0xb9, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a }, NSNumber.REAL, false, 0, 0.1 }
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SpanConstructorTestData))]
|
||||
public void SpanConstructorTest(byte[] data, int type, bool boolValue, long longValue, double doubleValue)
|
||||
{
|
||||
NSNumber number = new NSNumber((Span<byte>)data, type);
|
||||
Assert.Equal(boolValue, number.ToBool());
|
||||
Assert.Equal(longValue, number.ToLong());
|
||||
Assert.Equal(doubleValue, number.ToDouble(), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanConstructorInvalidValuesTest()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new NSNumber((Span<byte>)null, NSNumber.INTEGER));
|
||||
Assert.Throws<ArgumentNullException>(() => new NSNumber((Span<byte>)null, NSNumber.REAL));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new NSNumber((Span<byte>)Array.Empty<byte>(), NSNumber.INTEGER));
|
||||
Assert.Throws<ArgumentException>(() => new NSNumber((Span<byte>)Array.Empty<byte>(), NSNumber.REAL));
|
||||
Assert.Throws<ArgumentException>(() => new NSNumber((Span<byte>)Array.Empty<byte>(), 9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringAndTypeConstructorInvalidValuesTest()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new NSNumber((string)null, NSNumber.INTEGER));
|
||||
Assert.Throws<ArgumentNullException>(() => new NSNumber((string)null, NSNumber.REAL));
|
||||
Assert.Throws<ArgumentException>(() => new NSNumber("0", 9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void NSNumberConstructorTest()
|
||||
{
|
||||
@@ -71,6 +156,159 @@ namespace plistcil.test
|
||||
Assert.True(number.isReal());
|
||||
Assert.Equal(7200d, number.ToDouble());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
public static IEnumerable<object[]> StringConstructorTestData()
|
||||
{
|
||||
return new List<object[]>
|
||||
{
|
||||
// Long values, formatted as hexadecimal values
|
||||
new object[] { "0x00", false, 0, 0.0 },
|
||||
new object[] { "0x1000", true, 0x1000, 1.0 * 0x1000 },
|
||||
new object[] { "0x00001000", true, 0x1000, 1.0 * 0x1000 },
|
||||
new object[] { "0x0000000000001000", true, 0x1000, 1.0 * 0x1000 },
|
||||
|
||||
// Long values, formatted as decimal values
|
||||
new object[] { "0", false, 0, 0.0 },
|
||||
new object[] { "10", true, 10, 10.0 },
|
||||
|
||||
// Decimal values
|
||||
new object[] { "0.0", false, 0, 0.0 },
|
||||
new object[] { "0.10", false, 0, 0.1 },
|
||||
new object[] { "3.14", true, 3, 3.14 },
|
||||
|
||||
// Boolean values
|
||||
new object[] { "yes", true, 1, 1},
|
||||
new object[] { "true", true, 1, 1},
|
||||
new object[] { "Yes", true, 1, 1},
|
||||
new object[] { "True", true, 1, 1},
|
||||
new object[] { "YES", true, 1, 1},
|
||||
new object[] { "TRUE", true, 1, 1},
|
||||
|
||||
new object[] { "no", false, 0, 0},
|
||||
new object[] { "false", false, 0, 0},
|
||||
new object[] { "No", false, 0, 0},
|
||||
new object[] { "False", false, 0, 0},
|
||||
new object[] { "NO", false, 0, 0},
|
||||
new object[] { "FALSE", false, 0, 0},
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(StringConstructorTestData))]
|
||||
public void StringConstructorTest(string value, bool boolValue, long longValue, double doubleValue)
|
||||
{
|
||||
NSNumber number = new NSNumber(value);
|
||||
Assert.Equal(boolValue, number.ToBool());
|
||||
Assert.Equal(longValue, number.ToLong());
|
||||
Assert.Equal(doubleValue, number.ToDouble(), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StringConstructorInvalidValuesTest()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new NSNumber(null));
|
||||
Assert.Throws<ArgumentException>(() => new NSNumber("plist"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> Int32ConstructorTestData()
|
||||
{
|
||||
return new List<object[]>
|
||||
{
|
||||
// Long values, formatted as hexadecimal values
|
||||
new object[] { 0, false, 0, 0.0 },
|
||||
new object[] { 1, true, 1, 1.0 },
|
||||
new object[] { -1, true, -1, -1.0 },
|
||||
new object[] { int.MaxValue, true, int.MaxValue, int.MaxValue },
|
||||
new object[] { int.MinValue, true, int.MinValue, int.MinValue },
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Int32ConstructorTestData))]
|
||||
public void Int32ConstructorTest(int value, bool boolValue, long longValue, double doubleValue)
|
||||
{
|
||||
NSNumber number = new NSNumber(value);
|
||||
Assert.Equal(boolValue, number.ToBool());
|
||||
Assert.Equal(longValue, number.ToLong());
|
||||
Assert.Equal(doubleValue, number.ToDouble(), 5);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> Int64ConstructorTestData()
|
||||
{
|
||||
return new List<object[]>
|
||||
{
|
||||
// Long values, formatted as hexadecimal values
|
||||
new object[] { 0, false, 0, 0.0 },
|
||||
new object[] { 1, true, 1, 1.0 },
|
||||
new object[] { -1, true, -1, -1.0 },
|
||||
new object[] { long.MaxValue, true, long.MaxValue, long.MaxValue },
|
||||
new object[] { long.MinValue, true, long.MinValue, long.MinValue },
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Int64ConstructorTestData))]
|
||||
public void Int64ConstructorTest(long value, bool boolValue, long longValue, double doubleValue)
|
||||
{
|
||||
NSNumber number = new NSNumber(value);
|
||||
Assert.Equal(boolValue, number.ToBool());
|
||||
Assert.Equal(longValue, number.ToLong());
|
||||
Assert.Equal(doubleValue, number.ToDouble(), 5);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> DoubleConstructorTestData()
|
||||
{
|
||||
return new List<object[]>
|
||||
{
|
||||
// Long values, formatted as hexadecimal values
|
||||
new object[] { 0.0, false, 0, 0.0 },
|
||||
new object[] { 1.0, true, 1, 1.0 },
|
||||
new object[] { -1.0, true, -1, -1.0 },
|
||||
new object[] { double.Epsilon, false, 0, double.Epsilon },
|
||||
new object[] { double.MaxValue, true, long.MinValue /* Overflow! */, double.MaxValue },
|
||||
new object[] { double.MinValue, true, long.MinValue, double.MinValue },
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DoubleConstructorTestData))]
|
||||
public void DoubleConstructorTest(double value, bool boolValue, long longValue, double doubleValue)
|
||||
{
|
||||
NSNumber number = new NSNumber(value);
|
||||
Assert.Equal(boolValue, number.ToBool());
|
||||
Assert.Equal(longValue, number.ToLong());
|
||||
Assert.Equal(doubleValue, number.ToDouble(), 5);
|
||||
}
|
||||
public static IEnumerable<object[]> BoolConstructorTestData()
|
||||
{
|
||||
return new List<object[]>
|
||||
{
|
||||
// Long values, formatted as hexadecimal values
|
||||
new object[] { false, false, 0, 0.0 },
|
||||
new object[] { true, true, 1, 1.0 },
|
||||
};
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(BoolConstructorTestData))]
|
||||
public void BoolConstructorTest(bool value, bool boolValue, long longValue, double doubleValue)
|
||||
{
|
||||
NSNumber number = new NSNumber(value);
|
||||
Assert.Equal(boolValue, number.ToBool());
|
||||
Assert.Equal(longValue, number.ToLong());
|
||||
Assert.Equal(doubleValue, number.ToDouble(), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualTest()
|
||||
{
|
||||
NSNumber a = new NSNumber(2);
|
||||
NSNumber b = new NSNumber(2);
|
||||
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
Assert.True(a.Equals(b));
|
||||
Assert.True(b.Equals(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,7 +487,7 @@ namespace Claunia.PropertyList
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an unsigned integers from a span.
|
||||
/// Parses an unsigned integer from a span.
|
||||
/// </summary>
|
||||
/// <returns>The byte array containing the unsigned integer.</returns>
|
||||
/// <param name="bytes">The unsigned integer represented by the given bytes.</param>
|
||||
@@ -515,22 +515,56 @@ namespace Claunia.PropertyList
|
||||
/// <param name="bytes">The bytes representing the long integer.</param>
|
||||
public static long ParseLong(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if(bytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bytes));
|
||||
}
|
||||
|
||||
if(bytes.Length == 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes));
|
||||
}
|
||||
|
||||
// https://opensource.apple.com/source/CF/CF-1153.18/CFBinaryPList.c,
|
||||
// __CFBinaryPlistCreateObjectFiltered, case kCFBinaryPlistMarkerInt:
|
||||
//
|
||||
// in format version '00', 1, 2, and 4-byte integers have to be interpreted as unsigned,
|
||||
// whereas 8-byte integers are signed (and 16-byte when available)
|
||||
// negative 1, 2, 4-byte integers are always emitted as 8 bytes in format '00'
|
||||
// integers are not required to be in the most compact possible representation,
|
||||
// but only the last 64 bits are significant currently
|
||||
switch(bytes.Length)
|
||||
{
|
||||
case 1: return bytes[0];
|
||||
|
||||
case 2: return BinaryPrimitives.ReadUInt16BigEndian(bytes);
|
||||
|
||||
case 3: throw new NotSupportedException();
|
||||
|
||||
case 4: return BinaryPrimitives.ReadUInt32BigEndian(bytes);
|
||||
|
||||
case 8: return (long)BinaryPrimitives.ReadUInt64BigEndian(bytes);
|
||||
// Transition from unsigned to signed
|
||||
case 8: return BinaryPrimitives.ReadInt64BigEndian(bytes);
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes),
|
||||
$"Cannot read a byte span of length {bytes.Length}");
|
||||
// Only the last 64 bits are significant currently
|
||||
case 16: return BinaryPrimitives.ReadInt64BigEndian(bytes.Slice(8));
|
||||
}
|
||||
|
||||
if (bytes.Length < 8)
|
||||
{
|
||||
// Compatability with existing archives, including anything with a non-power-of-2
|
||||
// size and 16-byte values, and architectures that don't support unaligned access
|
||||
long value = 0;
|
||||
for(int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
value = (value << 8) + bytes[i];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Theoretically we could handle non-power-of-2 byte arrays larger than 8, with the code
|
||||
// above, and it appears the reference implementation does exactly that. But it seems to
|
||||
// be an extreme edge case.
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes),
|
||||
$"Cannot read a byte span of length {bytes.Length}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -540,6 +574,11 @@ namespace Claunia.PropertyList
|
||||
/// <param name="bytes">The bytes representing the double.</param>
|
||||
public static double ParseDouble(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if(bytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(bytes));
|
||||
}
|
||||
|
||||
if(bytes.Length == 8) return BitConverter.Int64BitsToDouble(ParseLong(bytes));
|
||||
if(bytes.Length == 4) return BitConverter.ToSingle(BitConverter.GetBytes(ParseLong(bytes)), 0);
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace Claunia.PropertyList
|
||||
longValue = (long)Math.Round(doubleValue);
|
||||
break;
|
||||
|
||||
default: throw new ArgumentException("Type argument is not valid.");
|
||||
default: throw new ArgumentException("Type argument is not valid.", nameof(type));
|
||||
}
|
||||
|
||||
this.type = type;
|
||||
@@ -124,13 +124,12 @@ namespace Claunia.PropertyList
|
||||
long l;
|
||||
double d;
|
||||
|
||||
if(text.StartsWith("0x") && long.TryParse("", NumberStyles.HexNumber, CultureInfo.InvariantCulture, out l))
|
||||
if(text.StartsWith("0x") && long.TryParse(text.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out l))
|
||||
{
|
||||
doubleValue = longValue = l;
|
||||
type = INTEGER;
|
||||
}
|
||||
|
||||
if(long.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out l))
|
||||
else if(long.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out l))
|
||||
{
|
||||
doubleValue = longValue = l;
|
||||
type = INTEGER;
|
||||
@@ -151,6 +150,7 @@ namespace Claunia.PropertyList
|
||||
if(isTrue || isFalse)
|
||||
{
|
||||
type = BOOLEAN;
|
||||
boolValue = isTrue;
|
||||
doubleValue = longValue = boolValue ? 1 : 0;
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user