diff --git a/plist-cil.test/BinaryPropertyListParserTests.cs b/plist-cil.test/BinaryPropertyListParserTests.cs index daafbbf..01f3fb6 100644 --- a/plist-cil.test/BinaryPropertyListParserTests.cs +++ b/plist-cil.test/BinaryPropertyListParserTests.cs @@ -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)); diff --git a/plist-cil.test/NSNumberTests.cs b/plist-cil.test/NSNumberTests.cs index 28ba3f3..9075a3f 100644 --- a/plist-cil.test/NSNumberTests.cs +++ b/plist-cil.test/NSNumberTests.cs @@ -1,10 +1,95 @@ using Claunia.PropertyList; +using System; +using System.Collections.Generic; using Xunit; namespace plistcil.test { public class NSNumberTests { + public static IEnumerable SpanConstructorTestData() + { + return new List + { + // 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)data, type); + Assert.Equal(boolValue, number.ToBool()); + Assert.Equal(longValue, number.ToLong()); + Assert.Equal(doubleValue, number.ToDouble(), 5); + } + + [Fact] + public void SpanConstructorInvalidValuesTest() + { + Assert.Throws(() => new NSNumber((Span)null, NSNumber.INTEGER)); + Assert.Throws(() => new NSNumber((Span)null, NSNumber.REAL)); + Assert.Throws(() => new NSNumber((Span)Array.Empty(), NSNumber.INTEGER)); + Assert.Throws(() => new NSNumber((Span)Array.Empty(), NSNumber.REAL)); + Assert.Throws(() => new NSNumber((Span)Array.Empty(), 9)); + } + + [Fact] + public void StringAndTypeConstructorInvalidValuesTest() + { + Assert.Throws(() => new NSNumber((string)null, NSNumber.INTEGER)); + Assert.Throws(() => new NSNumber((string)null, NSNumber.REAL)); + Assert.Throws(() => 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 StringConstructorTestData() + { + return new List + { + // 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(() => new NSNumber(null)); + Assert.Throws(() => new NSNumber("plist")); + } + + public static IEnumerable Int32ConstructorTestData() + { + return new List + { + // 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 Int64ConstructorTestData() + { + return new List + { + // 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 DoubleConstructorTestData() + { + return new List + { + // 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 BoolConstructorTestData() + { + return new List + { + // 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)); + } } } \ No newline at end of file diff --git a/plist-cil/BinaryPropertyListParser.cs b/plist-cil/BinaryPropertyListParser.cs index 4793d46..d53d536 100644 --- a/plist-cil/BinaryPropertyListParser.cs +++ b/plist-cil/BinaryPropertyListParser.cs @@ -487,7 +487,7 @@ namespace Claunia.PropertyList } /// - /// Parses an unsigned integers from a span. + /// Parses an unsigned integer from a span. /// /// The byte array containing the unsigned integer. /// The unsigned integer represented by the given bytes. @@ -515,22 +515,56 @@ namespace Claunia.PropertyList /// The bytes representing the long integer. public static long ParseLong(ReadOnlySpan 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}"); } /// @@ -540,6 +574,11 @@ namespace Claunia.PropertyList /// The bytes representing the double. public static double ParseDouble(ReadOnlySpan 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); diff --git a/plist-cil/NSNumber.cs b/plist-cil/NSNumber.cs index ed91a94..272d3d1 100644 --- a/plist-cil/NSNumber.cs +++ b/plist-cil/NSNumber.cs @@ -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