diff --git a/CHANGELOG.md b/CHANGELOG.md index 3649b18..3f0af6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Future - Improved tokenization performance by allowing and forcing more aggresive inlining. - In my benchmark, this is about 8 to 9% faster +- Some exception messages have changed in how they reference child items. + "Can not deserialize array at position 0 to list: It has a non-integer key 'a' at element 2" + Would now be + "Can not deserialize array at position 0 to list: It has a non-integer key 'a' at element 1". + This is because internally we now always treat array and object tokens as key value pairs instead of individual items. # 1.3.0 - Removed net5 support and added net7 support diff --git a/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs b/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs index e14705c..7a7c998 100644 --- a/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs @@ -176,7 +176,7 @@ public void ExplicitToListNonIntegerKey() { PhpSerialization.Deserialize>("a:3:{i:0;s:5:\"Hello\";s:1:\"a\";s:5:\"World\";i:2;i:12345;}") ); - Assert.AreEqual("Can not deserialize array at position 0 to list: It has a non-integer key 'a' at element 2 (position 21).", ex.Message); + Assert.AreEqual("Can not deserialize array at position 0 to list: It has a non-integer key 'a' at element 1 (position 21).", ex.Message); } [TestMethod] diff --git a/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj b/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj index 9805039..e37dfd5 100644 --- a/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj +++ b/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj @@ -4,15 +4,15 @@ false - - - - + + + + - + - + \ No newline at end of file diff --git a/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs b/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs index 68fa496..d606403 100644 --- a/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs @@ -29,7 +29,6 @@ public void SerializesPhpDynamicObjectWithClassname() { data.SetClassName("phpDynamicObject"); data.Foo = "a"; data.Bar = 3.1415; - System.Console.WriteLine(data.Bar); Assert.AreEqual( "O:16:\"phpDynamicObject\":2:{s:3:\"Foo\";s:1:\"a\";s:3:\"Bar\";d:3.1415;}", PhpSerialization.Serialize(data) diff --git a/PhpSerializerNET/Deserializers/Array/ArrayDeserializer.cs b/PhpSerializerNET/Deserializers/Array/ArrayDeserializer.cs new file mode 100644 index 0000000..753a8ba --- /dev/null +++ b/PhpSerializerNET/Deserializers/Array/ArrayDeserializer.cs @@ -0,0 +1,22 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; + +namespace PhpSerializerNET; + +internal abstract class ArrayDeserializer { + protected readonly PhpDeserializationOptions _options; + internal PrimitiveDeserializer PrimitiveDeserializer { get; set; } + internal ObjectDeserializer ObjectDeserializer { get; set; } + + internal ArrayDeserializer(PhpDeserializationOptions options) { + this._options = options; + } + + internal abstract object Deserialize(PhpSerializeToken token); + internal abstract object Deserialize(PhpSerializeToken token, Type targetType); +} diff --git a/PhpSerializerNET/Deserializers/Array/TypedArrayDeserializer.cs b/PhpSerializerNET/Deserializers/Array/TypedArrayDeserializer.cs new file mode 100644 index 0000000..ca54d87 --- /dev/null +++ b/PhpSerializerNET/Deserializers/Array/TypedArrayDeserializer.cs @@ -0,0 +1,246 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace PhpSerializerNET; + +internal class TypedArrayDeserializer : ArrayDeserializer { + public TypedArrayDeserializer(PhpDeserializationOptions options) : base(options) { + } + + internal override object Deserialize(PhpSerializeToken token) { + return token.Type switch { + PhpSerializerType.Array => this.DeserializeArray(token), + PhpSerializerType.Object => this.ObjectDeserializer.Deserialize(token), + _ => this.PrimitiveDeserializer.Deserialize(token), + }; + } + + internal override object Deserialize(PhpSerializeToken token, Type targetType) { + switch (token.Type) { + case PhpSerializerType.Array: + if (targetType.IsAssignableTo(typeof(IList))) { + return this.MakeList(targetType, token); + } else if (targetType.IsAssignableTo(typeof(IDictionary))) { + return this.MakeDictionary(targetType, token); + } else if (targetType.IsClass) { + return this.MakeObject(targetType, token); + } else { + return this.MakeStruct(targetType, token); + } + case PhpSerializerType.Object: + return this.ObjectDeserializer.Deserialize(token, targetType); + default: + return this.PrimitiveDeserializer.Deserialize(token, targetType); + } + } + + private object MakeObject(Type targetType, PhpSerializeToken token) { + var result = Activator.CreateInstance(targetType); + Dictionary properties = TypeLookup.GetPropertyInfos(targetType, this._options); + + foreach(var item in token.Children) { + object propertyName; + if (item.Key.Type == PhpSerializerType.String) { + propertyName = this._options.CaseSensitiveProperties ? item.Key.Value : item.Key.Value.ToLower(); + } else if (item.Key.Type == PhpSerializerType.Integer) { + propertyName = item.Key.Value.PhpToLong(); + } else { + throw new DeserializationException( + $"Error encountered deserizalizing an object of type '{targetType.FullName}': " + + $"The key '{item.Key.Value}' (from the token at position {item.Key.Position}) has an unsupported type of '{item.Key.Type}'." + ); + } + if (!properties.ContainsKey(propertyName)) { + if (!this._options.AllowExcessKeys) { + throw new DeserializationException( + $"Could not bind the key \"{item.Key.Value}\" to object of type {targetType.Name}: No such property." + ); + } + continue; + } + var property = properties[propertyName]; + if (property != null) { // null if PhpIgnore'd + try { + property.SetValue( + result, + this.Deserialize(item.Value, property.PropertyType) + ); + } catch (Exception exception) { + throw new DeserializationException( + $"Exception encountered while trying to assign '{item.Value.Value}' to {targetType.Name}.{property.Name}. See inner exception for details.", + exception + ); + } + } + } + return result; + } + + + private object MakeDictionary(Type targetType, PhpSerializeToken token) { + var result = (IDictionary)Activator.CreateInstance(targetType); + if (result == null) { + throw new NullReferenceException($"Activator.CreateInstance({targetType.FullName}) returned null"); + } + if (!targetType.GenericTypeArguments.Any()) { + foreach(var item in token.Children) { + result.Add( + this.Deserialize(item.Key), + this.Deserialize(item.Value) + ); + } + return result; + } + Type keyType = targetType.GenericTypeArguments[0]; + Type valueType = targetType.GenericTypeArguments[1]; + + foreach(var item in token.Children) { + result.Add( + keyType == typeof(object) + ? this.Deserialize(item.Key) + : this.Deserialize(item.Key, keyType), + valueType == typeof(object) + ? this.Deserialize(item.Value) + : this.Deserialize(item.Value, valueType) + ); + } + return result; + } + + public object DeserializeArray(PhpSerializeToken token) { + if (this._options.UseLists == ListOptions.Never) { + var result = new Dictionary(); + foreach(var item in token.Children) { + result.Add( + this.Deserialize(item.Key), + this.Deserialize(item.Value) + ); + } + return result; + } + long previousKey = -1; + bool isList = true; + bool consecutive = true; + for (int i = 0; i < token.Children.Length; i ++) { + var item = token.Children[i]; + if (item.Key.Type != PhpSerializerType.Integer) { + isList = false; + break; + } else { + var key = item.Key.Value.PhpToLong(); + if (i == 0 || key == previousKey + 1) { + previousKey = key; + } else { + consecutive = false; + } + } + } + if (!isList || (this._options.UseLists == ListOptions.Default && consecutive == false)) { + var result = new Dictionary(); + foreach(var item in token.Children) { + result.Add( + this.Deserialize(item.Key), + this.Deserialize(item.Value) + ); + } + return result; + } else { + var result = new List(); + foreach(var item in token.Children) { + result.Add(this.Deserialize(item.Value)); + } + return result; + } + } + + private object MakeArray(Type targetType, PhpSerializeToken token) { + var elementType = targetType.GetElementType() ?? throw new InvalidOperationException("targetType.GetElementType() returned null"); + Array result = Array.CreateInstance(elementType, token.Children.Length); + + var arrayIndex = 0; + foreach(var item in token.Children) { + result.SetValue( + elementType == typeof(object) + ? this.Deserialize(item.Value) + : this.Deserialize(item.Value, elementType), + arrayIndex + ); + arrayIndex++; + } + return result; + } + + private object MakeList(Type targetType, PhpSerializeToken token) { + for (int i = 0; i < token.Children.Length; i ++) { + var item = token.Children[i]; + if (item.Key.Type != PhpSerializerType.Integer) { + throw new DeserializationException( + $"Can not deserialize array at position {token.Position} to list: " + + $"It has a non-integer key '{item.Key.Value}' at element {i} (position {item.Key.Position})." + ); + } + } + + if (targetType.IsArray) { + return this.MakeArray(targetType, token); + } + var result = (IList)Activator.CreateInstance(targetType); + if (result == null) { + throw new NullReferenceException("Activator.CreateInstance(targetType) returned null"); + } + Type itemType = typeof(object); + if (targetType.GenericTypeArguments.Length >= 1) { + itemType = targetType.GenericTypeArguments[0]; + } + + foreach(var item in token.Children) { + result.Add( + itemType == typeof(object) + ? this.Deserialize(item.Key) + : this.Deserialize(item.Value, itemType) + ); + } + return result; + } + + private object MakeStruct(Type targetType, PhpSerializeToken token) { + var result = Activator.CreateInstance(targetType); + Dictionary fields = TypeLookup.GetFieldInfos(targetType, this._options); + + foreach(var item in token.Children) { + var fieldName = this._options.CaseSensitiveProperties ? item.Key.Value : item.Key.Value.ToLower(); + if (!fields.ContainsKey(fieldName)) { + if (!this._options.AllowExcessKeys) { + throw new DeserializationException( + $"Could not bind the key \"{item.Key.Value}\" to struct of type {targetType.Name}: No such field." + ); + } + continue; + } + if (fields[fieldName] != null) { + var field = fields[fieldName]; + try { + field.SetValue(result, this.Deserialize(item.Value, field.FieldType)); + } catch (Exception exception) { + throw new DeserializationException( + $"Exception encountered while trying to assign '{item.Value}' to {targetType.Name}.{field.Name}. " + + "See inner exception for details.", + exception + ); + } + } + } + return result; + } +} \ No newline at end of file diff --git a/PhpSerializerNET/Deserializers/Array/UntypedArrayDeserializer.cs b/PhpSerializerNET/Deserializers/Array/UntypedArrayDeserializer.cs new file mode 100644 index 0000000..ff5ed5e --- /dev/null +++ b/PhpSerializerNET/Deserializers/Array/UntypedArrayDeserializer.cs @@ -0,0 +1,73 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; +using System.Collections.Generic; + +namespace PhpSerializerNET; + +internal class UntypedArrayDeserializer : ArrayDeserializer { + public UntypedArrayDeserializer(PhpDeserializationOptions options) : base(options) { + } + + internal override object Deserialize(PhpSerializeToken token) { + return token.Type switch { + PhpSerializerType.Array => this.DeserializeArray(token), + PhpSerializerType.Object => this.ObjectDeserializer.Deserialize(token), + _ => this.PrimitiveDeserializer.Deserialize(token), + }; + } + + public object DeserializeArray(PhpSerializeToken token) { + if (this._options.UseLists == ListOptions.Never) { + var result = new Dictionary(); + foreach(var item in token.Children) { + result.Add( + this.Deserialize(item.Key), + this.Deserialize(item.Value) + ); + } + return result; + } + long previousKey = -1; + bool isList = true; + bool consecutive = true; + for (int i = 0; i < token.Children.Length; i++) { + var item = token.Children[i]; + if (item.Key.Type != PhpSerializerType.Integer) { + isList = false; + break; + } else { + var key = item.Key.Value.PhpToLong(); + if (i == 0 || key == previousKey + 1) { + previousKey = key; + } else { + consecutive = false; + } + } + } + if (!isList || (this._options.UseLists == ListOptions.Default && consecutive == false)) { + var result = new Dictionary(); + foreach(var item in token.Children) { + result.Add( + this.Deserialize(item.Key), + this.Deserialize(item.Value) + ); + } + return result; + } else { + var result = new List(); + foreach(var item in token.Children) { + result.Add(this.Deserialize(item.Value)); + } + return result; + } + } + + internal override object Deserialize(PhpSerializeToken token, Type targetType) { + throw new NotImplementedException(); + } +} diff --git a/PhpSerializerNET/Deserializers/Object/ObjectDeserializer.cs b/PhpSerializerNET/Deserializers/Object/ObjectDeserializer.cs new file mode 100644 index 0000000..4db65a9 --- /dev/null +++ b/PhpSerializerNET/Deserializers/Object/ObjectDeserializer.cs @@ -0,0 +1,22 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; + +namespace PhpSerializerNET; + +internal abstract class ObjectDeserializer { + protected readonly PhpDeserializationOptions _options; + internal PrimitiveDeserializer PrimitiveDeserializer { get; set; } + internal ArrayDeserializer ArrayDeserializer { get; set; } + + internal ObjectDeserializer(PhpDeserializationOptions options) { + this._options = options; + } + + internal abstract object Deserialize(PhpSerializeToken token); + internal abstract object Deserialize(PhpSerializeToken token, Type targetType); +} diff --git a/PhpSerializerNET/Deserializers/Object/TypedObjectDeserializer.cs b/PhpSerializerNET/Deserializers/Object/TypedObjectDeserializer.cs new file mode 100644 index 0000000..2d4d35f --- /dev/null +++ b/PhpSerializerNET/Deserializers/Object/TypedObjectDeserializer.cs @@ -0,0 +1,184 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace PhpSerializerNET; + +internal class TypedObjectDeserializer : ObjectDeserializer { + public TypedObjectDeserializer(PhpDeserializationOptions options) : base(options) { + } + internal override object Deserialize(PhpSerializeToken token) { + switch (token.Type) { + case PhpSerializerType.Array: + return this.ArrayDeserializer.Deserialize(token); + case PhpSerializerType.Object: + return this.CreateObject(token); + default: + return this.PrimitiveDeserializer.Deserialize(token); + } + } + + internal override object Deserialize(PhpSerializeToken token, Type targetType) { + switch (token.Type) { + case PhpSerializerType.Array: + return this.ArrayDeserializer.Deserialize(token); + case PhpSerializerType.Object: + object result; + if (typeof(IDictionary).IsAssignableFrom(targetType)) { + result = MakeDictionary(targetType, token); + } else if (targetType.IsClass) { + result = MakeObject(targetType, token); + } else { + result = MakeStruct(targetType, token); + } + if (result is IPhpObject phpObject and not PhpDateTime) { + phpObject.SetClassName(token.Value); + } + return result; + default: + return this.PrimitiveDeserializer.Deserialize(token, targetType); + } + } + + private object CreateObject(PhpSerializeToken token) { + var typeName = token.Value; + object constructedObject; + Type targetType = null; + if (typeName != "stdClass" && this._options.EnableTypeLookup) { + targetType = TypeLookup.FindTypeInAssymbly(typeName, this._options.TypeCache.HasFlag(TypeCacheFlag.ClassNames)); + } + if (targetType != null && typeName != "stdClass") { + constructedObject = this.Deserialize(token, targetType); + } else { + dynamic result; + if (_options.StdClass == StdClassOption.Dynamic) { + result = new PhpDynamicObject(); + } else if (this._options.StdClass == StdClassOption.Dictionary) { + result = new PhpObjectDictionary(); + } else { + throw new DeserializationException("Encountered 'stdClass' and the behavior 'Throw' was specified in deserialization options."); + } + foreach(var item in token.Children) { + result.TryAdd( + item.Key.Value, + this.Deserialize(item.Value) + ); + } + constructedObject = result; + } + if (constructedObject is IPhpObject phpObject and not PhpDateTime) { + phpObject.SetClassName(typeName); + } + return constructedObject; + } + + private object MakeObject(Type targetType, PhpSerializeToken token) { + var result = Activator.CreateInstance(targetType); + Dictionary properties = TypeLookup.GetPropertyInfos(targetType, this._options); + + foreach(var item in token.Children) { + object propertyName; + if (item.Key.Type == PhpSerializerType.String) { + propertyName = this._options.CaseSensitiveProperties ? item.Key.Value : item.Key.Value.ToLower(); + } else if (item.Key.Type == PhpSerializerType.Integer) { + propertyName = item.Key.Value.PhpToLong(); + } else { + throw new DeserializationException( + $"Error encountered deserizalizing an object of type '{targetType.FullName}': " + + $"The key '{item.Key.Value}' (from the token at position {item.Key.Position}) has an unsupported type of '{item.Key.Type}'." + ); + } + if (!properties.ContainsKey(propertyName)) { + if (!this._options.AllowExcessKeys) { + throw new DeserializationException( + $"Could not bind the key \"{item.Key.Value}\" to object of type {targetType.Name}: No such property." + ); + } + continue; + } + var property = properties[propertyName]; + if (property != null) { // null if PhpIgnore'd + try { + property.SetValue( + result, + this.Deserialize(item.Value, property.PropertyType) + ); + } catch (Exception exception) { + throw new DeserializationException( + $"Exception encountered while trying to assign '{item.Value.Value}' to {targetType.Name}.{property.Name}. See inner exception for details.", + exception + ); + } + } + } + return result; + } + + private object MakeStruct(Type targetType, PhpSerializeToken token) { + var result = Activator.CreateInstance(targetType); + Dictionary fields = TypeLookup.GetFieldInfos(targetType, this._options); + + foreach(var item in token.Children) { + var fieldName = this._options.CaseSensitiveProperties ? item.Key.Value : item.Key.Value.ToLower(); + if (!fields.ContainsKey(fieldName)) { + if (!this._options.AllowExcessKeys) { + throw new DeserializationException( + $"Could not bind the key \"{item.Key.Value}\" to struct of type {targetType.Name}: No such field." + ); + } + continue; + } + if (fields[fieldName] != null) { + var field = fields[fieldName]; + try { + field.SetValue(result, this.Deserialize(item.Value, field.FieldType)); + } catch (Exception exception) { + throw new DeserializationException( + $"Exception encountered while trying to assign '{item.Value}' to {targetType.Name}.{field.Name}. " + + "See inner exception for details.", + exception + ); + } + } + } + return result; + } + + private object MakeDictionary(Type targetType, PhpSerializeToken token) { + var result = (IDictionary)Activator.CreateInstance(targetType); + if (result == null) { + throw new NullReferenceException($"Activator.CreateInstance({targetType.FullName}) returned null"); + } + if (!targetType.GenericTypeArguments.Any()) { + foreach(var item in token.Children) { + result.Add( + this.Deserialize(item.Key), + this.Deserialize(item.Value) + ); + } + return result; + } + Type keyType = targetType.GenericTypeArguments[0]; + Type valueType = targetType.GenericTypeArguments[1]; + + foreach(var item in token.Children) { + result.Add( + keyType == typeof(object) + ? this.Deserialize(item.Key) + : this.Deserialize(item.Key, keyType), + valueType == typeof(object) + ? this.Deserialize(item.Value) + : this.Deserialize(item.Value, valueType) + ); + } + return result; + } +} \ No newline at end of file diff --git a/PhpSerializerNET/Deserializers/Object/UntypedObjectDeserializer.cs b/PhpSerializerNET/Deserializers/Object/UntypedObjectDeserializer.cs new file mode 100644 index 0000000..77dcbd7 --- /dev/null +++ b/PhpSerializerNET/Deserializers/Object/UntypedObjectDeserializer.cs @@ -0,0 +1,52 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; + +namespace PhpSerializerNET; + +internal class UntypedObjectDeserializer : ObjectDeserializer { + public UntypedObjectDeserializer(PhpDeserializationOptions options) : base(options) { + } + + internal override object Deserialize(PhpSerializeToken token) { + return token.Type switch { + PhpSerializerType.Array => this.ArrayDeserializer.Deserialize(token), + PhpSerializerType.Object => this.CreateObject(token), + _ => this.PrimitiveDeserializer.Deserialize(token), + }; + } + + private object CreateObject(PhpSerializeToken token) { + var typeName = token.Value; + object constructedObject; + + dynamic result; + if (_options.StdClass == StdClassOption.Dynamic) { + result = new PhpDynamicObject(); + } else if (this._options.StdClass == StdClassOption.Dictionary) { + result = new PhpObjectDictionary(); + } else { + throw new DeserializationException("Encountered 'stdClass' and the behavior 'Throw' was specified in deserialization options."); + } + foreach(var item in token.Children) { + result.TryAdd( + item.Key.Value, + this.Deserialize(item.Value) + ); + } + constructedObject = result; + + if (constructedObject is IPhpObject phpObject and not PhpDateTime) { + phpObject.SetClassName(typeName); + } + return constructedObject; + } + + internal override object Deserialize(PhpSerializeToken token, Type targetType) { + throw new NotImplementedException(); + } +} diff --git a/PhpSerializerNET/Deserializers/Primitive/DefaultPrimitiveDeserializer.cs b/PhpSerializerNET/Deserializers/Primitive/DefaultPrimitiveDeserializer.cs new file mode 100644 index 0000000..28732dd --- /dev/null +++ b/PhpSerializerNET/Deserializers/Primitive/DefaultPrimitiveDeserializer.cs @@ -0,0 +1,40 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; + +namespace PhpSerializerNET; + +internal class DefaultPrimitiveDeserializer : PrimitiveDeserializer { + public DefaultPrimitiveDeserializer(PhpDeserializationOptions options) : base(options) { + } + + internal override object Deserialize(PhpSerializeToken token) { + switch (token.Type) { + case PhpSerializerType.Boolean: + return token.Value.PhpToBool(); + case PhpSerializerType.Integer: + return token.Value.PhpToLong(); + case PhpSerializerType.Floating: + return token.Value.PhpToDouble(); + case PhpSerializerType.String: + if (this._options.NumberStringToBool && (token.Value == "0" || token.Value == "1")) { + return token.Value.PhpToBool(); + } + return token.Value; + case PhpSerializerType.Array: + case PhpSerializerType.Object: + throw new ArgumentException("The token given to DefaultPrimitiveDeserializer must be primitive."); + case PhpSerializerType.Null: + default: + return null; + } + } + + internal override object Deserialize(PhpSerializeToken token, Type targetType) { + throw new NotImplementedException("DefaultPrimitiveDeserializer does not implement target type deserialization"); + } +} diff --git a/PhpSerializerNET/Deserializers/Primitive/PrimitiveDeserializer.cs b/PhpSerializerNET/Deserializers/Primitive/PrimitiveDeserializer.cs new file mode 100644 index 0000000..ec248e9 --- /dev/null +++ b/PhpSerializerNET/Deserializers/Primitive/PrimitiveDeserializer.cs @@ -0,0 +1,20 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; + +namespace PhpSerializerNET; + +internal abstract class PrimitiveDeserializer { + protected readonly PhpDeserializationOptions _options; + + protected PrimitiveDeserializer(PhpDeserializationOptions options) { + this._options = options; + } + + internal abstract object Deserialize(PhpSerializeToken token); + internal abstract object Deserialize(PhpSerializeToken token, Type targetType); +} diff --git a/PhpSerializerNET/Deserializers/Primitive/TypedPrimitiveDeserializer.cs b/PhpSerializerNET/Deserializers/Primitive/TypedPrimitiveDeserializer.cs new file mode 100644 index 0000000..72ba26a --- /dev/null +++ b/PhpSerializerNET/Deserializers/Primitive/TypedPrimitiveDeserializer.cs @@ -0,0 +1,188 @@ +/*! + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +!*/ + +using System; +using System.Globalization; +using System.Reflection; + +namespace PhpSerializerNET; + +internal class TypedPrimitiveDeserializer : PrimitiveDeserializer { + public TypedPrimitiveDeserializer(PhpDeserializationOptions options) : base(options) { + } + + internal override object Deserialize(PhpSerializeToken token) { + switch (token.Type) { + case PhpSerializerType.Boolean: + return token.Value.PhpToBool(); + case PhpSerializerType.Integer: + return token.Value.PhpToLong(); + case PhpSerializerType.Floating: + return token.Value.PhpToDouble(); + case PhpSerializerType.String: + if (this._options.NumberStringToBool && (token.Value == "0" || token.Value == "1")) { + return token.Value.PhpToBool(); + } + return token.Value; + case PhpSerializerType.Array: + case PhpSerializerType.Object: + throw new ArgumentException("The token given to PrimitiveDeserializer must be primitive."); + case PhpSerializerType.Null: + default: + return null; + } + } + + internal override object Deserialize(PhpSerializeToken token, Type targetType) { + if (targetType == null) { + throw new ArgumentNullException(nameof(targetType)); + } + + switch (token.Type) { + case PhpSerializerType.Boolean: { + return DeserializeBoolean(targetType, token); + } + case PhpSerializerType.Integer: + return DeserializeInteger(targetType, token); + case PhpSerializerType.Floating: + return DeserializeDouble(targetType, token); + case PhpSerializerType.String: + return DeserializeTokenFromSimpleType(targetType, token); + case PhpSerializerType.Object: + case PhpSerializerType.Array: { + throw new ArgumentException("The token given to PrimitiveDeserializer must be primitive."); + } + case PhpSerializerType.Null: + default: + if (targetType.IsValueType) { + return Activator.CreateInstance(targetType); + } else { + return null; + } + } + } + + private object DeserializeInteger(Type targetType, PhpSerializeToken token) { + return Type.GetTypeCode(targetType) switch { + TypeCode.Int16 => short.Parse(token.Value), + TypeCode.Int32 => int.Parse(token.Value), + TypeCode.Int64 => long.Parse(token.Value), + TypeCode.UInt16 => ushort.Parse(token.Value), + TypeCode.UInt32 => uint.Parse(token.Value), + TypeCode.UInt64 => ulong.Parse(token.Value), + TypeCode.SByte => sbyte.Parse(token.Value), + _ => this.DeserializeTokenFromSimpleType(targetType, token), + }; + } + + private object DeserializeDouble(Type targetType, PhpSerializeToken token) { + if (targetType == typeof(double) || targetType == typeof(float)) { + return token.Value.PhpToDouble(); + } + + token.Value = token.Value switch { + "INF" => double.PositiveInfinity.ToString(CultureInfo.InvariantCulture), + "-INF" => double.NegativeInfinity.ToString(CultureInfo.InvariantCulture), + _ => token.Value, + }; + return this.DeserializeTokenFromSimpleType(targetType, token); + } + + private static object DeserializeBoolean(Type targetType, PhpSerializeToken token) { + if (targetType == typeof(bool) || targetType == typeof(bool?)) { + return token.Value.PhpToBool(); + } + Type underlyingType = targetType; + if (targetType.IsNullableReferenceType()) { + underlyingType = targetType.GenericTypeArguments[0]; + } + + if (underlyingType.IsIConvertible()) { + return ((IConvertible)token.Value.PhpToBool()).ToType(underlyingType, CultureInfo.InvariantCulture); + } else { + throw new DeserializationException( + $"Can not assign value \"{token.Value}\" (at position {token.Position}) to target type of {targetType.Name}." + ); + } + } + + private object DeserializeTokenFromSimpleType(Type givenType, PhpSerializeToken token) { + var targetType = givenType; + if (!targetType.IsPrimitive && targetType.IsNullableReferenceType()) { + if (token.Value == "" && _options.EmptyStringToDefault) { + return null; + } + + targetType = targetType.GenericTypeArguments[0]; + if (targetType == null) { + throw new NullReferenceException("Could not get underlying type for nullable reference type " + givenType); + } + } + + // Short-circuit strings: + if (targetType == typeof(string)) { + return token.Value == "" && _options.EmptyStringToDefault + ? default + : token.Value; + } + + if (targetType.IsEnum) { + // Enums are converted by name if the token is a string and by underlying value if they are not + if (token.Value == "" && this._options.EmptyStringToDefault) { + return Activator.CreateInstance(targetType); + } + + if (token.Type != PhpSerializerType.String) { + return Enum.Parse(targetType, token.Value); + } + + FieldInfo foundFieldInfo = TypeLookup.GetEnumInfo(targetType, token.Value, this._options); + + if (foundFieldInfo == null) { + throw new DeserializationException( + $"Exception encountered while trying to assign '{token.Value}' to type '{targetType.Name}'. " + + $"The value could not be matched to an enum member."); + } + + return foundFieldInfo.GetRawConstantValue(); + } + + if (targetType.IsIConvertible()) { + if (token.Value == "" && _options.EmptyStringToDefault) { + return Activator.CreateInstance(targetType); + } + + if (targetType == typeof(bool)) { + if (_options.NumberStringToBool && token.Value is "0" or "1") { + return token.Value.PhpToBool(); + } + } + + try { + return ((IConvertible)token.Value).ToType(targetType, CultureInfo.InvariantCulture); + } catch (Exception exception) { + throw new DeserializationException( + $"Exception encountered while trying to assign '{token.Value}' to type {targetType.Name}. See inner exception for details.", + exception + ); + } + } + + if (targetType == typeof(Guid)) { + return token.Value == "" && _options.EmptyStringToDefault + ? default + : new Guid(token.Value); + } + + if (targetType == typeof(object)) { + return token.Value == "" && _options.EmptyStringToDefault + ? default + : token.Value; + } + + throw new DeserializationException($"Can not assign value \"{token.Value}\" (at position {token.Position}) to target type of {targetType.Name}."); + } +} \ No newline at end of file diff --git a/PhpSerializerNET/Enums/ListOptions.cs b/PhpSerializerNET/Enums/ListOptions.cs index 7ec85e2..ca679e5 100644 --- a/PhpSerializerNET/Enums/ListOptions.cs +++ b/PhpSerializerNET/Enums/ListOptions.cs @@ -16,7 +16,7 @@ public enum ListOptions { Default, /// - /// Convert associative array to list when all keys are integers, consecutiveh or not. + /// Convert associative array to list when all keys are integers, consecutive or not. /// OnAllIntegerKeys, diff --git a/PhpSerializerNET/PhpDeserializer.cs b/PhpSerializerNET/PhpDeserializer.cs index 767eeea..5ba71c3 100644 --- a/PhpSerializerNET/PhpDeserializer.cs +++ b/PhpSerializerNET/PhpDeserializer.cs @@ -1,478 +1,105 @@ -/** - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. -**/ - using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; namespace PhpSerializerNET; + internal class PhpDeserializer { private readonly PhpDeserializationOptions _options; - private readonly PhpSerializeToken _token; + private PhpSerializeToken _token; - public PhpDeserializer(string input, PhpDeserializationOptions options) { - _options = options; - if (_options == null) { - _options = PhpDeserializationOptions.DefaultOptions; - } + internal PhpDeserializer(string input, PhpDeserializationOptions options) { + this._options = options ?? PhpDeserializationOptions.DefaultOptions; this._token = new PhpTokenizer(input, this._options.InputEncoding).Tokenize(); } - public object Deserialize() { - return this.DeserializeToken(this._token); - } - - public object Deserialize(Type targetType) { - return this.DeserializeToken(targetType, this._token); - } - - public T Deserialize() { - return (T)this.Deserialize(typeof(T)); - } - - /// - /// Reset the type lookup cache. - /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. - /// - public static void ClearTypeCache() { - TypeLookup.ClearTypeCache(); - } + private ObjectDeserializer GetObjectDeserializer() { + ObjectDeserializer deserializer; + PrimitiveDeserializer primitiveDeserializer; + ArrayDeserializer arraydeserializer; + if (this._options.EnableTypeLookup && this._token.ContainsObjects()) { + deserializer = new TypedObjectDeserializer(this._options); + primitiveDeserializer = new TypedPrimitiveDeserializer(this._options); + arraydeserializer = new TypedArrayDeserializer(this._options); - /// - /// Reset the property info cache. - /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. - /// - public static void ClearPropertyInfoCache() { - TypeLookup.ClearPropertyInfoCache(); - } + arraydeserializer.PrimitiveDeserializer = primitiveDeserializer; + arraydeserializer.ObjectDeserializer = deserializer; - private object DeserializeToken(PhpSerializeToken token) { - switch (token.Type) { - case PhpSerializerType.Boolean: - return token.Value.PhpToBool(); - case PhpSerializerType.Integer: - return token.Value.PhpToLong(); - case PhpSerializerType.Floating: - return token.Value.PhpToDouble(); - case PhpSerializerType.String: - if (this._options.NumberStringToBool && (token.Value == "0" || token.Value == "1")) { - return token.Value.PhpToBool(); - } - return token.Value; - case PhpSerializerType.Array: - return MakeCollection(token); - case PhpSerializerType.Object: - return MakeClass(token); - case PhpSerializerType.Null: - default: - return null; + deserializer.PrimitiveDeserializer = primitiveDeserializer; + deserializer.ArrayDeserializer = arraydeserializer; + return deserializer; } - } + deserializer = new UntypedObjectDeserializer(this._options); + primitiveDeserializer = new DefaultPrimitiveDeserializer(this._options); + arraydeserializer = new UntypedArrayDeserializer(this._options); - private object MakeClass(PhpSerializeToken token) { - var typeName = token.Value; - object constructedObject; - Type targetType = null; - if (typeName != "stdClass" && this._options.EnableTypeLookup) { - targetType = TypeLookup.FindTypeInAssymbly(typeName, this._options.TypeCache.HasFlag(TypeCacheFlag.ClassNames)); - } - if (targetType != null && typeName != "stdClass") { - constructedObject = this.DeserializeToken(targetType, token); - } else { - dynamic result; - if (_options.StdClass == StdClassOption.Dynamic) { - result = new PhpDynamicObject(); - } else if (this._options.StdClass == StdClassOption.Dictionary) { - result = new PhpObjectDictionary(); - } else { - throw new DeserializationException("Encountered 'stdClass' and the behavior 'Throw' was specified in deserialization options."); - } - for (int i = 0; i < token.Children.Length; i += 2) { - result.TryAdd( - token.Children[i].Value, - this.DeserializeToken(token.Children[i + 1]) - ); - } - constructedObject = result; - } - if (constructedObject is IPhpObject phpObject and not PhpDateTime) { - phpObject.SetClassName(typeName); - } - return constructedObject; - } + arraydeserializer.PrimitiveDeserializer = primitiveDeserializer; + arraydeserializer.ObjectDeserializer = deserializer; - private object DeserializeToken(Type targetType, PhpSerializeToken token) { - if (targetType == null) { - throw new ArgumentNullException(nameof(targetType)); - } - - switch (token.Type) { - case PhpSerializerType.Boolean: { - return DeserializeBoolean(targetType, token); - } - case PhpSerializerType.Integer: - return DeserializeInteger(targetType, token); - case PhpSerializerType.Floating: - return DeserializeDouble(targetType, token); - case PhpSerializerType.String: - return DeserializeTokenFromSimpleType(targetType, token); - case PhpSerializerType.Object: { - object result; - if (typeof(IDictionary).IsAssignableFrom(targetType)) { - result = MakeDictionary(targetType, token); - } else if (targetType.IsClass) { - result = MakeObject(targetType, token); - } else { - result = MakeStruct(targetType, token); - } - if (result is IPhpObject phpObject and not PhpDateTime) { - phpObject.SetClassName(token.Value); - } - return result; - } - case PhpSerializerType.Array: { - if (targetType.IsAssignableTo(typeof(IList))) { - return this.MakeList(targetType, token); - } else if (targetType.IsAssignableTo(typeof(IDictionary))) { - return this.MakeDictionary(targetType, token); - } else if (targetType.IsClass) { - return this.MakeObject(targetType, token); - } else { - return this.MakeStruct(targetType, token); - } - } - case PhpSerializerType.Null: - default: - if (targetType.IsValueType) { - return Activator.CreateInstance(targetType); - } else { - return null; - } - } + deserializer.PrimitiveDeserializer = primitiveDeserializer; + deserializer.ArrayDeserializer = arraydeserializer; + return deserializer; } - private object DeserializeInteger(Type targetType, PhpSerializeToken token) { - return Type.GetTypeCode(targetType) switch { - TypeCode.Int16 => short.Parse(token.Value), - TypeCode.Int32 => int.Parse(token.Value), - TypeCode.Int64 => long.Parse(token.Value), - TypeCode.UInt16 => ushort.Parse(token.Value), - TypeCode.UInt32 => uint.Parse(token.Value), - TypeCode.UInt64 => ulong.Parse(token.Value), - TypeCode.SByte => sbyte.Parse(token.Value), - _ => this.DeserializeTokenFromSimpleType(targetType, token), - }; - } - - private object DeserializeDouble(Type targetType, PhpSerializeToken token) { - if (targetType == typeof(double) || targetType == typeof(float)) { - return token.Value.PhpToDouble(); - } - - token.Value = token.Value switch { - "INF" => double.PositiveInfinity.ToString(CultureInfo.InvariantCulture), - "-INF" => double.NegativeInfinity.ToString(CultureInfo.InvariantCulture), - _ => token.Value, - }; - return this.DeserializeTokenFromSimpleType(targetType, token); - } - - private static object DeserializeBoolean(Type targetType, PhpSerializeToken token) { - if (targetType == typeof(bool) || targetType == typeof(bool?)) { - return token.Value.PhpToBool(); - } - Type underlyingType = targetType; - if (targetType.IsNullableReferenceType()) { - underlyingType = targetType.GenericTypeArguments[0]; - } - - if (underlyingType.IsIConvertible()) { - return ((IConvertible)token.Value.PhpToBool()).ToType(underlyingType, CultureInfo.InvariantCulture); - } else { - throw new DeserializationException( - $"Can not assign value \"{token.Value}\" (at position {token.Position}) to target type of {targetType.Name}." - ); - } - } - - private object DeserializeTokenFromSimpleType(Type givenType, PhpSerializeToken token) { - var targetType = givenType; - if (!targetType.IsPrimitive && targetType.IsNullableReferenceType()) { - if (token.Value == "" && _options.EmptyStringToDefault) { - return null; - } - - targetType = targetType.GenericTypeArguments[0]; - if (targetType == null) { - throw new NullReferenceException("Could not get underlying type for nullable reference type " + givenType); - } - } - - // Short-circuit strings: - if (targetType == typeof(string)) { - return token.Value == "" && _options.EmptyStringToDefault - ? default - : token.Value; - } - - if (targetType.IsEnum) { - // Enums are converted by name if the token is a string and by underlying value if they are not - if (token.Value == "" && this._options.EmptyStringToDefault) { - return Activator.CreateInstance(targetType); - } - - if (token.Type != PhpSerializerType.String) { - return Enum.Parse(targetType, token.Value); - } - - FieldInfo foundFieldInfo = TypeLookup.GetEnumInfo(targetType, token.Value, this._options); - - if (foundFieldInfo == null) { - throw new DeserializationException( - $"Exception encountered while trying to assign '{token.Value}' to type '{targetType.Name}'. " + - $"The value could not be matched to an enum member."); - } - - return foundFieldInfo.GetRawConstantValue(); - } - - if (targetType.IsIConvertible()) { - if (token.Value == "" && _options.EmptyStringToDefault) { - return Activator.CreateInstance(targetType); - } - - if (targetType == typeof(bool)) { - if (_options.NumberStringToBool && token.Value is "0" or "1") { - return token.Value.PhpToBool(); - } - } - - try { - return ((IConvertible)token.Value).ToType(targetType, CultureInfo.InvariantCulture); - } catch (Exception exception) { - throw new DeserializationException( - $"Exception encountered while trying to assign '{token.Value}' to type {targetType.Name}. See inner exception for details.", - exception - ); - } - } - - if (targetType == typeof(Guid)) { - return token.Value == "" && _options.EmptyStringToDefault - ? default - : new Guid(token.Value); - } - - if (targetType == typeof(object)) { - return token.Value == "" && _options.EmptyStringToDefault - ? default - : token.Value; - } - - throw new DeserializationException($"Can not assign value \"{token.Value}\" (at position {token.Position}) to target type of {targetType.Name}."); - } - - private object MakeStruct(Type targetType, PhpSerializeToken token) { - var result = Activator.CreateInstance(targetType); - Dictionary fields = TypeLookup.GetFieldInfos(targetType, this._options); + internal object Deserialize() { + var primitiveDeserializer = new DefaultPrimitiveDeserializer(this._options); + switch (this._token.Type) { + case PhpSerializerType.Array: { + var arraydeserializer = new UntypedArrayDeserializer(this._options); + arraydeserializer.ObjectDeserializer = this.GetObjectDeserializer(); + arraydeserializer.PrimitiveDeserializer = primitiveDeserializer; - for (int i = 0; i < token.Children.Length; i += 2) { - var fieldName = this._options.CaseSensitiveProperties ? token.Children[i].Value : token.Children[i].Value.ToLower(); - var valueToken = token.Children[i + 1]; - if (!fields.ContainsKey(fieldName)) { - if (!this._options.AllowExcessKeys) { - throw new DeserializationException( - $"Could not bind the key \"{token.Children[i].Value}\" to struct of type {targetType.Name}: No such field." - ); - } - continue; - } - if (fields[fieldName] != null) { - var field = fields[fieldName]; - try { - field.SetValue(result, DeserializeToken(field.FieldType, valueToken)); - } catch (Exception exception) { - throw new DeserializationException( - $"Exception encountered while trying to assign '{valueToken.Value}' to {targetType.Name}.{field.Name}. " + - "See inner exception for details.", - exception - ); + arraydeserializer.ObjectDeserializer.ArrayDeserializer = arraydeserializer; + arraydeserializer.ObjectDeserializer.PrimitiveDeserializer = arraydeserializer.PrimitiveDeserializer; + return arraydeserializer.Deserialize(this._token); } + case PhpSerializerType.Object: { + return this.GetObjectDeserializer().Deserialize(this._token); } + default: + return new DefaultPrimitiveDeserializer(this._options).Deserialize(this._token); } - return result; } - private object MakeObject(Type targetType, PhpSerializeToken token) { - var result = Activator.CreateInstance(targetType); - Dictionary properties = TypeLookup.GetPropertyInfos(targetType, this._options); - - for (int i = 0; i < token.Children.Length; i += 2) { - object propertyName; - if (token.Children[i].Type == PhpSerializerType.String) { - propertyName = this._options.CaseSensitiveProperties ? token.Children[i].Value : token.Children[i].Value.ToLower(); - } else if (token.Children[i].Type == PhpSerializerType.Integer) { - propertyName = token.Children[i].Value.PhpToLong(); - } else { - throw new DeserializationException( - $"Error encountered deserizalizing an object of type '{targetType.FullName}': " + - $"The key '{token.Children[i].Value}' (from the token at position {token.Children[i].Position}) has an unsupported type of '{token.Children[i].Type}'." - ); - } + internal object Deserialize(Type targetType) { + var primitiveDeserializer = new TypedPrimitiveDeserializer(this._options); + var arrayDeserializer = new TypedArrayDeserializer(this._options); + var objectDeserializer = new TypedObjectDeserializer(this._options); - var valueToken = token.Children[i + 1]; + objectDeserializer.PrimitiveDeserializer = primitiveDeserializer; + arrayDeserializer.PrimitiveDeserializer = primitiveDeserializer; - if (!properties.ContainsKey(propertyName)) { - if (!this._options.AllowExcessKeys) { - throw new DeserializationException( - $"Could not bind the key \"{token.Children[i].Value}\" to object of type {targetType.Name}: No such property." - ); - } - continue; + objectDeserializer.ArrayDeserializer = arrayDeserializer; + arrayDeserializer.ObjectDeserializer = objectDeserializer; + switch (this._token.Type) { + case PhpSerializerType.Array: { + return arrayDeserializer.Deserialize(this._token, targetType); } - var property = properties[propertyName]; - if (property != null) { // null if PhpIgnore'd - try { - property.SetValue( - result, - DeserializeToken(property.PropertyType, valueToken) - ); - } catch (Exception exception) { - throw new DeserializationException( - $"Exception encountered while trying to assign '{valueToken.Value}' to {targetType.Name}.{property.Name}. See inner exception for details.", - exception - ); - } + case PhpSerializerType.Object: { + return objectDeserializer.Deserialize(this._token, targetType); } + default: + return primitiveDeserializer.Deserialize(this._token, targetType); } - return result; - } - - private object MakeArray(Type targetType, PhpSerializeToken token) { - var elementType = targetType.GetElementType() ?? throw new InvalidOperationException("targetType.GetElementType() returned null"); - Array result = Array.CreateInstance(elementType, token.Children.Length / 2); - - var arrayIndex = 0; - for (int i = 1; i < token.Children.Length; i += 2) { - result.SetValue( - elementType == typeof(object) - ? DeserializeToken(token.Children[i]) - : DeserializeToken(elementType, token.Children[i]), - arrayIndex - ); - arrayIndex++; - } - return result; } - private object MakeList(Type targetType, PhpSerializeToken token) { - for (int i = 0; i < token.Children.Length; i += 2) { - if (token.Children[i].Type != PhpSerializerType.Integer) { - throw new DeserializationException( - $"Can not deserialize array at position {token.Position} to list: " + - $"It has a non-integer key '{token.Children[i].Value}' at element {i} (position {token.Children[i].Position})." - ); - } - } - - if (targetType.IsArray) { - return MakeArray(targetType, token); - } - var result = (IList)Activator.CreateInstance(targetType); - if (result == null) { - throw new NullReferenceException("Activator.CreateInstance(targetType) returned null"); - } - Type itemType = typeof(object); - if (targetType.GenericTypeArguments.Length >= 1) { - itemType = targetType.GenericTypeArguments[0]; - } - - for (int i = 1; i < token.Children.Length; i += 2) { - result.Add( - itemType == typeof(object) - ? DeserializeToken(token.Children[i]) - : DeserializeToken(itemType, token.Children[i]) - ); - } - return result; + internal T Deserialize() { + return (T)this.Deserialize(typeof(T)); } - private object MakeDictionary(Type targetType, PhpSerializeToken token) { - var result = (IDictionary)Activator.CreateInstance(targetType); - if (result == null) { - throw new NullReferenceException($"Activator.CreateInstance({targetType.FullName}) returned null"); - } - if (!targetType.GenericTypeArguments.Any()) { - for (int i = 0; i < token.Children.Length; i += 2) { - var keyToken = token.Children[i]; - var valueToken = token.Children[i + 1]; - result.Add( - DeserializeToken(keyToken), - DeserializeToken(valueToken) - ); - } - return result; - } - Type keyType = targetType.GenericTypeArguments[0]; - Type valueType = targetType.GenericTypeArguments[1]; - for (int i = 0; i < token.Children.Length; i += 2) { - var keyToken = token.Children[i]; - var valueToken = token.Children[i + 1]; - result.Add( - keyType == typeof(object) - ? DeserializeToken(keyToken) - : DeserializeToken(keyType, keyToken), - valueType == typeof(object) - ? DeserializeToken(valueToken) - : DeserializeToken(valueType, valueToken) - ); - } - return result; + /// + /// Reset the type lookup cache. + /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. + /// + public static void ClearTypeCache() { + TypeLookup.ClearTypeCache(); } - private object MakeCollection(PhpSerializeToken token) { - if (this._options.UseLists == ListOptions.Never) { - return this.MakeDictionary(typeof(Dictionary), token); - } - long previousKey = -1; - bool isList = true; - bool consecutive = true; - for (int i = 0; i < token.Children.Length; i += 2) { - if (token.Children[i].Type != PhpSerializerType.Integer) { - isList = false; - break; - } else { - var key = token.Children[i].Value.PhpToLong(); - if (i == 0 || key == previousKey + 1) { - previousKey = key; - } else { - consecutive = false; - } - } - } - if (!isList || (this._options.UseLists == ListOptions.Default && consecutive == false)) { - var result = new Dictionary(); - for (int i = 0; i < token.Children.Length; i += 2) { - result.Add( - this.DeserializeToken(token.Children[i]), - this.DeserializeToken(token.Children[i + 1]) - ); - } - return result; - } else { - var result = new List(); - for (int i = 1; i < token.Children.Length; i += 2) { - result.Add(this.DeserializeToken(token.Children[i])); - } - return result; - } + /// + /// Reset the property info cache. + /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. + /// + public static void ClearPropertyInfoCache() { + TypeLookup.ClearPropertyInfoCache(); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET/PhpSerialization.cs b/PhpSerializerNET/PhpSerialization.cs index 4d68796..26babcb 100644 --- a/PhpSerializerNET/PhpSerialization.cs +++ b/PhpSerializerNET/PhpSerialization.cs @@ -113,17 +113,17 @@ public static string Serialize(object? input, PhpSerializiationOptions? options .Serialize(input) ?? throw new NullReferenceException($"{nameof(PhpSerializer)}.{nameof(Serialize)} returned null"); } - /// - /// Reset the type lookup cache. - /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. - /// + // /// + // /// Reset the type lookup cache. + // /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. + // /// public static void ClearTypeCache() => PhpDeserializer.ClearTypeCache(); - /// - /// Reset the property info cache. - /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. - /// + // /// + // /// Reset the property info cache. + // /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. + // /// public static void ClearPropertyInfoCache() => PhpDeserializer.ClearPropertyInfoCache(); } diff --git a/PhpSerializerNET/PhpSerializeToken.cs b/PhpSerializerNET/PhpSerializeToken.cs index 747669d..f7761a2 100644 --- a/PhpSerializerNET/PhpSerializeToken.cs +++ b/PhpSerializerNET/PhpSerializeToken.cs @@ -1,10 +1,15 @@ /** - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ + +using System.Collections.Generic; +using System.Linq; namespace PhpSerializerNET; +#nullable enable + /// /// PHP Serialization format token. Holds type, length, position (of the token in the input string) and child information. /// @@ -12,5 +17,15 @@ internal record struct PhpSerializeToken( PhpSerializerType Type, int Position, string Value, - PhpSerializeToken[] Children -); \ No newline at end of file + KeyValuePair[] Children +) { + internal bool ContainsObjects() { + if (this.Type == PhpSerializerType.Object) { + return true; + } + if (this.Children == null) { + return false; + } + return this.Children.Any(y => y.Key.ContainsObjects() || y.Value.ContainsObjects()); + } +} diff --git a/PhpSerializerNET/PhpTokenizer.cs b/PhpSerializerNET/PhpTokenizer.cs index 711f26d..3ae2535 100644 --- a/PhpSerializerNET/PhpTokenizer.cs +++ b/PhpSerializerNET/PhpTokenizer.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; @@ -205,19 +206,21 @@ private PhpSerializeToken GetObjectToken() { int propertyCount = this.GetLength(PhpSerializerType.Object); this.GetDelimiter(); this.GetBracketOpen(); - result.Children = new PhpSerializeToken[propertyCount * 2]; - int i = 0; - try { - while (this._input[this._position] != '}') { - result.Children[i++] = this.GetToken(); - } - } catch (System.IndexOutOfRangeException ex) { + result.Children = new KeyValuePair[propertyCount]; + int i; + for(i = 0; i < propertyCount; i++) { + result.Children[i] = new KeyValuePair( + this.GetToken(), + this.GetToken() + ); + } + if(this._input[this._position] != '}') { throw new DeserializationException( $"Object at position {result.Position} should have {propertyCount} properties, " + - $"but actually has {(int)((i + 1) / 2)} or more properties.", - ex + $"but actually has {propertyCount + 1} or more properties." ); } + this.GetBracketClose(); return result; } @@ -229,17 +232,18 @@ private PhpSerializeToken GetArrayToken() { int length = this.GetLength(PhpSerializerType.Array); this.GetDelimiter(); this.GetBracketOpen(); - result.Children = new PhpSerializeToken[length * 2]; - int i = 0; - try { - while (this._input[this._position] != '}') { - result.Children[i++] = this.GetToken(); - } - } catch (IndexOutOfRangeException ex) { + result.Children = new KeyValuePair[length]; + int i; + for(i = 0; i < length; i++) { + result.Children[i] = new KeyValuePair( + this.GetToken(), + this.GetToken() + ); + } + if(this._input[this._position] != '}') { throw new DeserializationException( $"Array at position {result.Position} should be of length {length}, " + - $"but actual length is {(int)((i + 1) / 2)} or more.", - ex + $"but actual length is {length + 1} or more." ); } this.GetBracketClose(); @@ -299,4 +303,4 @@ internal PhpSerializeToken Tokenize() { } return result; } -} \ No newline at end of file +}