diff --git a/plist-cil.test/ValuePreprocessorTests.cs b/plist-cil.test/ValuePreprocessorTests.cs new file mode 100644 index 0000000..07ff529 --- /dev/null +++ b/plist-cil.test/ValuePreprocessorTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Linq; +using Claunia.PropertyList; +using Xunit; + +namespace plistcil.test +{ + public static class ValuePreprocessorTests + { + // lock tests to make sure temporarily added / replaced preprocessors don't interfere with the other tests in this suite + private static readonly object _testLock = new(); + + [Fact] + public static void TestPassiveDefaultPreprocessorsRegistered() + { + byte[] testByteArray = [0x1, 0x2, 0x4, 0x8]; + + Assert.Equal(true, ValuePreprocessor.Preprocess(true, ValuePreprocessor.Type.BOOL)); + Assert.Equal(false, ValuePreprocessor.Preprocess(false, ValuePreprocessor.Type.BOOL)); + Assert.Equal("true", ValuePreprocessor.Preprocess("true", ValuePreprocessor.Type.BOOL)); + + Assert.Equal("42", ValuePreprocessor.Preprocess("42", ValuePreprocessor.Type.INTEGER)); + Assert.Equal(testByteArray, ValuePreprocessor.Preprocess(testByteArray, ValuePreprocessor.Type.INTEGER)); + + Assert.Equal("3.14159", ValuePreprocessor.Preprocess("3.14159", ValuePreprocessor.Type.FLOATING_POINT)); + Assert.Equal(testByteArray, ValuePreprocessor.Preprocess(testByteArray, ValuePreprocessor.Type.FLOATING_POINT)); + + Assert.Equal("2.71828", ValuePreprocessor.Preprocess("2.71828", ValuePreprocessor.Type.UNDEFINED_NUMBER)); + + Assert.Equal("TestString", ValuePreprocessor.Preprocess("TestString", ValuePreprocessor.Type.STRING)); + Assert.Equal(testByteArray, ValuePreprocessor.Preprocess(testByteArray, ValuePreprocessor.Type.STRING)); + + Assert.Equal("TestData", ValuePreprocessor.Preprocess("TestData", ValuePreprocessor.Type.DATA)); + Assert.Equal(testByteArray, ValuePreprocessor.Preprocess(testByteArray, ValuePreprocessor.Type.DATA)); + + Assert.Equal(testByteArray, ValuePreprocessor.Preprocess(testByteArray, ValuePreprocessor.Type.DATE)); + Assert.Equal("01.02.1903", ValuePreprocessor.Preprocess("01.02.1903", ValuePreprocessor.Type.DATE)); + Assert.Equal(23.0, ValuePreprocessor.Preprocess(23.0, ValuePreprocessor.Type.DATE)); + } + + [Fact] + public static void TestRegisterPreprocessor() + { + lock(_testLock) + { + Func examplePreprocessor = value => new string(value.Reverse().ToArray()); + string testString = "TestString"; + string expected = "gnirtStseT"; + + var testType = (ValuePreprocessor.Type)42; + + ValuePreprocessor.Set(examplePreprocessor, testType); + string actual = ValuePreprocessor.Preprocess(testString, testType); + + Assert.Equal(actual, expected); + + ValuePreprocessor.Unset(testType); + } + } + + [Fact] + public static void TestRegisteredPreprocessorSelection1() + { + lock(_testLock) + { + Func examplePreprocessor = value => (short)(value - 1); + short testShort = 42; + string testString = "TestString"; + + var testType = (ValuePreprocessor.Type)42; + + // correct value type, differing data type + ValuePreprocessor.Set(examplePreprocessor, testType); + ValuePreprocessor.Set(ValuePreprocessor.GetDefault(), testType); + + string actual1 = ValuePreprocessor.Preprocess(testString, testType); + short actual2 = ValuePreprocessor.Preprocess(testShort, testType); + + // assert unchanged, since the selected preprocessor != tested preprocessor + Assert.Equal(actual1, testString); + Assert.NotEqual(actual2, testShort); + + ValuePreprocessor.Remove(testType); + ValuePreprocessor.Remove(testType); + } + } + + [Fact] + public static void TestRegisteredPreprocessorSelection2() + { + lock(_testLock) + { + Func examplePreprocessor = value => new string(value.Reverse().ToArray()); + byte[] testByteArray = [0x42,]; + string testString = "TestString"; + + var testType = (ValuePreprocessor.Type)42; + + // correct value type, differing data type + ValuePreprocessor.Set(examplePreprocessor, testType); + ValuePreprocessor.Set(ValuePreprocessor.GetDefault(), testType); + + string actual1 = ValuePreprocessor.Preprocess(testString, testType); + byte[] actual2 = ValuePreprocessor.Preprocess(testByteArray, testType); + + Assert.NotEqual(actual1, testString); + + // assert unchanged, since the selected preprocessor != tested preprocessor + Assert.Equal(actual2, testByteArray); + + ValuePreprocessor.Unset(testType); + ValuePreprocessor.Remove(testType); + } + } + + [Fact] + public static void TestUnregisteredPreprocessorThrows() + { + int[] testArray = [1, 2, 4, 8]; + + // there's no registered preprocessor for byte array arguments for STRING + Assert.Throws(() => ValuePreprocessor.Preprocess(testArray, ValuePreprocessor.Type.STRING)); + } + } +} \ No newline at end of file diff --git a/plist-cil/ASCIIPropertyListParser.cs b/plist-cil/ASCIIPropertyListParser.cs index 42ecb43..9303561 100644 --- a/plist-cil/ASCIIPropertyListParser.cs +++ b/plist-cil/ASCIIPropertyListParser.cs @@ -409,15 +409,15 @@ namespace Claunia.PropertyList quotedString[4] == DATE_DATE_FIELD_DELIMITER) try { - return new NSDate(quotedString); + return new NSDate(ValuePreprocessor.Preprocess(quotedString, ValuePreprocessor.Type.DATE)); } catch(Exception) { //not a date? --> return string - return new NSString(quotedString); + return new NSString(ValuePreprocessor.Preprocess(quotedString, ValuePreprocessor.Type.STRING)); } - return new NSString(quotedString); + return new NSString(ValuePreprocessor.Preprocess(quotedString, ValuePreprocessor.Type.STRING)); } default: { @@ -429,7 +429,7 @@ namespace Claunia.PropertyList //non-numerical -> string or boolean string parsedString = ParseString(); - return new NSString(parsedString); + return new NSString(ValuePreprocessor.Preprocess(parsedString, ValuePreprocessor.Type.STRING)); } } } @@ -529,9 +529,9 @@ namespace Claunia.PropertyList Expect(DATA_GSBOOL_TRUE_TOKEN, DATA_GSBOOL_FALSE_TOKEN); if(Accept(DATA_GSBOOL_TRUE_TOKEN)) - obj = new NSNumber(true); + obj = new NSNumber(ValuePreprocessor.Preprocess(true, ValuePreprocessor.Type.BOOL)); else - obj = new NSNumber(false); + obj = new NSNumber(ValuePreprocessor.Preprocess(false, ValuePreprocessor.Type.BOOL)); //Skip the parsed boolean token Skip(); @@ -541,14 +541,14 @@ namespace Claunia.PropertyList //Date Skip(); string dateString = ReadInputUntil(DATA_END_TOKEN); - obj = new NSDate(dateString); + obj = new NSDate(ValuePreprocessor.Preprocess(dateString, ValuePreprocessor.Type.DATE)); } else if(Accept(DATA_GSINT_BEGIN_TOKEN, DATA_GSREAL_BEGIN_TOKEN)) { //Number Skip(); string numberString = ReadInputUntil(DATA_END_TOKEN); - obj = new NSNumber(numberString); + obj = new NSNumber(ValuePreprocessor.Preprocess(numberString, ValuePreprocessor.Type.UNDEFINED_NUMBER)); } //parse data end token @@ -569,7 +569,7 @@ namespace Claunia.PropertyList bytes[i] = (byte)byteValue; } - obj = new NSData(bytes); + obj = new NSData(ValuePreprocessor.Preprocess(bytes, ValuePreprocessor.Type.DATA)); //skip end token Skip(); @@ -586,18 +586,18 @@ namespace Claunia.PropertyList if(numericalString.Length <= 4 || numericalString[4] != DATE_DATE_FIELD_DELIMITER) - return new NSString(numericalString); + return new NSString(ValuePreprocessor.Preprocess(numericalString, ValuePreprocessor.Type.STRING)); try { - return new NSDate(numericalString); + return new NSDate(ValuePreprocessor.Preprocess(numericalString, ValuePreprocessor.Type.DATE)); } catch(Exception) { //An exception occurs if the string is not a date but just a string } - return new NSString(numericalString); + return new NSString(ValuePreprocessor.Preprocess(numericalString, ValuePreprocessor.Type.STRING)); } /// diff --git a/plist-cil/BinaryPropertyListParser.cs b/plist-cil/BinaryPropertyListParser.cs index 95a0e41..3559714 100644 --- a/plist-cil/BinaryPropertyListParser.cs +++ b/plist-cil/BinaryPropertyListParser.cs @@ -158,7 +158,7 @@ namespace Claunia.PropertyList //Read all bytes into a list byte[] buf = PropertyListParser.ReadAll(fs); - // Don't close the stream - that would be the responisibility of code that class + // Don't close the stream - that would be the responsibility of code that class // Parse return Parse(buf); } @@ -204,12 +204,12 @@ namespace Claunia.PropertyList case 0x8: { //false - return new NSNumber(false); + return new NSNumber(ValuePreprocessor.Preprocess(false, ValuePreprocessor.Type.BOOL)); } case 0x9: { //true - return new NSNumber(true); + return new NSNumber(ValuePreprocessor.Preprocess(true, ValuePreprocessor.Type.BOOL)); } case 0xC: { @@ -243,14 +243,14 @@ namespace Claunia.PropertyList //integer int length = 1 << objInfo; - return new NSNumber(bytes.Slice(offset + 1, length), NSNumber.INTEGER); + return new NSNumber(ValuePreprocessor.Preprocess(bytes.Slice(offset + 1, length).ToArray(), ValuePreprocessor.Type.INTEGER), NSNumber.INTEGER); } case 0x2: { //real int length = 1 << objInfo; - return new NSNumber(bytes.Slice(offset + 1, length), NSNumber.REAL); + return new NSNumber(ValuePreprocessor.Preprocess(bytes.Slice(offset + 1, length).ToArray(), ValuePreprocessor.Type.FLOATING_POINT), NSNumber.REAL); } case 0x3: { @@ -260,21 +260,21 @@ namespace Claunia.PropertyList PropertyListFormatException("The given binary property list contains a date object of an unknown type (" + objInfo + ")"); - return new NSDate(bytes.Slice(offset + 1, 8)); + return new NSDate(ValuePreprocessor.Preprocess(bytes.Slice(offset + 1, 8).ToArray(), ValuePreprocessor.Type.DATE)); } case 0x4: { //Data ReadLengthAndOffset(bytes, objInfo, offset, out int length, out int dataoffset); - return new NSData(CopyOfRange(bytes, offset + dataoffset, offset + dataoffset + length)); + return new NSData(ValuePreprocessor.Preprocess(CopyOfRange(bytes, offset + dataoffset, offset + dataoffset + length), ValuePreprocessor.Type.DATA)); } case 0x5: { //ASCII String, each character is 1 byte ReadLengthAndOffset(bytes, objInfo, offset, out int length, out int stroffset); - return new NSString(bytes.Slice(offset + stroffset, length), Encoding.ASCII); + return new NSString(ValuePreprocessor.Preprocess(bytes.Slice(offset + stroffset, length).ToArray(), ValuePreprocessor.Type.STRING), Encoding.ASCII); } case 0x6: { diff --git a/plist-cil/PropertyListParser.cs b/plist-cil/PropertyListParser.cs index d5105fc..688de80 100644 --- a/plist-cil/PropertyListParser.cs +++ b/plist-cil/PropertyListParser.cs @@ -1,4 +1,4 @@ -// plist-cil - An open source library to parse and generate property lists for .NET +// plist-cil - An open source library to parse and generate property lists for .NET // Copyright (C) 2015 Natalia Portillo // // This code is based on: @@ -147,6 +147,12 @@ namespace Claunia.PropertyList return type; } + /// Set up preprocessing functions for plist values. + /// A function that preprocesses the passed string and returns the adjusted value. + /// The type of value preprocessor to use. + public static void SetValuePreprocessor(Func preprocessor, ValuePreprocessor.Type type) => + ValuePreprocessor.Set(preprocessor, type); + /// Reads all bytes from an Stream and stores them in an array, up to a maximum count. /// The Stream pointing to the data that should be stored in the array. internal static byte[] ReadAll(Stream fs) diff --git a/plist-cil/ValuePreprocessor.cs b/plist-cil/ValuePreprocessor.cs new file mode 100644 index 0000000..d205a8e --- /dev/null +++ b/plist-cil/ValuePreprocessor.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; + +namespace Claunia.PropertyList +{ + /// + /// Allows you to override the default value class initialization for the values found + /// in the parsed plists by registering your own preprocessing implementations. + /// + public static class ValuePreprocessor + { + /// + /// Indicates the semantic type of content the preprocessor will work on--independent + /// from the underlying data type (which will be string in most cases anyway). + /// + public enum Type + { + BOOL, INTEGER, FLOATING_POINT, + UNDEFINED_NUMBER, STRING, DATA, + DATE + }; + + /// + /// A null-implementation of a preprocessor for registered, but passive, use cases. + /// + private static T NullPreprocessor(T value) => value; + + private record struct TypeIdentifier(Type ValueType, System.Type DataType); + + /// + /// Default preprocessors for all the standard cases. + /// + private static readonly Dictionary _preprocessors = new() + { + { new TypeIdentifier(Type.BOOL, typeof(bool)), NullPreprocessor }, + { new TypeIdentifier(Type.BOOL, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.INTEGER, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.INTEGER, typeof(byte[])), NullPreprocessor }, + { new TypeIdentifier(Type.FLOATING_POINT, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.FLOATING_POINT, typeof(byte[])), NullPreprocessor }, + { new TypeIdentifier(Type.UNDEFINED_NUMBER, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.STRING, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.STRING, typeof(byte[])), NullPreprocessor }, + { new TypeIdentifier(Type.DATA, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.DATA, typeof(byte[])), NullPreprocessor }, + { new TypeIdentifier(Type.DATE, typeof(string)), NullPreprocessor }, + { new TypeIdentifier(Type.DATE, typeof(double)), NullPreprocessor }, + { new TypeIdentifier(Type.DATE, typeof(byte[])), NullPreprocessor }, + }; + + /// + /// Get a default preprocessor. + /// + public static Func GetDefault() => NullPreprocessor; + + /// + /// Set up a custom preprocessor. + /// + public static void Set(Func preprocessor, Type type) => + _preprocessors[new(type, typeof(T))] = preprocessor; + + + /// + /// Unset a specific preprocessor--replaces it with a null-implementation + /// to prevent argument errors. + /// + /// If no appropriate preprocessor--not even a default null-implementation--was set up. + public static void Unset(Type type) => + _preprocessors[GetValidTypeIdentifier(type)] = NullPreprocessor; + + /// + /// Completely unregister a specific preprocessor--remove it instead of + /// replacing it with a null-implementation. + /// + /// If no appropriate preprocessor--not even a default null-implementation--was registered. + public static void Remove(Type type) => + _preprocessors.Remove(GetValidTypeIdentifier(type)); + + /// + /// Preprocess the supplied data using the appropriate registered implementation. + /// + /// If no appropriate preprocessor--not even a default null-implementation--was registered. + public static T Preprocess(T value, Type type) => TryGetPreprocessor(type, out Func preprocess) + ? preprocess(value) + : throw new ArgumentException($"Failed to find a preprocessor for value '{value}'."); + + /// + /// Gets the appropriate registered implementation--or null--and casts it back to + /// the required type. + /// + private static bool TryGetPreprocessor(Type type, out Func preprocess) + { + if(_preprocessors.TryGetValue(new TypeIdentifier(type, typeof(T)), out Delegate preprocessor)) + { + preprocess = (Func)preprocessor; + + return true; + } + + preprocess = default; + + return false; + } + + /// + /// Gets a type identifier if a preprocessor exists for it. + /// + /// If no appropriate preprocessor--not even a default null-implementation--was set up. + private static TypeIdentifier GetValidTypeIdentifier(Type type) + { + var identifier = new TypeIdentifier(type, typeof(T)); + + if(!_preprocessors.ContainsKey(identifier)) + { + throw new ArgumentException($"Failed to find a valid preprocessor type identifier."); + } + + return identifier; + } + } +} diff --git a/plist-cil/XmlPropertyListParser.cs b/plist-cil/XmlPropertyListParser.cs index e1b2f32..644602f 100644 --- a/plist-cil/XmlPropertyListParser.cs +++ b/plist-cil/XmlPropertyListParser.cs @@ -173,13 +173,13 @@ namespace Claunia.PropertyList return array; } - case "true": return new NSNumber(true); - case "false": return new NSNumber(false); - case "integer": return new NSNumber(GetNodeTextContents(n), NSNumber.INTEGER); - case "real": return new NSNumber(GetNodeTextContents(n), NSNumber.REAL); - case "string": return new NSString(GetNodeTextContents(n)); - case "data": return new NSData(GetNodeTextContents(n)); - default: return n.Name.Equals("date") ? new NSDate(GetNodeTextContents(n)) : null; + case "true": return new NSNumber(ValuePreprocessor.Preprocess(true, ValuePreprocessor.Type.BOOL)); + case "false": return new NSNumber(ValuePreprocessor.Preprocess(false, ValuePreprocessor.Type.BOOL)); + case "integer": return new NSNumber(ValuePreprocessor.Preprocess(GetNodeTextContents(n), ValuePreprocessor.Type.INTEGER), NSNumber.INTEGER); + case "real": return new NSNumber(ValuePreprocessor.Preprocess(GetNodeTextContents(n), ValuePreprocessor.Type.FLOATING_POINT), NSNumber.REAL); + case "string": return new NSString(ValuePreprocessor.Preprocess(GetNodeTextContents(n), ValuePreprocessor.Type.STRING)); + case "data": return new NSData(ValuePreprocessor.Preprocess(GetNodeTextContents(n), ValuePreprocessor.Type.DATA)); + default: return n.Name.Equals("date") ? new NSDate(ValuePreprocessor.Preprocess(GetNodeTextContents(n), ValuePreprocessor.Type.DATE)) : null; } }