From d6010720eb7b227e86f71302bb28527ecced1d05 Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Mon, 22 Jul 2024 23:08:22 +0200 Subject: [PATCH 01/11] Split the tokenizer into a validator and tokenizer. The main benefit is reduced memory allocations, as we now count the tokens during the validation step and then work off of a flat array without nesting children. I have also slightly loosened the coupling between tokenizer and deserializer. Lastly there has been some housekeeping with naming and file locations. --- .../Deserialize/ArrayDeserialization.cs | 1 + .../Options/EmptyStringToDefault.cs | 2 +- .../Deserialize/Options/UseLists.cs | 1 + .../DeserializationException.cs} | 0 .../Deserialization/PhpDataType.cs | 42 +++ .../{ => Deserialization}/PhpDeserializer.cs | 264 +++++++++--------- .../PhpDeserializiationOptions.cs | 0 PhpSerializerNET/Deserialization/PhpToken.cs | 22 ++ .../PhpTokenValidator.cs} | 219 +++++++-------- .../Deserialization/PhpTokenizer.cs | 213 ++++++++++++++ .../Extensions/ArrayExtensions.cs | 16 +- PhpSerializerNET/PhpSerialization.cs | 54 ++-- PhpSerializerNET/PhpSerializeToken.cs | 16 -- PhpSerializerNET/PhpSerializerNET.csproj | 2 +- PhpSerializerNET/PhpSerializerType.cs | 17 -- 15 files changed, 554 insertions(+), 315 deletions(-) rename PhpSerializerNET/{PhpSerializationException.cs => Deserialization/DeserializationException.cs} (100%) create mode 100644 PhpSerializerNET/Deserialization/PhpDataType.cs rename PhpSerializerNET/{ => Deserialization}/PhpDeserializer.cs (56%) rename PhpSerializerNET/{ => Deserialization}/PhpDeserializiationOptions.cs (100%) create mode 100644 PhpSerializerNET/Deserialization/PhpToken.cs rename PhpSerializerNET/{PhpTokenizer.cs => Deserialization/PhpTokenValidator.cs} (51%) create mode 100644 PhpSerializerNET/Deserialization/PhpTokenizer.cs delete mode 100644 PhpSerializerNET/PhpSerializeToken.cs delete mode 100644 PhpSerializerNET/PhpSerializerType.cs diff --git a/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs b/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs index e14705c..9ec6f95 100644 --- a/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs @@ -160,6 +160,7 @@ public void ExplicitToStructWrongField() { public void ExplicitToList() { var result = PhpSerialization.Deserialize>("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}"); + Assert.AreEqual(3, result.Count); CollectionAssert.AreEqual(new List() { "Hello", "World", "12345" }, result); } diff --git a/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs b/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs index 18a76df..436e35e 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs @@ -164,7 +164,7 @@ public void Enabled_StringArrayToIntList() { [TestMethod] public void Enabled_StringArrayToNullableIntList() { - var result = PhpSerialization.Deserialize>("a:1:{i:0;s:0:\"\";}"); + var result = PhpSerialization.Deserialize>("a:1:{i:1;s:0:\"\";}"); CollectionAssert.AreEqual(new List { default }, result); } diff --git a/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs b/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs index 73ed6c6..d74384e 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs @@ -52,6 +52,7 @@ public void Option_Default_NonConsequetive() { } ); + Assert.AreEqual(typeof (Dictionary), result.GetType()); var dictionary = result as Dictionary; Assert.IsNotNull(dictionary); Assert.AreEqual(2, dictionary.Count); diff --git a/PhpSerializerNET/PhpSerializationException.cs b/PhpSerializerNET/Deserialization/DeserializationException.cs similarity index 100% rename from PhpSerializerNET/PhpSerializationException.cs rename to PhpSerializerNET/Deserialization/DeserializationException.cs diff --git a/PhpSerializerNET/Deserialization/PhpDataType.cs b/PhpSerializerNET/Deserialization/PhpDataType.cs new file mode 100644 index 0000000..efc1ee5 --- /dev/null +++ b/PhpSerializerNET/Deserialization/PhpDataType.cs @@ -0,0 +1,42 @@ +/** + 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/. +**/ + +namespace PhpSerializerNET; + +/// +/// PHP data types that can be de/serialized. +/// +internal enum PhpDataType : byte { + /// + /// Null (N;) + /// + Null, + /// + /// Boolean value (b:n;) + /// + Boolean, + /// + /// Integer value (i:[value];) + /// + Integer, + /// + /// Floating point number (f:[value];) + /// + Floating, + /// + /// String (s:[length]:"[value]") + /// + String, + /// + /// Array (a:[length]:{[children]}) + /// + Array, + /// + /// Object (O:[identLength]:"[ident]":[length]:{[children]}) + /// + Object +} + diff --git a/PhpSerializerNET/PhpDeserializer.cs b/PhpSerializerNET/Deserialization/PhpDeserializer.cs similarity index 56% rename from PhpSerializerNET/PhpDeserializer.cs rename to PhpSerializerNET/Deserialization/PhpDeserializer.cs index 767eeea..98a1a0f 100644 --- a/PhpSerializerNET/PhpDeserializer.cs +++ b/PhpSerializerNET/Deserialization/PhpDeserializer.cs @@ -3,7 +3,6 @@ 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; @@ -13,70 +12,54 @@ This Source Code Form is subject to the terms of the Mozilla Public namespace PhpSerializerNET; -internal class PhpDeserializer { +internal ref struct PhpDeserializer { private readonly PhpDeserializationOptions _options; - private readonly PhpSerializeToken _token; + private readonly Span _tokens; + private int _currentToken = 0; - public PhpDeserializer(string input, PhpDeserializationOptions options) { + internal PhpDeserializer(Span tokens, PhpDeserializationOptions options) { _options = options; - if (_options == null) { - _options = PhpDeserializationOptions.DefaultOptions; - } - this._token = new PhpTokenizer(input, this._options.InputEncoding).Tokenize(); + _tokens = tokens; } - public object Deserialize() { - return this.DeserializeToken(this._token); + internal object Deserialize() { + return this.DeserializeToken(); } - public object Deserialize(Type targetType) { - return this.DeserializeToken(targetType, this._token); + internal object Deserialize(Type targetType) { + return this.DeserializeToken(targetType); } - public T Deserialize() { + internal 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(); - } - - /// - /// 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(); - } - - private object DeserializeToken(PhpSerializeToken token) { + private object DeserializeToken() { + var token = this._tokens[this._currentToken]; + this._currentToken++; switch (token.Type) { - case PhpSerializerType.Boolean: + case PhpDataType.Boolean: return token.Value.PhpToBool(); - case PhpSerializerType.Integer: + case PhpDataType.Integer: return token.Value.PhpToLong(); - case PhpSerializerType.Floating: + case PhpDataType.Floating: return token.Value.PhpToDouble(); - case PhpSerializerType.String: + case PhpDataType.String: if (this._options.NumberStringToBool && (token.Value == "0" || token.Value == "1")) { return token.Value.PhpToBool(); } return token.Value; - case PhpSerializerType.Array: + case PhpDataType.Array: return MakeCollection(token); - case PhpSerializerType.Object: + case PhpDataType.Object: return MakeClass(token); - case PhpSerializerType.Null: + case PhpDataType.Null: default: return null; } } - private object MakeClass(PhpSerializeToken token) { + private object MakeClass(PhpToken token) { var typeName = token.Value; object constructedObject; Type targetType = null; @@ -84,7 +67,7 @@ private object MakeClass(PhpSerializeToken token) { targetType = TypeLookup.FindTypeInAssymbly(typeName, this._options.TypeCache.HasFlag(TypeCacheFlag.ClassNames)); } if (targetType != null && typeName != "stdClass") { - constructedObject = this.DeserializeToken(targetType, token); + constructedObject = this.DeserializeToken(targetType); } else { dynamic result; if (_options.StdClass == StdClassOption.Dynamic) { @@ -94,10 +77,12 @@ private object MakeClass(PhpSerializeToken token) { } 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) { + for (int i = 0; i < token.Length; i++) { + var key = this.DeserializeToken(); + var value = this.DeserializeToken(); result.TryAdd( - token.Children[i].Value, - this.DeserializeToken(token.Children[i + 1]) + (string)key, + value ); } constructedObject = result; @@ -108,22 +93,22 @@ private object MakeClass(PhpSerializeToken token) { return constructedObject; } - private object DeserializeToken(Type targetType, PhpSerializeToken token) { + private object DeserializeToken(Type targetType) { if (targetType == null) { throw new ArgumentNullException(nameof(targetType)); } - + var token = this._tokens[this._currentToken]; + this._currentToken++; switch (token.Type) { - case PhpSerializerType.Boolean: { - return DeserializeBoolean(targetType, token); - } - case PhpSerializerType.Integer: + case PhpDataType.Boolean: + return DeserializeBoolean(targetType, token); + case PhpDataType.Integer: return DeserializeInteger(targetType, token); - case PhpSerializerType.Floating: + case PhpDataType.Floating: return DeserializeDouble(targetType, token); - case PhpSerializerType.String: - return DeserializeTokenFromSimpleType(targetType, token); - case PhpSerializerType.Object: { + case PhpDataType.String: + return DeserializeTokenFromSimpleType(targetType, token.Type, token.Value, token.Position); + case PhpDataType.Object: { object result; if (typeof(IDictionary).IsAssignableFrom(targetType)) { result = MakeDictionary(targetType, token); @@ -137,7 +122,7 @@ private object DeserializeToken(Type targetType, PhpSerializeToken token) { } return result; } - case PhpSerializerType.Array: { + case PhpDataType.Array: { if (targetType.IsAssignableTo(typeof(IList))) { return this.MakeList(targetType, token); } else if (targetType.IsAssignableTo(typeof(IDictionary))) { @@ -148,7 +133,7 @@ private object DeserializeToken(Type targetType, PhpSerializeToken token) { return this.MakeStruct(targetType, token); } } - case PhpSerializerType.Null: + case PhpDataType.Null: default: if (targetType.IsValueType) { return Activator.CreateInstance(targetType); @@ -158,7 +143,7 @@ private object DeserializeToken(Type targetType, PhpSerializeToken token) { } } - private object DeserializeInteger(Type targetType, PhpSerializeToken token) { + private object DeserializeInteger(Type targetType, PhpToken token) { return Type.GetTypeCode(targetType) switch { TypeCode.Int16 => short.Parse(token.Value), TypeCode.Int32 => int.Parse(token.Value), @@ -167,24 +152,24 @@ private object DeserializeInteger(Type targetType, PhpSerializeToken token) { TypeCode.UInt32 => uint.Parse(token.Value), TypeCode.UInt64 => ulong.Parse(token.Value), TypeCode.SByte => sbyte.Parse(token.Value), - _ => this.DeserializeTokenFromSimpleType(targetType, token), + _ => this.DeserializeTokenFromSimpleType(targetType, token.Type, token.Value, token.Position), }; } - private object DeserializeDouble(Type targetType, PhpSerializeToken token) { + private object DeserializeDouble(Type targetType, PhpToken token) { if (targetType == typeof(double) || targetType == typeof(float)) { return token.Value.PhpToDouble(); } - token.Value = token.Value switch { + string value = token.Value switch { "INF" => double.PositiveInfinity.ToString(CultureInfo.InvariantCulture), "-INF" => double.NegativeInfinity.ToString(CultureInfo.InvariantCulture), _ => token.Value, }; - return this.DeserializeTokenFromSimpleType(targetType, token); + return this.DeserializeTokenFromSimpleType(targetType, token.Type, value, token.Position); } - private static object DeserializeBoolean(Type targetType, PhpSerializeToken token) { + private static object DeserializeBoolean(Type targetType, PhpToken token) { if (targetType == typeof(bool) || targetType == typeof(bool?)) { return token.Value.PhpToBool(); } @@ -202,41 +187,45 @@ private static object DeserializeBoolean(Type targetType, PhpSerializeToken toke } } - private object DeserializeTokenFromSimpleType(Type givenType, PhpSerializeToken token) { - var targetType = givenType; + private object DeserializeTokenFromSimpleType( + Type targetType, + PhpDataType dataType, + string value, + int tokenPosition + ) { if (!targetType.IsPrimitive && targetType.IsNullableReferenceType()) { - if (token.Value == "" && _options.EmptyStringToDefault) { + if (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); + throw new NullReferenceException("Could not get underlying type for nullable reference type " + targetType); } } // Short-circuit strings: if (targetType == typeof(string)) { - return token.Value == "" && _options.EmptyStringToDefault + return value == "" && _options.EmptyStringToDefault ? default - : token.Value; + : 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) { + if (value == "" && this._options.EmptyStringToDefault) { return Activator.CreateInstance(targetType); } - if (token.Type != PhpSerializerType.String) { - return Enum.Parse(targetType, token.Value); + if (dataType != PhpDataType.String) { + return Enum.Parse(targetType, value); } - FieldInfo foundFieldInfo = TypeLookup.GetEnumInfo(targetType, token.Value, this._options); + FieldInfo foundFieldInfo = TypeLookup.GetEnumInfo(targetType, value, this._options); if (foundFieldInfo == null) { throw new DeserializationException( - $"Exception encountered while trying to assign '{token.Value}' to type '{targetType.Name}'. " + + $"Exception encountered while trying to assign '{value}' to type '{targetType.Name}'. " + $"The value could not be matched to an enum member."); } @@ -244,52 +233,54 @@ private object DeserializeTokenFromSimpleType(Type givenType, PhpSerializeToken } if (targetType.IsIConvertible()) { - if (token.Value == "" && _options.EmptyStringToDefault) { + if (value == "" && _options.EmptyStringToDefault) { return Activator.CreateInstance(targetType); } if (targetType == typeof(bool)) { - if (_options.NumberStringToBool && token.Value is "0" or "1") { - return token.Value.PhpToBool(); + if (_options.NumberStringToBool && value is "0" or "1") { + return value.PhpToBool(); } } try { - return ((IConvertible)token.Value).ToType(targetType, CultureInfo.InvariantCulture); + return ((IConvertible)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 encountered while trying to assign '{value}' to type {targetType.Name}. See inner exception for details.", exception ); } } if (targetType == typeof(Guid)) { - return token.Value == "" && _options.EmptyStringToDefault + return value == "" && _options.EmptyStringToDefault ? default - : new Guid(token.Value); + : new Guid(value); } if (targetType == typeof(object)) { - return token.Value == "" && _options.EmptyStringToDefault + return value == "" && _options.EmptyStringToDefault ? default - : token.Value; + : value; } - throw new DeserializationException($"Can not assign value \"{token.Value}\" (at position {token.Position}) to target type of {targetType.Name}."); + throw new DeserializationException($"Can not assign value \"{value}\" (at position {tokenPosition}) to target type of {targetType.Name}."); } - private object MakeStruct(Type targetType, PhpSerializeToken token) { + private object MakeStruct(Type targetType, PhpToken token) { var result = Activator.CreateInstance(targetType); Dictionary fields = TypeLookup.GetFieldInfos(targetType, this._options); - 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]; + for (int i = 0; i < token.Length; i++) { + var fieldName = this._options.CaseSensitiveProperties + ? this._tokens[this._currentToken++].Value + : this._tokens[this._currentToken++].Value.ToLower(); + 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." + $"Could not bind the key \"{fieldName}\" to struct of type {targetType.Name}: No such field." ); } continue; @@ -297,8 +288,9 @@ private object MakeStruct(Type targetType, PhpSerializeToken token) { if (fields[fieldName] != null) { var field = fields[fieldName]; try { - field.SetValue(result, DeserializeToken(field.FieldType, valueToken)); + field.SetValue(result, DeserializeToken(field.FieldType)); } catch (Exception exception) { + var valueToken = this._tokens[this._currentToken]; throw new DeserializationException( $"Exception encountered while trying to assign '{valueToken.Value}' to {targetType.Name}.{field.Name}. " + "See inner exception for details.", @@ -310,41 +302,43 @@ private object MakeStruct(Type targetType, PhpSerializeToken token) { return result; } - private object MakeObject(Type targetType, PhpSerializeToken token) { + private object MakeObject(Type targetType, PhpToken token) { var result = Activator.CreateInstance(targetType); Dictionary properties = TypeLookup.GetPropertyInfos(targetType, this._options); - for (int i = 0; i < token.Children.Length; i += 2) { + for (int i = 0; i < token.Length; i++) { 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(); + var nameToken = this._tokens[_currentToken]; + if (nameToken.Type == PhpDataType.String) { + propertyName = this._options.CaseSensitiveProperties + ? nameToken.Value + : nameToken.Value.ToLower(); + } else if (nameToken.Type == PhpDataType.Integer) { + propertyName = nameToken.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}'." + $"The key '{nameToken.Value}' (from the token at position {nameToken.Position}) has an unsupported type of '{nameToken.Type}'." ); } - - var valueToken = token.Children[i + 1]; - 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." + $"Could not bind the key \"{propertyName}\" to object of type {targetType.Name}: No such property." ); } continue; } + _currentToken++; var property = properties[propertyName]; if (property != null) { // null if PhpIgnore'd try { property.SetValue( result, - DeserializeToken(property.PropertyType, valueToken) + DeserializeToken(property.PropertyType) ); } catch (Exception exception) { + var valueToken = _tokens[_currentToken-1]; throw new DeserializationException( $"Exception encountered while trying to assign '{valueToken.Value}' to {targetType.Name}.{property.Name}. See inner exception for details.", exception @@ -355,16 +349,17 @@ private object MakeObject(Type targetType, PhpSerializeToken token) { return result; } - private object MakeArray(Type targetType, PhpSerializeToken token) { + private object MakeArray(Type targetType, PhpToken token) { var elementType = targetType.GetElementType() ?? throw new InvalidOperationException("targetType.GetElementType() returned null"); - Array result = Array.CreateInstance(elementType, token.Children.Length / 2); + Array result = Array.CreateInstance(elementType, token.Length); var arrayIndex = 0; - for (int i = 1; i < token.Children.Length; i += 2) { + for (int i = 0; i < token.Length; i++) { + _currentToken++; result.SetValue( elementType == typeof(object) - ? DeserializeToken(token.Children[i]) - : DeserializeToken(elementType, token.Children[i]), + ? DeserializeToken() + : DeserializeToken(elementType), arrayIndex ); arrayIndex++; @@ -372,12 +367,13 @@ private object MakeArray(Type targetType, PhpSerializeToken token) { 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) { + private object MakeList(Type targetType, PhpToken token) { + for (int i = 0; i < token.Length; i += 2) { + if (this._tokens[_currentToken+i].Type != PhpDataType.Integer) { + var badToken = this._tokens[_currentToken+i]; 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})." + $"It has a non-integer key '{badToken.Value}' at element {i} (position {badToken.Position})." ); } } @@ -389,33 +385,34 @@ private object MakeList(Type targetType, PhpSerializeToken token) { if (result == null) { throw new NullReferenceException("Activator.CreateInstance(targetType) returned null"); } - Type itemType = typeof(object); + Type itemType; if (targetType.GenericTypeArguments.Length >= 1) { itemType = targetType.GenericTypeArguments[0]; + } else { + itemType = typeof(object); } - for (int i = 1; i < token.Children.Length; i += 2) { + for (int i = 0; i < token.Length; i++) { + _currentToken++; result.Add( itemType == typeof(object) - ? DeserializeToken(token.Children[i]) - : DeserializeToken(itemType, token.Children[i]) + ? DeserializeToken() + : DeserializeToken(itemType) ); } return result; } - private object MakeDictionary(Type targetType, PhpSerializeToken token) { + private object MakeDictionary(Type targetType, PhpToken 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]; + for (int i = 0; i < token.Length; i++) { result.Add( - DeserializeToken(keyToken), - DeserializeToken(valueToken) + DeserializeToken(), + DeserializeToken() ); } return result; @@ -423,34 +420,32 @@ private object MakeDictionary(Type targetType, PhpSerializeToken token) { 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]; + for (int i = 0; i < token.Length; i++) { result.Add( keyType == typeof(object) - ? DeserializeToken(keyToken) - : DeserializeToken(keyType, keyToken), + ? DeserializeToken() + : DeserializeToken(keyType), valueType == typeof(object) - ? DeserializeToken(valueToken) - : DeserializeToken(valueType, valueToken) + ? DeserializeToken() + : DeserializeToken(valueType) ); } return result; } - private object MakeCollection(PhpSerializeToken token) { + private object MakeCollection(PhpToken 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) { + for (int i = 0; i < token.Length*2; i+=2) { + if (this._tokens[_currentToken+i].Type != PhpDataType.Integer) { isList = false; break; } else { - var key = token.Children[i].Value.PhpToLong(); + var key = this._tokens[_currentToken+i].Value.PhpToLong(); if (i == 0 || key == previousKey + 1) { previousKey = key; } else { @@ -460,17 +455,18 @@ private object MakeCollection(PhpSerializeToken token) { } if (!isList || (this._options.UseLists == ListOptions.Default && consecutive == false)) { var result = new Dictionary(); - for (int i = 0; i < token.Children.Length; i += 2) { + for (int i = 0; i < token.Length; i++) { result.Add( - this.DeserializeToken(token.Children[i]), - this.DeserializeToken(token.Children[i + 1]) + this.DeserializeToken(), + this.DeserializeToken() ); } return result; } else { var result = new List(); - for (int i = 1; i < token.Children.Length; i += 2) { - result.Add(this.DeserializeToken(token.Children[i])); + for (int i = 0; i < token.Length; i++) { + _currentToken++; + result.Add(this.DeserializeToken()); } return result; } diff --git a/PhpSerializerNET/PhpDeserializiationOptions.cs b/PhpSerializerNET/Deserialization/PhpDeserializiationOptions.cs similarity index 100% rename from PhpSerializerNET/PhpDeserializiationOptions.cs rename to PhpSerializerNET/Deserialization/PhpDeserializiationOptions.cs diff --git a/PhpSerializerNET/Deserialization/PhpToken.cs b/PhpSerializerNET/Deserialization/PhpToken.cs new file mode 100644 index 0000000..30301f0 --- /dev/null +++ b/PhpSerializerNET/Deserialization/PhpToken.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/. +**/ +namespace PhpSerializerNET; + +/// +/// PHP data token. Holds the type, position (in the input string), length and value. +/// +internal readonly struct PhpToken { + internal readonly PhpDataType Type; + internal readonly int Position; + internal readonly int Length; + internal readonly string Value; + internal PhpToken(PhpDataType type, int position, string value = "", int length = 0) { + this.Type = type; + this.Position = position; + this.Value = value; + this.Length = length; + } +} \ No newline at end of file diff --git a/PhpSerializerNET/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs similarity index 51% rename from PhpSerializerNET/PhpTokenizer.cs rename to PhpSerializerNET/Deserialization/PhpTokenValidator.cs index 711f26d..c60624e 100644 --- a/PhpSerializerNET/PhpTokenizer.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs @@ -5,31 +5,20 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; namespace PhpSerializerNET; -public class PhpTokenizer { - +internal ref struct PhpTokenValidator { private int _position; - private readonly Encoding _inputEncoding; - - private readonly byte[] _input; + private int _tokenCount = 0; + private readonly ReadOnlySpan _input; private readonly int _lastIndex; -#if DEBUG - private char DebugCurrentCharacter => (char)_input[_position]; - private char[] DebugInput => _inputEncoding.GetChars(_input); -#endif - - public PhpTokenizer(string input, Encoding inputEncoding) { - this._inputEncoding = inputEncoding; - this._input = Encoding.Convert( - Encoding.Default, - this._inputEncoding, - Encoding.Default.GetBytes(input) - ); + internal PhpTokenValidator(in ReadOnlySpan input) { + this._input = input; this._position = 0; this._lastIndex = this._input.Length - 1; } @@ -53,15 +42,15 @@ private void CheckBounds(char expectation) { } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private PhpSerializerType GetDataType() { - var result = (char)this._input[this._position] switch { - 'N' => PhpSerializerType.Null, - 'b' => PhpSerializerType.Boolean, - 's' => PhpSerializerType.String, - 'i' => PhpSerializerType.Integer, - 'd' => PhpSerializerType.Floating, - 'a' => PhpSerializerType.Array, - 'O' => PhpSerializerType.Object, + private PhpDataType GetDataType() { + var result = this._input[this._position] switch { + (byte)'N' => PhpDataType.Null, + (byte)'b' => PhpDataType.Boolean, + (byte)'s' => PhpDataType.String, + (byte)'i' => PhpDataType.Integer, + (byte)'d' => PhpDataType.Floating, + (byte)'a' => PhpDataType.Array, + (byte)'O' => PhpDataType.Object, _ => throw new DeserializationException($"Unexpected token '{(char)this._input[this._position]}' at position {this._position}.") }; this._position++; @@ -78,19 +67,18 @@ private void GetCharacter(char character) { this._position++; } - // [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetTerminator() { this.GetCharacter(';'); } - // [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetDelimiter() { this.GetCharacter(':'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetNumbers(bool isFloating) { + private void GetNumbers(bool isFloating) { bool valid = true; - int start = this._position; int end = this._position; for (; this._input[this._position] != ';' && this._position < this._lastIndex && valid; this._position++) { @@ -118,12 +106,11 @@ private string GetNumbers(bool isFloating) { $"Unexpected end of input. Expected ':' at index {this._position}, but input ends at index {this._lastIndex}" ); } - return this._input.Utf8Substring(start, end - start, this._inputEncoding); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetLength(PhpSerializerType dataType) { + private int GetLength(PhpDataType dataType) { int length = 0; for (; this._input[this._position] != ':' && this._position < this._lastIndex; this._position++) { @@ -137,166 +124,162 @@ private int GetLength(PhpSerializerType dataType) { return length; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetBoolean() { - this.CheckBounds("0' or '1"); + private void GetBoolean() { - string result = (char)this._input[this._position] switch { - '1' => "1", - '0' => "0", - _ => throw new DeserializationException( - $"Unexpected token in boolean at index {this._position}. Expected either '1' or '0', but found '{(char)this._input[this._position]}' instead." - ) - }; + this.CheckBounds("0' or '1"); + var item = this._input[this._position]; + if (item != 48 && item != 49) { + throw new DeserializationException( + $"Unexpected token in boolean at index {this._position}. Expected either '1' or '0', but found '{(char)item}' instead." + ); + } this._position++; - return result; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetBracketClose() { this.GetCharacter('}'); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetBracketOpen() { this.GetCharacter('{'); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetNCharacters(int length) { + private void GetNCharacters(int length) { if (this._position + length > this._lastIndex) { throw new DeserializationException( $"Illegal length of {length}. The string at position {this._position} points to out of bounds index {this._position + length}." ); } - int start = this._position; this._position += length; - return this._input.Utf8Substring(start, length, this._inputEncoding); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal PhpSerializeToken GetToken() { - return this.GetDataType() switch { - PhpSerializerType.Boolean => this.GetBooleanToken(), - PhpSerializerType.Null => this.GetNullToken(), - PhpSerializerType.String => this.GetStringToken(), - PhpSerializerType.Integer => this.GetIntegerToken(), - PhpSerializerType.Floating => this.GetFloatingToken(), - PhpSerializerType.Array => this.GetArrayToken(), - PhpSerializerType.Object => this.GetObjectToken(), - _ => new PhpSerializeToken() + internal void GetToken() { + switch (this.GetDataType()) { + case PhpDataType.Boolean: + this.GetBooleanToken(); + break; + case PhpDataType.Null: + this.GetNullToken(); + break; + case PhpDataType.String: + this.GetStringToken(); + break; + case PhpDataType.Integer: + this.GetIntegerToken(); + break; + case PhpDataType.Floating: + this.GetFloatingToken(); + break; + case PhpDataType.Array: + this.GetArrayToken(); + break; + case PhpDataType.Object: + this.GetObjectToken(); + break; }; + this._tokenCount++; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetObjectToken() { - var result = new PhpSerializeToken() { - Type = PhpSerializerType.Object, - Position = _position - 1, - }; + private void GetObjectToken() { + int position = _position - 1; this.GetDelimiter(); - int classNamelength = this.GetLength(PhpSerializerType.Object); + int classNamelength = this.GetLength(PhpDataType.Object); this.GetDelimiter(); this.GetCharacter('"'); - result.Value = this.GetNCharacters(classNamelength); + this.GetNCharacters(classNamelength); this.GetCharacter('"'); this.GetDelimiter(); - int propertyCount = this.GetLength(PhpSerializerType.Object); + int propertyCount = this.GetLength(PhpDataType.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(); + while (this._input[this._position] != '}') { + this.GetToken(); + i++; + if (i > propertyCount*2) { + throw new DeserializationException( + $"Object at position {position} should have {propertyCount} properties, " + + $"but actually has {(i + 1) / 2} or more properties." + ); } - } catch (System.IndexOutOfRangeException ex) { - throw new DeserializationException( - $"Object at position {result.Position} should have {propertyCount} properties, " + - $"but actually has {(int)((i + 1) / 2)} or more properties.", - ex - ); } + this.GetBracketClose(); - return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetArrayToken() { - var result = new PhpSerializeToken() { Type = PhpSerializerType.Array, Position = _position - 1 }; + private void GetArrayToken() { + int position = _position - 1; this.GetDelimiter(); - int length = this.GetLength(PhpSerializerType.Array); + int length = this.GetLength(PhpDataType.Array); this.GetDelimiter(); this.GetBracketOpen(); - result.Children = new PhpSerializeToken[length * 2]; + int maxTokenCount = length * 2; int i = 0; - try { - while (this._input[this._position] != '}') { - result.Children[i++] = this.GetToken(); + while (this._input[this._position] != '}') { + this.GetToken(); + i++; + if (i > maxTokenCount) { + throw new DeserializationException( + $"Array at position {position} should be of length {length}, " + + $"but actual length is {(int)((i + 1) / 2)} or more." + ); } - } catch (IndexOutOfRangeException ex) { - throw new DeserializationException( - $"Array at position {result.Position} should be of length {length}, " + - $"but actual length is {(int)((i + 1) / 2)} or more.", - ex - ); } this.GetBracketClose(); - return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetFloatingToken() { - var result = new PhpSerializeToken() { Type = PhpSerializerType.Floating, Position = _position - 1 }; + private void GetFloatingToken() { this.GetDelimiter(); - result.Value = this.GetNumbers(true); + this.GetNumbers(true); this.GetTerminator(); - return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetIntegerToken() { - var result = new PhpSerializeToken() { Type = PhpSerializerType.Integer, Position = _position - 1 }; + private void GetIntegerToken() { this.GetDelimiter(); - result.Value = this.GetNumbers(false); + this.GetNumbers(false); this.GetTerminator(); - return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetStringToken() { - var result = new PhpSerializeToken() { Type = PhpSerializerType.String, Position = _position - 1 }; + private void GetStringToken() { this.GetDelimiter(); - int length = this.GetLength(result.Type); + int length = this.GetLength(PhpDataType.String); this.GetDelimiter(); this.GetCharacter('"'); - result.Value = this.GetNCharacters(length); + this.GetNCharacters(length); this.GetCharacter('"'); this.GetTerminator(); - return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetNullToken() { + private void GetNullToken() { this.GetTerminator(); - return new PhpSerializeToken() { Type = PhpSerializerType.Null, Position = _position - 2 }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private PhpSerializeToken GetBooleanToken() { - var result = new PhpSerializeToken() { Type = PhpSerializerType.Boolean, Position = _position - 1 }; + private void GetBooleanToken() { this.GetDelimiter(); - result.Value = this.GetBoolean(); + this.GetBoolean(); this.GetTerminator(); - return result; } - internal PhpSerializeToken Tokenize() { - var result = this.GetToken(); - if (this._position <= this._lastIndex) { - throw new DeserializationException($"Unexpected token '{(char)this._input[this._position]}' at position {this._position}."); + /// + /// Validate the PHP data and return the number of tokens found. + /// + /// The raw UTF8 bytes of PHP data to validate. + /// The number of tokens found. + /// + internal static int Validate(ReadOnlySpan input) { + var validatior = new PhpTokenValidator(input); + validatior.GetToken(); + if (validatior._position <= validatior._lastIndex) { + throw new DeserializationException($"Unexpected token '{(char)input[validatior._position]}' at position {validatior._position}."); } - return result; + return validatior._tokenCount; } -} \ No newline at end of file +} diff --git a/PhpSerializerNET/Deserialization/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenizer.cs new file mode 100644 index 0000000..8b83834 --- /dev/null +++ b/PhpSerializerNET/Deserialization/PhpTokenizer.cs @@ -0,0 +1,213 @@ +/** + 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.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace PhpSerializerNET; + +public ref struct PhpTokenizer { + private int _position; + private readonly Encoding _inputEncoding; + private Span _tokens; + private int _tokenPosition = 0; + + private readonly ReadOnlySpan _input; + + private PhpTokenizer(ReadOnlySpan input, Encoding inputEncoding, Span array) { + this._tokens = array; + this._inputEncoding = inputEncoding; + this._input = input; + this._position = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private PhpDataType GetDataType() { + var result = this._input[this._position] switch { + (byte)'N' => PhpDataType.Null, + (byte)'b' => PhpDataType.Boolean, + (byte)'s' => PhpDataType.String, + (byte)'i' => PhpDataType.Integer, + (byte)'d' => PhpDataType.Floating, + (byte)'a' => PhpDataType.Array, + (byte)'O' => PhpDataType.Object, + _ => throw new UnreachableException(), + }; + this._position++; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Advance() { + this._position++; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Advance(int positons) { + this._position += positons; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetNumbers() { + int start = this._position; + var span = this._input.Slice(this._position); + span = span.Slice(0, span.IndexOf((byte)';')); + this._position += span.Length; + return span.Utf8Substring(this._inputEncoding); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetLength() { + int length = 0; + for (; this._input[this._position] != ':'; this._position++) { + length = length * 10 + (_input[_position] - 48); + } + return length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetBoolean() { + string result = this._input[this._position] switch { + (byte)'1' => "1", + (byte)'0' => "0", + _ => throw new UnreachableException() + }; + this._position++; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetNCharacters(int length) { + int start = this._position; + this._position += length; + return this._input.Slice(start, length).Utf8Substring(this._inputEncoding); + } + + internal void GetToken() { + switch (this.GetDataType()) { + case PhpDataType.Boolean: + this.GetBooleanToken(); + break; + case PhpDataType.Null: + this.GetNullToken(); + break; + case PhpDataType.String: + this.GetStringToken(); + break; + case PhpDataType.Integer: + this.GetIntegerToken(); + break; + case PhpDataType.Floating: + this.GetFloatingToken(); + break; + case PhpDataType.Array: + this.GetArrayToken(); + break; + case PhpDataType.Object: + this.GetObjectToken(); + break; + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetNullToken() { + this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position-1); + this.Advance(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetBooleanToken() { + this.Advance(); + this._tokens[this._tokenPosition++] = new PhpToken( + PhpDataType.Boolean, + _position - 2, + this.GetBoolean(), + 0 + ); + this.Advance(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetStringToken() { + int position = _position -1; + this.Advance(); + int length = this.GetLength(); + this.Advance(2); + this._tokens[this._tokenPosition++] = new PhpToken( + PhpDataType.String, + position, + this.GetNCharacters(length) + ); + this.Advance(2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetIntegerToken() { + this.Advance(); + this._tokens[this._tokenPosition++] = new PhpToken( + PhpDataType.Integer, + this._position-2, + this.GetNumbers() + ); + this.Advance(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetFloatingToken() { + this.Advance(); + this._tokens[this._tokenPosition++] = new PhpToken( + PhpDataType.Floating, + this._position - 2, + this.GetNumbers() + ); + this.Advance(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetArrayToken() { + int position = _position - 1; + this.Advance(); + int length = this.GetLength(); + this._tokens[this._tokenPosition++] = new PhpToken( + PhpDataType.Array, + position, + "", + length + ); + this.Advance(2); + for (int i = 0; i < length * 2; i++) { + this.GetToken(); + } + this.Advance(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetObjectToken() { + int position = _position -1; + this.Advance(); + int classNamelength = this.GetLength(); + this.Advance(2); + string className = this.GetNCharacters(classNamelength); + this.Advance(2); + int propertyCount = this.GetLength(); + this._tokens[this._tokenPosition++] = new PhpToken( + PhpDataType.Object, + position, + className, + propertyCount + ); + this.Advance(2); + for (int i = 0; i < propertyCount * 2; i++) { + this.GetToken(); + } + this.Advance(); + } + + internal static void Tokenize(ReadOnlySpan inputBytes, Encoding inputEncoding, Span tokens) { + new PhpTokenizer(inputBytes, inputEncoding, tokens).GetToken(); + } +} \ No newline at end of file diff --git a/PhpSerializerNET/Extensions/ArrayExtensions.cs b/PhpSerializerNET/Extensions/ArrayExtensions.cs index fb948c8..86be9e4 100644 --- a/PhpSerializerNET/Extensions/ArrayExtensions.cs +++ b/PhpSerializerNET/Extensions/ArrayExtensions.cs @@ -4,6 +4,7 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ +using System; using System.Collections.Generic; using System.Reflection; using System.Text; @@ -11,22 +12,15 @@ This Source Code Form is subject to the terms of the Mozilla Public namespace PhpSerializerNET; internal static class ArrayExtensions { - - public static string Utf8Substring(this byte[] array, int start, int length, Encoding encoding) { - if (length > array.Length - start) { + public static string Utf8Substring(this ReadOnlySpan array, Encoding encoding) { + if (array.Length == 0) { return ""; } - if (encoding == Encoding.UTF8) { - // Using the ReadonlySpan<> saves some copying: - return Encoding.UTF8.GetString(new System.ReadOnlySpan(array, start, length)); + return Encoding.UTF8.GetString(array); } else { // Sadly, Encoding.Convert does not accept a Span. - byte[] substring = new byte[length]; - System.Buffer.BlockCopy(array, start, substring, 0, length); - return Encoding.UTF8.GetString( - Encoding.Convert(encoding, Encoding.UTF8, substring) - ); + return Encoding.UTF8.GetString(Encoding.Convert(encoding, Encoding.UTF8, array.ToArray())); } } diff --git a/PhpSerializerNET/PhpSerialization.cs b/PhpSerializerNET/PhpSerialization.cs index 4d68796..0e1be75 100644 --- a/PhpSerializerNET/PhpSerialization.cs +++ b/PhpSerializerNET/PhpSerialization.cs @@ -9,10 +9,35 @@ This Source Code Form is subject to the terms of the Mozilla Public using System; using System.Collections.Generic; +using System.Reflection.Metadata.Ecma335; +using System.Text; namespace PhpSerializerNET; public static class PhpSerialization { + private static Span Tokenize(string input, Encoding inputEncoding) { + ReadOnlySpan inputBytes = Encoding.Convert( + Encoding.Default, + inputEncoding, + Encoding.Default.GetBytes(input) + ); + int tokenCount = PhpTokenValidator.Validate(inputBytes); + Span tokens = new PhpToken[tokenCount]; + PhpTokenizer.Tokenize(inputBytes, inputEncoding, tokens); + return tokens; + } + + /// + /// 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(); + + /// + /// 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(); /// /// Deserialize the given string into an object. @@ -37,7 +62,10 @@ public static class PhpSerialization { if (string.IsNullOrEmpty(input)) { throw new ArgumentOutOfRangeException(nameof(input), "PhpSerialization.Deserialize(): Parameter 'input' must not be null or empty."); } - return new PhpDeserializer(input, options).Deserialize(); + if (options == null) { + options = PhpDeserializationOptions.DefaultOptions; + } + return new PhpDeserializer(Tokenize(input, options.InputEncoding), options).Deserialize(); } /// @@ -63,7 +91,10 @@ public static T Deserialize( if (string.IsNullOrEmpty(input)) { throw new ArgumentOutOfRangeException(nameof(input), "PhpSerialization.Deserialize(): Parameter 'input' must not be null or empty."); } - return new PhpDeserializer(input, options).Deserialize(); + if (options == null) { + options = PhpDeserializationOptions.DefaultOptions; + } + return new PhpDeserializer(Tokenize(input, options.InputEncoding), options).Deserialize(); } /// @@ -93,7 +124,10 @@ public static T Deserialize( if (string.IsNullOrEmpty(input)) { throw new ArgumentOutOfRangeException(nameof(input), "PhpSerialization.Deserialize(): Parameter 'input' must not be null or empty."); } - return new PhpDeserializer(input, options).Deserialize(type); + if (options == null) { + options = PhpDeserializationOptions.DefaultOptions; + } + return new PhpDeserializer(Tokenize(input, options.InputEncoding), options).Deserialize(type); } /// @@ -112,18 +146,4 @@ public static string Serialize(object? input, PhpSerializiationOptions? options return new PhpSerializer(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. - /// - 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. - /// - public static void ClearPropertyInfoCache() => - PhpDeserializer.ClearPropertyInfoCache(); } diff --git a/PhpSerializerNET/PhpSerializeToken.cs b/PhpSerializerNET/PhpSerializeToken.cs deleted file mode 100644 index 747669d..0000000 --- a/PhpSerializerNET/PhpSerializeToken.cs +++ /dev/null @@ -1,16 +0,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/. -**/ -namespace PhpSerializerNET; - -/// -/// PHP Serialization format token. Holds type, length, position (of the token in the input string) and child information. -/// -internal record struct PhpSerializeToken( - PhpSerializerType Type, - int Position, - string Value, - PhpSerializeToken[] Children -); \ No newline at end of file diff --git a/PhpSerializerNET/PhpSerializerNET.csproj b/PhpSerializerNET/PhpSerializerNET.csproj index 7227b38..63994d6 100644 --- a/PhpSerializerNET/PhpSerializerNET.csproj +++ b/PhpSerializerNET/PhpSerializerNET.csproj @@ -1,7 +1,7 @@ PhpSerializerNET - net6.0;net7.0;net8.0 + net8.0 10.0 1.4.0 StringEpsilon diff --git a/PhpSerializerNET/PhpSerializerType.cs b/PhpSerializerNET/PhpSerializerType.cs deleted file mode 100644 index 1434411..0000000 --- a/PhpSerializerNET/PhpSerializerType.cs +++ /dev/null @@ -1,17 +0,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/. -**/ - -namespace PhpSerializerNET; - -internal enum PhpSerializerType { - Null, - Boolean, - Integer, - Floating, - String, - Array, - Object -} From 1792a1e269de8134ddbc48ab0f053ebfdc89d37f Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Tue, 23 Jul 2024 00:51:34 +0200 Subject: [PATCH 02/11] Made tests go green again. --- PhpSerializerNET/Deserialization/PhpDeserializer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PhpSerializerNET/Deserialization/PhpDeserializer.cs b/PhpSerializerNET/Deserialization/PhpDeserializer.cs index 98a1a0f..492d675 100644 --- a/PhpSerializerNET/Deserialization/PhpDeserializer.cs +++ b/PhpSerializerNET/Deserialization/PhpDeserializer.cs @@ -67,6 +67,8 @@ private object MakeClass(PhpToken token) { targetType = TypeLookup.FindTypeInAssymbly(typeName, this._options.TypeCache.HasFlag(TypeCacheFlag.ClassNames)); } if (targetType != null && typeName != "stdClass") { + _currentToken--; // go back one because we're basically re-entering the object-token from the top. + // If we don't decrement the pointer, we'd start with the first child token instead of the object token. constructedObject = this.DeserializeToken(targetType); } else { dynamic result; @@ -308,7 +310,7 @@ private object MakeObject(Type targetType, PhpToken token) { for (int i = 0; i < token.Length; i++) { object propertyName; - var nameToken = this._tokens[_currentToken]; + var nameToken = this._tokens[_currentToken++]; if (nameToken.Type == PhpDataType.String) { propertyName = this._options.CaseSensitiveProperties ? nameToken.Value @@ -327,9 +329,9 @@ private object MakeObject(Type targetType, PhpToken token) { $"Could not bind the key \"{propertyName}\" to object of type {targetType.Name}: No such property." ); } + _currentToken++; continue; } - _currentToken++; var property = properties[propertyName]; if (property != null) { // null if PhpIgnore'd try { @@ -344,6 +346,8 @@ private object MakeObject(Type targetType, PhpToken token) { exception ); } + } else { + _currentToken++; } } return result; From 2de1f7a0146420b54f7f25172bec9045622cf20e Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Tue, 23 Jul 2024 20:27:20 +0200 Subject: [PATCH 03/11] Reclaim some of the lost performance. --- .../Deserialization/PhpTokenValidator.cs | 252 ++++++++---------- .../Deserialization/PhpTokenizer.cs | 47 ++-- .../Extensions/ArrayExtensions.cs | 16 +- 3 files changed, 128 insertions(+), 187 deletions(-) diff --git a/PhpSerializerNET/Deserialization/PhpTokenValidator.cs b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs index c60624e..b96674f 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenValidator.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs @@ -5,9 +5,7 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; -using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Text; namespace PhpSerializerNET; @@ -23,27 +21,8 @@ internal PhpTokenValidator(in ReadOnlySpan input) { this._lastIndex = this._input.Length - 1; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckBounds(string expectation) { - if (this._lastIndex < this._position) { - throw new DeserializationException( - $"Unexpected end of input. Expected '{expectation}' at index {this._position}, but input ends at index {this._lastIndex}" - ); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckBounds(char expectation) { - if (this._lastIndex < this._position) { - throw new DeserializationException( - $"Unexpected end of input. Expected '{expectation}' at index {this._position}, but input ends at index {this._lastIndex}" - ); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private PhpDataType GetDataType() { - var result = this._input[this._position] switch { + internal void GetToken() { + PhpDataType dataType = this._input[this._position++] switch { (byte)'N' => PhpDataType.Null, (byte)'b' => PhpDataType.Boolean, (byte)'s' => PhpDataType.String, @@ -51,48 +30,80 @@ private PhpDataType GetDataType() { (byte)'d' => PhpDataType.Floating, (byte)'a' => PhpDataType.Array, (byte)'O' => PhpDataType.Object, - _ => throw new DeserializationException($"Unexpected token '{(char)this._input[this._position]}' at position {this._position}.") + _ => throw new DeserializationException($"Unexpected token '{this.GetCharAt(this._position - 1)}' at position {this._position - 1}.") + }; + switch (dataType) { + case PhpDataType.Boolean: + this.GetCharacter(':'); + this.GetBoolean(); + this.GetCharacter(';'); + break; + case PhpDataType.Null: + this.GetCharacter(';'); + break; + case PhpDataType.String: + this.GetCharacter(':'); + int length = this.GetLength(PhpDataType.String); + this.GetCharacter(':'); + this.GetCharacter('"'); + this.GetNCharacters(length); + this.GetCharacter('"'); + this.GetCharacter(';'); + break; + case PhpDataType.Integer: + this.GetCharacter(':'); + this.GetInteger(); + this.GetCharacter(';'); + break; + case PhpDataType.Floating: + this.GetCharacter(':'); + this.GetFloat(); + this.GetCharacter(';'); + break; + case PhpDataType.Array: + this.GetArrayToken(); + break; + case PhpDataType.Object: + this.GetObjectToken(); + break; }; - this._position++; - return result; + this._tokenCount++; + } + + private char GetCharAt(int position) { + return (char)this._input[position]; } private void GetCharacter(char character) { - this.CheckBounds(character); - if (this._input[this._position] != character) { + if (this._lastIndex < this._position) { throw new DeserializationException( - $"Unexpected token at index {this._position}. Expected '{character}' but found '{(char)this._input[this._position]}' instead." + $"Unexpected end of input. Expected '{character}' at index {this._position}, but input ends at index {this._lastIndex}" + ); + } + if (this._input[this._position++] != character) { + throw new DeserializationException( + $"Unexpected token at index {this._position - 1}. Expected '{character}' but found '{this.GetCharAt(this._position - 1)}' instead." ); } - this._position++; - } - - - private void GetTerminator() { - this.GetCharacter(';'); - } - - private void GetDelimiter() { - this.GetCharacter(':'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetNumbers(bool isFloating) { + private void GetFloat() { bool valid = true; int end = this._position; for (; this._input[this._position] != ';' && this._position < this._lastIndex && valid; this._position++) { - _ = (char)this._input[this._position] switch { - >= '0' and <= '9' => true, - '+' => true, - '-' => true, - '.' => isFloating, - 'E' or 'e' => isFloating, // exponents. - 'I' or 'N' or 'F' => isFloating, // infinity. - 'N' or 'A' => isFloating, // NaN. + _ = this._input[this._position] switch { + >= (byte)'0' and <= (byte)'9' => true, + (byte)'+' => true, + (byte)'-' => true, + (byte)'.' => true, + (byte)'E' or (byte)'e' => true, // exponents. + (byte)'I' or (byte)'F' => true, // infinity. + (byte)'N' or (byte)'A' => true, // NaN. _ => throw new DeserializationException( $"Unexpected token at index {this._position}. " + - $"'{(char)this._input[this._position]}' is not a valid part of a {(isFloating ? "floating point " : "")}number." + $"'{(char)this._input[this._position]}' is not a valid part of a floating point number." ), }; end++; @@ -107,15 +118,39 @@ private void GetNumbers(bool isFloating) { ); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GetInteger() { + int end = this._position; + for (; this._input[this._position] != ';' && this._position < this._lastIndex; this._position++) { + _ = this._input[this._position] switch { + >= (byte)'0' and <= (byte)'9' => true, + (byte)'+' => true, + (byte)'-' => true, + _ => throw new DeserializationException( + $"Unexpected token at index {this._position}. " + + $"'{(char)this._input[this._position]}' is not a valid part of a number." + ), + }; + end++; + } + this._position = end; + + // Edgecase: input ends here without a delimeter following. Normal handling would give a misleading exception: + if (this._lastIndex == this._position && this._input[this._position] != (byte)';') { + throw new DeserializationException( + $"Unexpected end of input. Expected ':' at index {this._position}, but input ends at index {this._lastIndex}" + ); + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetLength(PhpDataType dataType) { int length = 0; for (; this._input[this._position] != ':' && this._position < this._lastIndex; this._position++) { - length = (char)this._input[this._position] switch { - >= '0' and <= '9' => length * 10 + (_input[_position] - 48), + length = this._input[this._position] switch { + >= (byte)'0' and <= (byte)'9' => length * 10 + (_input[_position] - 48), _ => throw new DeserializationException( $"{dataType} at position {this._position} has illegal, missing or malformed length." ), @@ -124,26 +159,23 @@ private int GetLength(PhpDataType dataType) { return length; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetBoolean() { - - this.CheckBounds("0' or '1"); - var item = this._input[this._position]; - if (item != 48 && item != 49) { + if (this._lastIndex < this._position) { throw new DeserializationException( - $"Unexpected token in boolean at index {this._position}. Expected either '1' or '0', but found '{(char)item}' instead." + $"Unexpected end of input. Expected '0' or '1' at index {this._position}, but input ends at index {this._lastIndex}" + ); + } + var item = this._input[this._position++]; + if (item != 48 && item != 49) { + throw new DeserializationException( + $"Unexpected token in boolean at index {this._position - 1}. " + + $"Expected either '1' or '0', but found '{(char)item}' instead." ); } - this._position++; - } - - private void GetBracketClose() { - this.GetCharacter('}'); - } - - private void GetBracketOpen() { - this.GetCharacter('{'); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetNCharacters(int length) { if (this._position + length > this._lastIndex) { throw new DeserializationException( @@ -153,69 +185,40 @@ private void GetNCharacters(int length) { this._position += length; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void GetToken() { - switch (this.GetDataType()) { - case PhpDataType.Boolean: - this.GetBooleanToken(); - break; - case PhpDataType.Null: - this.GetNullToken(); - break; - case PhpDataType.String: - this.GetStringToken(); - break; - case PhpDataType.Integer: - this.GetIntegerToken(); - break; - case PhpDataType.Floating: - this.GetFloatingToken(); - break; - case PhpDataType.Array: - this.GetArrayToken(); - break; - case PhpDataType.Object: - this.GetObjectToken(); - break; - }; - this._tokenCount++; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetObjectToken() { int position = _position - 1; - this.GetDelimiter(); + this.GetCharacter(':'); int classNamelength = this.GetLength(PhpDataType.Object); - this.GetDelimiter(); + this.GetCharacter(':'); this.GetCharacter('"'); this.GetNCharacters(classNamelength); this.GetCharacter('"'); - this.GetDelimiter(); + this.GetCharacter(':'); int propertyCount = this.GetLength(PhpDataType.Object); - this.GetDelimiter(); - this.GetBracketOpen(); + this.GetCharacter(':'); + this.GetCharacter('{'); int i = 0; while (this._input[this._position] != '}') { this.GetToken(); i++; - if (i > propertyCount*2) { + if (i > propertyCount * 2) { throw new DeserializationException( $"Object at position {position} should have {propertyCount} properties, " + $"but actually has {(i + 1) / 2} or more properties." ); } } - - this.GetBracketClose(); + this.GetCharacter('}'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetArrayToken() { int position = _position - 1; - this.GetDelimiter(); + this.GetCharacter(':'); int length = this.GetLength(PhpDataType.Array); - this.GetDelimiter(); - this.GetBracketOpen(); + this.GetCharacter(':'); + this.GetCharacter('{'); int maxTokenCount = length * 2; int i = 0; while (this._input[this._position] != '}') { @@ -224,48 +227,11 @@ private void GetArrayToken() { if (i > maxTokenCount) { throw new DeserializationException( $"Array at position {position} should be of length {length}, " + - $"but actual length is {(int)((i + 1) / 2)} or more." + $"but actual length is {(i + 1) / 2} or more." ); } } - this.GetBracketClose(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetFloatingToken() { - this.GetDelimiter(); - this.GetNumbers(true); - this.GetTerminator(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetIntegerToken() { - this.GetDelimiter(); - this.GetNumbers(false); - this.GetTerminator(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetStringToken() { - this.GetDelimiter(); - int length = this.GetLength(PhpDataType.String); - this.GetDelimiter(); - this.GetCharacter('"'); - this.GetNCharacters(length); - this.GetCharacter('"'); - this.GetTerminator(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetNullToken() { - this.GetTerminator(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void GetBooleanToken() { - this.GetDelimiter(); - this.GetBoolean(); - this.GetTerminator(); + this.GetCharacter('}'); } /// diff --git a/PhpSerializerNET/Deserialization/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenizer.cs index 8b83834..60de68a 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenizer.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenizer.cs @@ -12,23 +12,23 @@ This Source Code Form is subject to the terms of the Mozilla Public namespace PhpSerializerNET; public ref struct PhpTokenizer { - private int _position; private readonly Encoding _inputEncoding; - private Span _tokens; - private int _tokenPosition = 0; - private readonly ReadOnlySpan _input; + private Span _tokens; + private int _position; + private int _tokenPosition; private PhpTokenizer(ReadOnlySpan input, Encoding inputEncoding, Span array) { - this._tokens = array; this._inputEncoding = inputEncoding; this._input = input; + this._tokens = array; this._position = 0; + this._tokenPosition = 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private PhpDataType GetDataType() { - var result = this._input[this._position] switch { + return this._input[this._position++] switch { (byte)'N' => PhpDataType.Null, (byte)'b' => PhpDataType.Boolean, (byte)'s' => PhpDataType.String, @@ -38,8 +38,6 @@ private PhpDataType GetDataType() { (byte)'O' => PhpDataType.Object, _ => throw new UnreachableException(), }; - this._position++; - return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -54,10 +52,9 @@ private void Advance(int positons) { [MethodImpl(MethodImplOptions.AggressiveInlining)] private string GetNumbers() { int start = this._position; - var span = this._input.Slice(this._position); - span = span.Slice(0, span.IndexOf((byte)';')); - this._position += span.Length; - return span.Utf8Substring(this._inputEncoding); + int length = this._input.Slice(this._position).IndexOf((byte)';'); + this._position += length; + return this._inputEncoding.GetString(this._input.Slice(start, length)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -69,22 +66,10 @@ private int GetLength() { return length; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetBoolean() { - string result = this._input[this._position] switch { - (byte)'1' => "1", - (byte)'0' => "0", - _ => throw new UnreachableException() - }; - this._position++; - return result; - } [MethodImpl(MethodImplOptions.AggressiveInlining)] private string GetNCharacters(int length) { - int start = this._position; - this._position += length; - return this._input.Slice(start, length).Utf8Substring(this._inputEncoding); + return _inputEncoding.GetString(this._input.Slice(this._position, length)); } internal void GetToken() { @@ -125,7 +110,9 @@ private void GetBooleanToken() { this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Boolean, _position - 2, - this.GetBoolean(), + this._input[this._position++] == (byte)'1' + ? "1" + : "0", 0 ); this.Advance(); @@ -142,7 +129,7 @@ private void GetStringToken() { position, this.GetNCharacters(length) ); - this.Advance(2); + this.Advance(2+length); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -189,10 +176,10 @@ private void GetArrayToken() { private void GetObjectToken() { int position = _position -1; this.Advance(); - int classNamelength = this.GetLength(); - this.Advance(2); - string className = this.GetNCharacters(classNamelength); + int classNameLength = this.GetLength(); this.Advance(2); + string className = this.GetNCharacters(classNameLength); + this.Advance(2+classNameLength); int propertyCount = this.GetLength(); this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Object, diff --git a/PhpSerializerNET/Extensions/ArrayExtensions.cs b/PhpSerializerNET/Extensions/ArrayExtensions.cs index 86be9e4..a5e6552 100644 --- a/PhpSerializerNET/Extensions/ArrayExtensions.cs +++ b/PhpSerializerNET/Extensions/ArrayExtensions.cs @@ -12,19 +12,7 @@ This Source Code Form is subject to the terms of the Mozilla Public namespace PhpSerializerNET; internal static class ArrayExtensions { - public static string Utf8Substring(this ReadOnlySpan array, Encoding encoding) { - if (array.Length == 0) { - return ""; - } - if (encoding == Encoding.UTF8) { - return Encoding.UTF8.GetString(array); - } else { - // Sadly, Encoding.Convert does not accept a Span. - return Encoding.UTF8.GetString(Encoding.Convert(encoding, Encoding.UTF8, array.ToArray())); - } - } - - public static Dictionary GetAllProperties(this PropertyInfo[] properties, PhpDeserializationOptions options) { + internal static Dictionary GetAllProperties(this PropertyInfo[] properties, PhpDeserializationOptions options) { var result = new Dictionary(properties.Length); foreach (var property in properties) { var isIgnored = false; @@ -57,7 +45,7 @@ public static Dictionary GetAllProperties(this PropertyInf return result; } - public static Dictionary GetAllFields(this FieldInfo[] fields, PhpDeserializationOptions options) { + internal static Dictionary GetAllFields(this FieldInfo[] fields, PhpDeserializationOptions options) { var result = new Dictionary(fields.Length); foreach (var field in fields) { var isIgnored = false; From 2f3a72a701ada52bea712437f29969b3d23e638b Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Tue, 23 Jul 2024 21:50:08 +0200 Subject: [PATCH 04/11] Tokenizer: Small improvement to GetNumbers() and GetLenght(). --- .../Deserialization/PhpTokenizer.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/PhpSerializerNET/Deserialization/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenizer.cs index 60de68a..150d774 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenizer.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenizer.cs @@ -52,24 +52,22 @@ private void Advance(int positons) { [MethodImpl(MethodImplOptions.AggressiveInlining)] private string GetNumbers() { int start = this._position; - int length = this._input.Slice(this._position).IndexOf((byte)';'); - this._position += length; - return this._inputEncoding.GetString(this._input.Slice(start, length)); + while (this._input[this._position] != (byte)';') { + this._position++; + } + return this._inputEncoding.GetString(this._input.Slice(start, this._position-start)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetLength() { - int length = 0; - for (; this._input[this._position] != ':'; this._position++) { - length = length * 10 + (_input[_position] - 48); + if (this._input[this._position+1] == ':') { + return _input[_position++] - 48; } - return length; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetNCharacters(int length) { - return _inputEncoding.GetString(this._input.Slice(this._position, length)); + int start = this._position; + while (this._input[this._position] != (byte)':') { + this._position++; + } + return int.Parse(this._input.Slice(start, this._position-start)); } internal void GetToken() { @@ -127,7 +125,7 @@ private void GetStringToken() { this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.String, position, - this.GetNCharacters(length) + _inputEncoding.GetString(this._input.Slice(this._position, length)) ); this.Advance(2+length); } @@ -178,7 +176,7 @@ private void GetObjectToken() { this.Advance(); int classNameLength = this.GetLength(); this.Advance(2); - string className = this.GetNCharacters(classNameLength); + string className = _inputEncoding.GetString(this._input.Slice(this._position, classNameLength)); this.Advance(2+classNameLength); int propertyCount = this.GetLength(); this._tokens[this._tokenPosition++] = new PhpToken( From f3ddaddbbd167c56568ade34bd1ed45952764c2c Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Tue, 23 Jul 2024 21:52:39 +0200 Subject: [PATCH 05/11] Deserializer: Initialize collection types with proper length. Saves some time and allocation. --- .../Deserialization/PhpDeserializer.cs | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/PhpSerializerNET/Deserialization/PhpDeserializer.cs b/PhpSerializerNET/Deserialization/PhpDeserializer.cs index 492d675..ac113df 100644 --- a/PhpSerializerNET/Deserialization/PhpDeserializer.cs +++ b/PhpSerializerNET/Deserialization/PhpDeserializer.cs @@ -59,41 +59,7 @@ private object DeserializeToken() { } } - private object MakeClass(PhpToken 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") { - _currentToken--; // go back one because we're basically re-entering the object-token from the top. - // If we don't decrement the pointer, we'd start with the first child token instead of the object token. - constructedObject = this.DeserializeToken(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."); - } - for (int i = 0; i < token.Length; i++) { - var key = this.DeserializeToken(); - var value = this.DeserializeToken(); - result.TryAdd( - (string)key, - value - ); - } - constructedObject = result; - } - if (constructedObject is IPhpObject phpObject and not PhpDateTime) { - phpObject.SetClassName(typeName); - } - return constructedObject; - } + private object DeserializeToken(Type targetType) { if (targetType == null) { @@ -270,6 +236,42 @@ int tokenPosition throw new DeserializationException($"Can not assign value \"{value}\" (at position {tokenPosition}) to target type of {targetType.Name}."); } +private object MakeClass(PhpToken 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") { + _currentToken--; // go back one because we're basically re-entering the object-token from the top. + // If we don't decrement the pointer, we'd start with the first child token instead of the object token. + constructedObject = this.DeserializeToken(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."); + } + for (int i = 0; i < token.Length; i++) { + var key = this.DeserializeToken(); + var value = this.DeserializeToken(); + result.TryAdd( + (string)key, + value + ); + } + constructedObject = result; + } + if (constructedObject is IPhpObject phpObject and not PhpDateTime) { + phpObject.SetClassName(typeName); + } + return constructedObject; + } + private object MakeStruct(Type targetType, PhpToken token) { var result = Activator.CreateInstance(targetType); Dictionary fields = TypeLookup.GetFieldInfos(targetType, this._options); @@ -385,7 +387,7 @@ private object MakeList(Type targetType, PhpToken token) { if (targetType.IsArray) { return MakeArray(targetType, token); } - var result = (IList)Activator.CreateInstance(targetType); + var result = (IList)Activator.CreateInstance(targetType, token.Length); if (result == null) { throw new NullReferenceException("Activator.CreateInstance(targetType) returned null"); } @@ -458,7 +460,7 @@ private object MakeCollection(PhpToken token) { } } if (!isList || (this._options.UseLists == ListOptions.Default && consecutive == false)) { - var result = new Dictionary(); + var result = new Dictionary(token.Length); for (int i = 0; i < token.Length; i++) { result.Add( this.DeserializeToken(), @@ -467,7 +469,7 @@ private object MakeCollection(PhpToken token) { } return result; } else { - var result = new List(); + var result = new List(token.Length); for (int i = 0; i < token.Length; i++) { _currentToken++; result.Add(this.DeserializeToken()); From c744a1385e0f5bf06c475e1474c213759790b13e Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Tue, 23 Jul 2024 21:53:04 +0200 Subject: [PATCH 06/11] Update changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9381e..4ca5e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# Future + +## Breaking +- `PhpTokenizer` class is now internal +- Removed support for `net6.0` and `net7.0` + +## Internal +Split the deserialization into 3 phases: + 1. Validation of the input and counting of the data tokens. + 2. Parsing of the input into tokens + 3. Deserializations of the tokens into the target C# objects/structs. + +In version 1.4 and prior, this was a 2 step process. The new approach is slightly slower in some benchmarks, but needs +less memory. On my machine, deserializing an array of 24 integers: + +| | Time | Heap allocation | +|------------|---------:|----------------:| +| **Before** | 1.799 us | 4.54 KB | +| **After** | 1.883 us | 4.13 KB | + +Other benchmarks also indicate a roughly 5-10% performance penalty on arrays, objects and strings. + + # 1.4.0 - Now targets .NET 6.0, 7.0 and 8.0 - Improved tokenization performance by allowing and forcing more aggresive inlining. From f384f55cc1ee235d330253a24099924533d0ae8d Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Wed, 24 Jul 2024 19:16:28 +0200 Subject: [PATCH 07/11] Validator: Throw meaningful exception on empty integers and doubles. --- CHANGELOG.md | 4 +- .../Deserialization/PhpTokenValidator.cs | 38 ++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca5e85..9e07f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - `PhpTokenizer` class is now internal - Removed support for `net6.0` and `net7.0` +## Regular changes +- Integers and doubles without a value now give a better error message (`i:;` and `d:;`). + ## Internal Split the deserialization into 3 phases: 1. Validation of the input and counting of the data tokens. @@ -20,7 +23,6 @@ less memory. On my machine, deserializing an array of 24 integers: Other benchmarks also indicate a roughly 5-10% performance penalty on arrays, objects and strings. - # 1.4.0 - Now targets .NET 6.0, 7.0 and 8.0 - Improved tokenization performance by allowing and forcing more aggresive inlining. diff --git a/PhpSerializerNET/Deserialization/PhpTokenValidator.cs b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs index b96674f..5607a03 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenValidator.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs @@ -89,10 +89,8 @@ private void GetCharacter(char character) { [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetFloat() { - bool valid = true; - int end = this._position; - - for (; this._input[this._position] != ';' && this._position < this._lastIndex && valid; this._position++) { + int i = this._position; + for (; this._input[i] != (byte)';' && i < this._lastIndex; i++) { _ = this._input[this._position] switch { >= (byte)'0' and <= (byte)'9' => true, (byte)'+' => true, @@ -103,16 +101,19 @@ private void GetFloat() { (byte)'N' or (byte)'A' => true, // NaN. _ => throw new DeserializationException( $"Unexpected token at index {this._position}. " + - $"'{(char)this._input[this._position]}' is not a valid part of a floating point number." + $"'{this.GetCharAt(this._position)}' is not a valid part of a floating point number." ), }; - end++; } - - this._position = end; + if (i == this._position) { + throw new DeserializationException( + $"Unexpected token at index {i}: Expected floating point number, but found ';' instead." + ); + } + this._position = i; // Edgecase: input ends here without a delimeter following. Normal handling would give a misleading exception: - if (this._lastIndex == this._position && (char)this._input[this._position] != ';') { + if (this._lastIndex == this._position && this._input[this._position] != (byte)';') { throw new DeserializationException( $"Unexpected end of input. Expected ':' at index {this._position}, but input ends at index {this._lastIndex}" ); @@ -120,21 +121,24 @@ private void GetFloat() { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetInteger() { - int end = this._position; - for (; this._input[this._position] != ';' && this._position < this._lastIndex; this._position++) { - _ = this._input[this._position] switch { + int i = this._position; + for (; this._input[i] != ';' && i < this._lastIndex; i++) { + _ = this._input[i] switch { >= (byte)'0' and <= (byte)'9' => true, (byte)'+' => true, (byte)'-' => true, _ => throw new DeserializationException( - $"Unexpected token at index {this._position}. " + - $"'{(char)this._input[this._position]}' is not a valid part of a number." + $"Unexpected token at index {i}. " + + $"'{this.GetCharAt(i)}' is not a valid part of a number." ), }; - end++; } - - this._position = end; + if (i == this._position) { + throw new DeserializationException( + $"Unexpected token at index {i}: Expected number, but found ';' instead." + ); + } + this._position = i; // Edgecase: input ends here without a delimeter following. Normal handling would give a misleading exception: if (this._lastIndex == this._position && this._input[this._position] != (byte)';') { From 1a6cb96da448bc03eb03eb971a2268abe4bedf58 Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Wed, 24 Jul 2024 19:17:16 +0200 Subject: [PATCH 08/11] Unit tests: Added tests for the last commit. And replaced test framework with XUnit. --- .../Deserialize/ArrayDeserialization.cs | 381 +++++++++--------- .../Deserialize/BooleanDeserialization.cs | 120 +++--- .../Deserialize/DeserializeStructs.cs | 136 +++---- .../Deserialize/DoubleDeserialization.cs | 253 ++++++------ .../Deserialize/EnumDeserialization.cs | 94 +++-- .../Deserialize/IPhpObjectDeserialization.cs | 55 ++- .../Deserialize/IntegerDeserialization.cs | 95 ++--- .../Deserialize/LongDeserialization.cs | 132 ++---- .../Deserialize/NullDeserialization.cs | 72 ++-- .../Deserialize/ObjectDeserialization.cs | 29 +- .../Deserialize/Options/AllowExcessKeys.cs | 31 +- .../Options/CaseSensitiveProperties.cs | 39 +- .../Options/EmptyStringToDefault.cs | 133 +++--- .../Deserialize/Options/EnableTypeLookup.cs | 71 ++-- .../Deserialize/Options/InputEncoding.cs | 13 +- .../Deserialize/Options/NumberStringToBool.cs | 21 +- .../Deserialize/Options/StdClass.cs | 45 +-- .../Deserialize/Options/UseLists.cs | 55 ++- .../Deserialize/PhpDateTimeDeserialization.cs | 29 +- .../Deserialize/StringDeserialization.cs | 135 +++---- .../Validation/TestArrayValidation.cs | 73 +--- .../Validation/TestBoolValidation.cs | 45 +-- .../Validation/TestDoubleValidation.cs | 49 +-- .../Validation/TestIntegerValidation.cs | 57 +-- .../Validation/TestNullValidation.cs | 23 +- .../Validation/TestObjectValidation.cs | 140 ++----- .../Deserialize/Validation/TestOtherErrors.cs | 131 +++--- .../Validation/TestStringValidation.cs | 64 +-- .../Other/DeserializeWithRuntimeType.cs | 79 ++-- .../Other/PhpDateTimeTest.cs | 23 +- .../Other/PhpDynamicObjectTest.cs | 31 +- .../PhpSerializerNET.Test.csproj | 15 +- .../Serialize/ArraySerialization.cs | 17 +- .../Serialize/BooleanSerialization.cs | 30 +- .../Serialize/CircularReferences.cs | 99 +++-- .../Serialize/DictionarySerialization.cs | 89 ++-- .../Serialize/DoubleSerialization.cs | 105 +++-- .../Serialize/DynamicSerialization.cs | 80 ++-- .../Serialize/EnumSerialization.cs | 36 +- .../Serialize/IPhpObjectSerialization.cs | 43 +- .../Serialize/IntegerSerialization.cs | 63 ++- .../Serialize/ListSerialization.cs | 49 ++- .../Serialize/LongSerialization.cs | 37 +- .../Serialize/NullSerialization.cs | 23 +- .../Serialize/ObjectSerialization.cs | 125 +++--- .../Serialize/PhpDateTimeSerialization.cs | 33 +- .../Serialize/StringSerialization.cs | 65 ++- .../Serialize/StructSerialization.cs | 45 +-- 48 files changed, 1599 insertions(+), 2009 deletions(-) diff --git a/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs b/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs index 9ec6f95..31d259a 100644 --- a/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/ArrayDeserialization.cs @@ -8,70 +8,70 @@ This Source Code Form is subject to the terms of the Mozilla Public using System.Collections; using System.Collections.Generic; using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; using static PhpSerializerNET.Test.DataTypes.DeserializeObjects; -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class DeserializeArraysTest { - [TestMethod] - public void ExplicitToClass() { - var deserializedObject = PhpSerialization.Deserialize( - "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" - ); - Assert.AreEqual("this is a string value", deserializedObject.AString); - Assert.AreEqual(10, deserializedObject.AnInteger); - Assert.AreEqual(1.2345, deserializedObject.ADouble); - Assert.AreEqual(true, deserializedObject.True); - Assert.AreEqual(false, deserializedObject.False); - } +namespace PhpSerializerNET.Test.Deserialize; + +public class DeserializeArraysTest { + [Fact] + public void ExplicitToClass() { + var deserializedObject = PhpSerialization.Deserialize( + "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" + ); + Assert.Equal("this is a string value", deserializedObject.AString); + Assert.Equal(10, deserializedObject.AnInteger); + Assert.Equal(1.2345, deserializedObject.ADouble); + Assert.True(deserializedObject.True); + Assert.False(deserializedObject.False); + } - [TestMethod] - public void ExplicitToClassFormatException() { - var ex = Assert.ThrowsException(() => - PhpSerialization.Deserialize("a:1:{s:9:\"AnInteger\";s:3:\"1b1\";}") - ); - Assert.AreEqual( - "Exception encountered while trying to assign '1b1' to SimpleClass.AnInteger. See inner exception for details.", - ex.Message - ); - } + [Fact] + public void ExplicitToClassFormatException() { + var ex = Assert.Throws(() => + PhpSerialization.Deserialize("a:1:{s:9:\"AnInteger\";s:3:\"1b1\";}") + ); + Assert.Equal( + "Exception encountered while trying to assign '1b1' to SimpleClass.AnInteger. See inner exception for details.", + ex.Message + ); + } - [TestMethod] - public void ExplicitToClassWrongProperty() { - var ex = Assert.ThrowsException(() => - PhpSerialization.Deserialize( - "a:1:{s:7:\"BString\";s:22:\"this is a string value\";}" - ) - ); - Assert.AreEqual("Could not bind the key \"BString\" to object of type SimpleClass: No such property.", ex.Message); - } + [Fact] + public void ExplicitToClassWrongProperty() { + var ex = Assert.Throws(() => + PhpSerialization.Deserialize( + "a:1:{s:7:\"BString\";s:22:\"this is a string value\";}" + ) + ); + Assert.Equal("Could not bind the key \"BString\" to object of type SimpleClass: No such property.", ex.Message); + } - [TestMethod] - public void ExplicitToDictionaryOfObject() { - var result = PhpSerialization.Deserialize>( - "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" - ); + [Fact] + public void ExplicitToDictionaryOfObject() { + var result = PhpSerialization.Deserialize>( + "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" + ); - Assert.IsInstanceOfType(result, typeof(Dictionary)); - Assert.AreEqual(5, result.Count); + Assert.IsType>(result); + Assert.Equal(5, result.Count); - Assert.AreEqual("this is a string value", result["AString"]); - Assert.AreEqual((long)10, result["AnInteger"]); - Assert.AreEqual(1.2345, result["ADouble"]); - Assert.AreEqual(true, result["True"]); - Assert.AreEqual(false, result["False"]); - } + Assert.Equal("this is a string value", result["AString"]); + Assert.Equal((long)10, result["AnInteger"]); + Assert.Equal(1.2345, result["ADouble"]); + Assert.Equal(true, result["True"]); + Assert.Equal(false, result["False"]); + } - [TestMethod] - public void ExplicitToDictionaryOfComplexType() { - var result = PhpSerialization.Deserialize>( - "a:1:{s:4:\"AKey\";a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}}" - ); + [Fact] + public void ExplicitToDictionaryOfComplexType() { + var result = PhpSerialization.Deserialize>( + "a:1:{s:4:\"AKey\";a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}}" + ); - var expected = new Dictionary - { + var expected = new Dictionary + { { "AKey", new SimpleClass @@ -86,166 +86,157 @@ public void ExplicitToDictionaryOfComplexType() { } }; - // No easy way to assert dicts in MsTest :/ + // No easy way to assert dicts in MsTest :/ - Assert.AreEqual(expected.Count, result.Count); + Assert.Equal(expected.Count, result.Count); - foreach (var ((expectedKey, expectedValue), (actualKey, actualValue)) in expected.Zip(result)) { - Assert.AreEqual(expectedKey, actualKey); - Assert.AreEqual(expectedValue.ADouble, actualValue.ADouble); - Assert.AreEqual(expectedValue.AString, actualValue.AString); - Assert.AreEqual(expectedValue.AnInteger, actualValue.AnInteger); - Assert.AreEqual(expectedValue.False, actualValue.False); - Assert.AreEqual(expectedValue.True, actualValue.True); - } + foreach (var ((expectedKey, expectedValue), (actualKey, actualValue)) in expected.Zip(result)) { + Assert.Equal(expectedKey, actualKey); + Assert.Equal(expectedValue.ADouble, actualValue.ADouble); + Assert.Equal(expectedValue.AString, actualValue.AString); + Assert.Equal(expectedValue.AnInteger, actualValue.AnInteger); + Assert.Equal(expectedValue.False, actualValue.False); + Assert.Equal(expectedValue.True, actualValue.True); } + } - [TestMethod] - public void ExplicitToHashtable() { - var result = PhpSerialization.Deserialize( - "a:5:{i:0;s:22:\"this is a string value\";i:1;i:10;i:2;d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" - ); - - Assert.IsInstanceOfType(result, typeof(Hashtable)); - Assert.AreEqual(5, result.Count); - // the cast to long on the keys is because of the hashtable and C# intrinsics. - // (int)0 and (long)0 aren't identical enough for the hashtable - Assert.AreEqual("this is a string value", result[(long)0]); - Assert.AreEqual((long)10, result[(long)1]); - Assert.AreEqual(1.2345, result[(long)2]); - Assert.AreEqual(true, result["True"]); - Assert.AreEqual(false, result["False"]); - } + [Fact] + public void ExplicitToHashtable() { + var result = PhpSerialization.Deserialize( + "a:5:{i:0;s:22:\"this is a string value\";i:1;i:10;i:2;d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" + ); + + Assert.IsType(result); + Assert.Equal(5, result.Count); + // the cast to long on the keys is because of the hashtable and C# intrinsics. + // (int)0 and (long)0 aren't identical enough for the hashtable + Assert.Equal("this is a string value", result[(long)0]); + Assert.Equal((long)10, result[(long)1]); + Assert.Equal(1.2345, result[(long)2]); + Assert.Equal(true, result["True"]); + Assert.Equal(false, result["False"]); + } - [TestMethod] - public void ExplicitToClass_MappingInfo() { - var deserializedObject = PhpSerialization.Deserialize( - "a:3:{s:2:\"en\";s:12:\"Hello World!\";s:2:\"de\";s:11:\"Hallo Welt!\";s:2:\"It\";s:11:\"Ciao mondo!\";}" - ); - - // en and de mapped to differently named property: - Assert.AreEqual("Hello World!", deserializedObject.English); - Assert.AreEqual("Hallo Welt!", deserializedObject.German); - // "it" correctly ignored: - Assert.AreEqual(null, deserializedObject.It); - } + [Fact] + public void ExplicitToClass_MappingInfo() { + var deserializedObject = PhpSerialization.Deserialize( + "a:3:{s:2:\"en\";s:12:\"Hello World!\";s:2:\"de\";s:11:\"Hallo Welt!\";s:2:\"It\";s:11:\"Ciao mondo!\";}" + ); + + // en and de mapped to differently named property: + Assert.Equal("Hello World!", deserializedObject.English); + Assert.Equal("Hallo Welt!", deserializedObject.German); + // "it" correctly ignored: + Assert.Null(deserializedObject.It); + } - [TestMethod] - public void ExplicitToStruct() { - var value = PhpSerialization.Deserialize( - "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" - ); - - Assert.AreEqual( - "Foo", - value.foo - ); - Assert.AreEqual( - "Bar", - value.bar - ); - } + [Fact] + public void ExplicitToStruct() { + var value = PhpSerialization.Deserialize( + "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" + ); + + Assert.Equal( + "Foo", + value.foo + ); + Assert.Equal( + "Bar", + value.bar + ); + } - [TestMethod] - public void ExplicitToStructWrongField() { - var ex = Assert.ThrowsException(() => - PhpSerialization.Deserialize( - "a:1:{s:7:\"BString\";s:22:\"this is a string value\";}" - ) - ); - Assert.AreEqual("Could not bind the key \"BString\" to struct of type AStruct: No such field.", ex.Message); - } + [Fact] + public void ExplicitToStructWrongField() { + var ex = Assert.Throws(() => + PhpSerialization.Deserialize( + "a:1:{s:7:\"BString\";s:22:\"this is a string value\";}" + ) + ); + Assert.Equal("Could not bind the key \"BString\" to struct of type AStruct: No such field.", ex.Message); + } - [TestMethod] - public void ExplicitToList() { - var result = PhpSerialization.Deserialize>("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}"); + [Fact] + public void ExplicitToList() { + var result = PhpSerialization.Deserialize>("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}"); - Assert.AreEqual(3, result.Count); - CollectionAssert.AreEqual(new List() { "Hello", "World", "12345" }, result); - } + Assert.Equal(3, result.Count); + Assert.Equal(new List() { "Hello", "World", "12345" }, result); + } - [TestMethod] - public void ExplicitToArray() { - var result = PhpSerialization.Deserialize("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}"); + [Fact] + public void ExplicitToArray() { + var result = PhpSerialization.Deserialize("a:3:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";i:2;i:12345;}"); - CollectionAssert.AreEqual(new string[] { "Hello", "World", "12345" }, result); - } + Assert.Equal(new string[] { "Hello", "World", "12345" }, result); + } - [TestMethod] - public void ExplicitToListNonIntegerKey() { - var ex = Assert.ThrowsException(() => - PhpSerialization.Deserialize>("a:3:{i:0;s:5:\"Hello\";s:1:\"a\";s:5:\"World\";i:2;i:12345;}") - ); + [Fact] + public void ExplicitToListNonIntegerKey() { + var ex = Assert.Throws(() => + 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.Equal("Can not deserialize array at position 0 to list: It has a non-integer key 'a' at element 2 (position 21).", ex.Message); + } - [TestMethod] - public void ExplicitToEmptyList() { - var result = PhpSerialization.Deserialize>("a:0:{}"); - CollectionAssert.AreEqual(new List(), result); - } + [Fact] + public void ExplicitToEmptyList() { + var result = PhpSerialization.Deserialize>("a:0:{}"); + Assert.Equal(new List(), result); + } - [TestMethod] - public void ImplicitToDictionary() { - var result = PhpSerialization.Deserialize( - "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" - ); - - Assert.IsInstanceOfType(result, typeof(Dictionary)); - var dictionary = result as Dictionary; - Assert.AreEqual(5, dictionary.Count); - - Assert.AreEqual("this is a string value", dictionary["AString"]); - Assert.AreEqual((long)10, dictionary["AnInteger"]); - Assert.AreEqual(1.2345, dictionary["ADouble"]); - Assert.AreEqual(true, dictionary["True"]); - Assert.AreEqual(false, dictionary["False"]); - } + [Fact] + public void ImplicitToDictionary() { + var result = PhpSerialization.Deserialize( + "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" + ); + + Assert.IsType>(result); + var dictionary = result as Dictionary; + Assert.Equal(5, dictionary.Count); + + Assert.Equal("this is a string value", dictionary["AString"]); + Assert.Equal((long)10, dictionary["AnInteger"]); + Assert.Equal(1.2345, dictionary["ADouble"]); + Assert.Equal(true, dictionary["True"]); + Assert.Equal(false, dictionary["False"]); + } - [TestMethod] - public void ExcplicitToNestedObject() { - var result = PhpSerialization.Deserialize("a:2:{s:3:\"Foo\";s:5:\"First\";s:3:\"Bar\";a:2:{s:3:\"Foo\";s:6:\"Second\";s:3:\"Bar\";N;}}"); - - Assert.AreEqual( - "First", - result.Foo - ); - Assert.IsNotNull( - result.Bar - ); - Assert.AreEqual( - "Second", - result.Bar.Foo - ); - } + [Fact] + public void ExcplicitToNestedObject() { + var result = PhpSerialization.Deserialize("a:2:{s:3:\"Foo\";s:5:\"First\";s:3:\"Bar\";a:2:{s:3:\"Foo\";s:6:\"Second\";s:3:\"Bar\";N;}}"); - [TestMethod] - public void Test_Issue11() { - // See https://github.com/StringEpsilon/PhpSerializerNET/issues/11 - var deserializedObject = PhpSerialization.Deserialize( - "a:1:{i:0;a:7:{s:1:\"A\";N;s:1:\"B\";N;s:1:\"C\";s:1:\"C\";s:5:\"odSdr\";i:1;s:1:\"D\";d:1;s:1:\"E\";N;s:1:\"F\";a:3:{s:1:\"X\";i:8;s:1:\"Y\";N;s:1:\"Z\";N;}}}" - ); - Assert.IsNotNull(deserializedObject); - } + Assert.Equal("First", result.Foo); + Assert.NotNull(result.Bar); + Assert.Equal("Second", result.Bar.Foo); + } - [TestMethod] - public void Test_Issue12() { - // See https://github.com/StringEpsilon/PhpSerializerNET/issues/12 - var result = PhpSerialization.Deserialize("a:1:{i:0;a:4:{s:1:\"A\";s:2:\"63\";s:1:\"B\";a:2:{i:558710;s:1:\"2\";i:558709;s:1:\"2\";}s:1:\"C\";s:2:\"71\";s:1:\"G\";a:3:{s:1:\"x\";s:6:\"446368\";s:1:\"y\";s:1:\"0\";s:1:\"z\";s:5:\"1.029\";}}}"); - Assert.IsNotNull(result); - } + [Fact] + public void Test_Issue11() { + // See https://github.com/StringEpsilon/PhpSerializerNET/issues/11 + var deserializedObject = PhpSerialization.Deserialize( + "a:1:{i:0;a:7:{s:1:\"A\";N;s:1:\"B\";N;s:1:\"C\";s:1:\"C\";s:5:\"odSdr\";i:1;s:1:\"D\";d:1;s:1:\"E\";N;s:1:\"F\";a:3:{s:1:\"X\";i:8;s:1:\"Y\";N;s:1:\"Z\";N;}}}" + ); + Assert.NotNull(deserializedObject); + } + + [Fact] + public void Test_Issue12() { + // See https://github.com/StringEpsilon/PhpSerializerNET/issues/12 + var result = PhpSerialization.Deserialize("a:1:{i:0;a:4:{s:1:\"A\";s:2:\"63\";s:1:\"B\";a:2:{i:558710;s:1:\"2\";i:558709;s:1:\"2\";}s:1:\"C\";s:2:\"71\";s:1:\"G\";a:3:{s:1:\"x\";s:6:\"446368\";s:1:\"y\";s:1:\"0\";s:1:\"z\";s:5:\"1.029\";}}}"); + Assert.NotNull(result); + } - [TestMethod] - public void MixedKeyArrayIntoObject() { - var result = PhpSerialization.Deserialize( - "a:4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}" - ); + [Fact] + public void MixedKeyArrayIntoObject() { + var result = PhpSerialization.Deserialize( + "a:4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}" + ); - Assert.AreEqual("Foo", result.Foo); - Assert.AreEqual("Bar", result.Bar); - Assert.AreEqual("A", result.Baz); - Assert.AreEqual("B", result.Dummy); - } + Assert.Equal("Foo", result.Foo); + Assert.Equal("Bar", result.Bar); + Assert.Equal("A", result.Baz); + Assert.Equal("B", result.Dummy); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/BooleanDeserialization.cs b/PhpSerializerNET.Test/Deserialize/BooleanDeserialization.cs index e72cc71..1726f89 100644 --- a/PhpSerializerNET.Test/Deserialize/BooleanDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/BooleanDeserialization.cs @@ -5,72 +5,58 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class DeserializeBooleansTest { - [TestMethod] - public void DeserializesTrue() { - Assert.AreEqual( - true, - PhpSerialization.Deserialize("b:1;") - ); - } - - [TestMethod] - public void DeserializesTrueExplicit() { - - Assert.AreEqual( - true, - PhpSerialization.Deserialize("b:1;") - ); - } - - [TestMethod] - public void DeserializesFalse() { - Assert.AreEqual( - false, - PhpSerialization.Deserialize("b:0;") - ); - } - - [TestMethod] - public void DeserializesFalseExplicit() { - Assert.AreEqual( - false, - PhpSerialization.Deserialize("b:0;") - ); - } - - [TestMethod] - public void DeserializesToLong() { - var result = PhpSerialization.Deserialize("b:0;"); - - Assert.AreEqual(0, result); - - result = PhpSerialization.Deserialize("b:1;"); - - Assert.AreEqual(1, result); - } - - [TestMethod] - public void DeserializesToString() { - var result = PhpSerialization.Deserialize("b:0;"); - - Assert.AreEqual("False", result); - - result = PhpSerialization.Deserialize("b:1;"); - - Assert.AreEqual("True", result); - } - - [TestMethod] - public void DeserializeToNullable() { - Assert.AreEqual( - false, - PhpSerialization.Deserialize("b:0;") - ); - } +using Xunit; + +namespace PhpSerializerNET.Test.Deserialize; + +public class DeserializeBooleansTest { + [Fact] + public void DeserializesTrue() { + Assert.Equal(true, PhpSerialization.Deserialize("b:1;")); + } + + [Fact] + public void DeserializesTrueExplicit() { + Assert.True(PhpSerialization.Deserialize("b:1;")); + } + + [Fact] + public void DeserializesFalse() { + Assert.Equal(false, PhpSerialization.Deserialize("b:0;")); + } + + [Fact] + public void DeserializesFalseExplicit() { + Assert.False(PhpSerialization.Deserialize("b:0;")); + } + + [Fact] + public void DeserializesToLong() { + var result = PhpSerialization.Deserialize("b:0;"); + + Assert.Equal(0, result); + + result = PhpSerialization.Deserialize("b:1;"); + + Assert.Equal(1, result); + } + + [Fact] + public void DeserializesToString() { + var result = PhpSerialization.Deserialize("b:0;"); + + Assert.Equal("False", result); + + result = PhpSerialization.Deserialize("b:1;"); + + Assert.Equal("True", result); + } + + [Fact] + public void DeserializeToNullable() { + Assert.Equal( + false, + PhpSerialization.Deserialize("b:0;") + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/DeserializeStructs.cs b/PhpSerializerNET.Test/Deserialize/DeserializeStructs.cs index 032dffb..01b84c8 100644 --- a/PhpSerializerNET.Test/Deserialize/DeserializeStructs.cs +++ b/PhpSerializerNET.Test/Deserialize/DeserializeStructs.cs @@ -4,86 +4,84 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize { +namespace PhpSerializerNET.Test.Deserialize; - [TestClass] - public class DeserializeStructsTest { - [TestMethod] - public void DeserializeArrayToStruct() { - var value = PhpSerialization.Deserialize( - "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" - ); - Assert.AreEqual("Foo", value.foo); - Assert.AreEqual("Bar", value.bar); - } +public class DeserializeStructsTest { + [Fact] + public void DeserializeArrayToStruct() { + var value = PhpSerialization.Deserialize( + "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" + ); + Assert.Equal("Foo", value.foo); + Assert.Equal("Bar", value.bar); + } - [TestMethod] - public void DeserializeObjectToStruct() { - var value = PhpSerialization.Deserialize( - "O:8:\"sdtClass\":2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" - ); - Assert.AreEqual("Foo", value.foo); - Assert.AreEqual("Bar", value.bar); - } + [Fact] + public void DeserializeObjectToStruct() { + var value = PhpSerialization.Deserialize( + "O:8:\"sdtClass\":2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" + ); + Assert.Equal("Foo", value.foo); + Assert.Equal("Bar", value.bar); + } - [TestMethod] - public void DeserializeWithIgnoredField() { - var value = PhpSerialization.Deserialize( - "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" - ); - Assert.AreEqual("Foo", value.foo); - Assert.AreEqual(null, value.bar); - } + [Fact] + public void DeserializeWithIgnoredField() { + var value = PhpSerialization.Deserialize( + "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}" + ); + Assert.Equal("Foo", value.foo); + Assert.Null(value.bar); + } - [TestMethod] - public void DeserializePropertyName() { - var value = PhpSerialization.Deserialize( - "a:2:{s:3:\"foo\";s:3:\"Foo\";s:6:\"foobar\";s:3:\"Bar\";}" - ); - Assert.AreEqual("Foo", value.foo); - Assert.AreEqual("Bar", value.bar); - } + [Fact] + public void DeserializePropertyName() { + var value = PhpSerialization.Deserialize( + "a:2:{s:3:\"foo\";s:3:\"Foo\";s:6:\"foobar\";s:3:\"Bar\";}" + ); + Assert.Equal("Foo", value.foo); + Assert.Equal("Bar", value.bar); + } - [TestMethod] - public void DeserializeBoolToStruct() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "b:1;" - ) - ); + [Fact] + public void DeserializeBoolToStruct() { + var ex = Assert.Throws( + () => PhpSerialization.Deserialize( + "b:1;" + ) + ); - Assert.AreEqual( - "Can not assign value \"1\" (at position 0) to target type of AStruct.", - ex.Message - ); - } + Assert.Equal( + "Can not assign value \"1\" (at position 0) to target type of AStruct.", + ex.Message + ); + } - [TestMethod] - public void DeserializeStringToStruct() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "s:3:\"foo\";" - ) - ); + [Fact] + public void DeserializeStringToStruct() { + var ex = Assert.Throws( + () => PhpSerialization.Deserialize( + "s:3:\"foo\";" + ) + ); - Assert.AreEqual( - "Can not assign value \"foo\" (at position 0) to target type of AStruct.", - ex.Message - ); - } + Assert.Equal( + "Can not assign value \"foo\" (at position 0) to target type of AStruct.", + ex.Message + ); + } - [TestMethod] - public void DeserializeNullToStruct() { - Assert.AreEqual( - default, - PhpSerialization.Deserialize( - "N;" - ) - ); - } + [Fact] + public void DeserializeNullToStruct() { + Assert.Equal( + default, + PhpSerialization.Deserialize( + "N;" + ) + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/DoubleDeserialization.cs b/PhpSerializerNET.Test/Deserialize/DoubleDeserialization.cs index 933345d..5f8213d 100644 --- a/PhpSerializerNET.Test/Deserialize/DoubleDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/DoubleDeserialization.cs @@ -6,133 +6,132 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class DoubleDeserializationTest { - - [TestMethod] - public void DeserializesNormalValue() { - Assert.AreEqual( - 1.23456789, - PhpSerialization.Deserialize("d:1.23456789;") - ); - Assert.AreEqual( - 1.23456789, - PhpSerialization.Deserialize("d:1.23456789;") - ); - } - - [TestMethod] - public void DeserializesOne() { - Assert.AreEqual( - (double)1, - PhpSerialization.Deserialize("d:1;") - ); - } - - [TestMethod] - public void DeserializesMinValue() { - Assert.AreEqual( - double.MinValue, - PhpSerialization.Deserialize("d:-1.7976931348623157E+308;") - ); - } - - [TestMethod] - public void DeserializesMaxValue() { - Assert.AreEqual( - double.MaxValue, - PhpSerialization.Deserialize("d:1.7976931348623157E+308;") - ); - } - - [TestMethod] - public void DeserializesInfinity() { - Assert.AreEqual( - double.PositiveInfinity, - PhpSerialization.Deserialize("d:INF;") - ); - } - - [TestMethod] - public void DeserializesNegativeInfinity() { - Assert.AreEqual( - double.NegativeInfinity, - PhpSerialization.Deserialize("d:-INF;") - ); - } - - [TestMethod] - public void DeserializesNotANumber() { - Assert.AreEqual( - double.NaN, - PhpSerialization.Deserialize("d:NAN;") - ); - } - - [TestMethod] - public void Explicit_DeserializesInfinity() { - Assert.AreEqual( - double.PositiveInfinity, - PhpSerialization.Deserialize("d:INF;") - ); - } - - [TestMethod] - public void Explicit_DeserializesNegativeInfinity() { - Assert.AreEqual( - double.NegativeInfinity, - PhpSerialization.Deserialize("d:-INF;") - ); - } - - [TestMethod] - public void Explicit_DeserializesNotANumber() { - Assert.AreEqual( - double.NaN, - PhpSerialization.Deserialize("d:NAN;") - ); - } - - [TestMethod] - public void Explicit_Nullable_DeserializesInfinity() { - Assert.AreEqual( - double.PositiveInfinity, - PhpSerialization.Deserialize("d:INF;") - ); - } - - [TestMethod] - public void Explicit_Nullable_DeserializesNegativeInfinity() { - Assert.AreEqual( - double.NegativeInfinity, - PhpSerialization.Deserialize("d:-INF;") - ); - } - - [TestMethod] - public void Explicit_Nullable_DeserializesNotANumber() { - Assert.AreEqual( - double.NaN, - PhpSerialization.Deserialize("d:NAN;") - ); - } - - [TestMethod] - public void DeserializesToNullable() { - Assert.AreEqual( - 3.1415, - PhpSerialization.Deserialize("d:3.1415;") - ); - } - - [TestMethod] - public void DeserializeDoubleToInt() { - double number = PhpSerialization.Deserialize("d:10;"); - Assert.AreEqual((long)10, number); - } +using Xunit; + +namespace PhpSerializerNET.Test.Deserialize; + +public class DoubleDeserializationTest { + + [Fact] + public void DeserializesNormalValue() { + Assert.Equal( + 1.23456789, + PhpSerialization.Deserialize("d:1.23456789;") + ); + Assert.Equal( + 1.23456789, + PhpSerialization.Deserialize("d:1.23456789;") + ); + } + + [Fact] + public void DeserializesOne() { + Assert.Equal( + (double)1, + PhpSerialization.Deserialize("d:1;") + ); + } + + [Fact] + public void DeserializesMinValue() { + Assert.Equal( + double.MinValue, + PhpSerialization.Deserialize("d:-1.7976931348623157E+308;") + ); + } + + [Fact] + public void DeserializesMaxValue() { + Assert.Equal( + double.MaxValue, + PhpSerialization.Deserialize("d:1.7976931348623157E+308;") + ); + } + + [Fact] + public void DeserializesInfinity() { + Assert.Equal( + double.PositiveInfinity, + PhpSerialization.Deserialize("d:INF;") + ); + } + + [Fact] + public void DeserializesNegativeInfinity() { + Assert.Equal( + double.NegativeInfinity, + PhpSerialization.Deserialize("d:-INF;") + ); + } + + [Fact] + public void DeserializesNotANumber() { + Assert.Equal( + double.NaN, + PhpSerialization.Deserialize("d:NAN;") + ); + } + + [Fact] + public void Explicit_DeserializesInfinity() { + Assert.Equal( + double.PositiveInfinity, + PhpSerialization.Deserialize("d:INF;") + ); + } + + [Fact] + public void Explicit_DeserializesNegativeInfinity() { + Assert.Equal( + double.NegativeInfinity, + PhpSerialization.Deserialize("d:-INF;") + ); + } + + [Fact] + public void Explicit_DeserializesNotANumber() { + Assert.Equal( + double.NaN, + PhpSerialization.Deserialize("d:NAN;") + ); + } + + [Fact] + public void Explicit_Nullable_DeserializesInfinity() { + Assert.Equal( + double.PositiveInfinity, + PhpSerialization.Deserialize("d:INF;") + ); + } + [Fact] + public void Explicit_Nullable_DeserializesNegativeInfinity() { + Assert.Equal( + double.NegativeInfinity, + PhpSerialization.Deserialize("d:-INF;") + ); } + + [Fact] + public void Explicit_Nullable_DeserializesNotANumber() { + Assert.Equal( + double.NaN, + PhpSerialization.Deserialize("d:NAN;") + ); + } + + [Fact] + public void DeserializesToNullable() { + Assert.Equal( + 3.1415, + PhpSerialization.Deserialize("d:3.1415;") + ); + } + + [Fact] + public void DeserializeDoubleToInt() { + double number = PhpSerialization.Deserialize("d:10;"); + Assert.Equal(10, number); + } + } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/EnumDeserialization.cs b/PhpSerializerNET.Test/Deserialize/EnumDeserialization.cs index 4aaa5b6..c512138 100644 --- a/PhpSerializerNET.Test/Deserialize/EnumDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/EnumDeserialization.cs @@ -5,63 +5,61 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize { +namespace PhpSerializerNET.Test.Deserialize; - [TestClass] - public class EnumDeserializationTest { +public class EnumDeserializationTest { - [TestMethod] - public void DeserializeLongBasedEnum() { - Assert.AreEqual( - IntEnum.A, - PhpSerialization.Deserialize("i:1;") - ); - } + [Fact] + public void DeserializeLongBasedEnum() { + Assert.Equal( + IntEnum.A, + PhpSerialization.Deserialize("i:1;") + ); + } - [TestMethod] - public void DeserializeIntBasedEnum() { - Assert.AreEqual( - LongEnum.A, - PhpSerialization.Deserialize("i:1;") - ); - } + [Fact] + public void DeserializeIntBasedEnum() { + Assert.Equal( + LongEnum.A, + PhpSerialization.Deserialize("i:1;") + ); + } - [TestMethod] - public void DeserializeFromString() { - Assert.AreEqual( - LongEnum.A, - PhpSerialization.Deserialize("s:1:\"A\";") - ); - } + [Fact] + public void DeserializeFromString() { + Assert.Equal( + LongEnum.A, + PhpSerialization.Deserialize("s:1:\"A\";") + ); + } - [TestMethod] - public void DeserializeFromStringWithPropertyName() { - Assert.AreEqual( - IntEnumWithPropertyName.A, - PhpSerialization.Deserialize("s:1:\"a\";") - ); + [Fact] + public void DeserializeFromStringWithPropertyName() { + Assert.Equal( + IntEnumWithPropertyName.A, + PhpSerialization.Deserialize("s:1:\"a\";") + ); - Assert.AreEqual( - IntEnumWithPropertyName.B, - PhpSerialization.Deserialize("s:1:\"c\";") - ); + Assert.Equal( + IntEnumWithPropertyName.B, + PhpSerialization.Deserialize("s:1:\"c\";") + ); - Assert.AreEqual( - IntEnumWithPropertyName.C, - PhpSerialization.Deserialize("s:1:\"C\";") - ); - } + Assert.Equal( + IntEnumWithPropertyName.C, + PhpSerialization.Deserialize("s:1:\"C\";") + ); + } - [TestMethod] - public void DeserializeToNullable() { - LongEnum? result = PhpSerialization.Deserialize("i:1;"); - Assert.AreEqual( - LongEnum.A, - result - ); - } + [Fact] + public void DeserializeToNullable() { + LongEnum? result = PhpSerialization.Deserialize("i:1;"); + Assert.Equal( + LongEnum.A, + result + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/IPhpObjectDeserialization.cs b/PhpSerializerNET.Test/Deserialize/IPhpObjectDeserialization.cs index b2e04bb..6003114 100644 --- a/PhpSerializerNET.Test/Deserialize/IPhpObjectDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/IPhpObjectDeserialization.cs @@ -3,38 +3,37 @@ 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test { - [TestClass] - public class IPhpObjectDeserializationTest { +namespace PhpSerializerNET.Test; - [TestMethod] - public void DeerializesIPhpObject() { // #Issue 25 - var result = PhpSerialization.Deserialize("O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}"); - Assert.AreEqual( // strings: - "MyPhpObject", - result.GetClassName() - ); - } +public class IPhpObjectDeserializationTest { - [TestMethod] - public void DeerializesPhpObjectDictionary() { - var result = PhpSerialization.Deserialize("O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}"); - Assert.AreEqual( // strings: - "MyPhpObject", - result.GetClassName() - ); - } + [Fact] + public void DeerializesIPhpObject() { // #Issue 25 + var result = PhpSerialization.Deserialize("O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}"); + Assert.Equal( // strings: + "MyPhpObject", + result.GetClassName() + ); + } + + [Fact] + public void DeerializesPhpObjectDictionary() { + var result = PhpSerialization.Deserialize("O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}"); + Assert.Equal( // strings: + "MyPhpObject", + result.GetClassName() + ); + } - [TestMethod] - public void DeerializesIPhpObjectStruct() { - var result = PhpSerialization.Deserialize("O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}"); - Assert.AreEqual( // strings: - "MyPhpObject", - result.GetClassName() - ); - } + [Fact] + public void DeerializesIPhpObjectStruct() { + var result = PhpSerialization.Deserialize("O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}"); + Assert.Equal( // strings: + "MyPhpObject", + result.GetClassName() + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/IntegerDeserialization.cs b/PhpSerializerNET.Test/Deserialize/IntegerDeserialization.cs index 5ba7446..7f40520 100644 --- a/PhpSerializerNET.Test/Deserialize/IntegerDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/IntegerDeserialization.cs @@ -5,69 +5,40 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class IntegerDeserializationTest { - [TestMethod] - public void DeserializeZero() { - Assert.AreEqual( - 0, - PhpSerialization.Deserialize("i:0;") - ); - } - - [TestMethod] - public void DeserializeOne() { - Assert.AreEqual( - 1, - PhpSerialization.Deserialize("i:1;") - ); - } - - [TestMethod] - public void DeserializeToNullable() { - var result = PhpSerialization.Deserialize("i:1;"); - Assert.IsInstanceOfType(result, typeof(int?)); - Assert.AreEqual( - 1, - result.Value - ); - } - - [TestMethod] - public void DeserializeIntMaxValue() { - Assert.AreEqual( - int.MaxValue, - PhpSerialization.Deserialize("i:2147483647;") - ); - } - - [TestMethod] - public void DeserializeIntMinValue() { - Assert.AreEqual( - int.MinValue, - PhpSerialization.Deserialize("i:-2147483648;") - ); - } +using Xunit; + +namespace PhpSerializerNET.Test.Deserialize; + +public class IntegerDeserializationTest { + [Theory] + [InlineData("i:0;", 0)] + [InlineData("i:1;", 1)] + [InlineData("i:2147483647;", int.MaxValue)] + [InlineData("i:-2147483648;", int.MinValue)] + public void DeserializeZero(string input, int expected) { + Assert.Equal(expected, PhpSerialization.Deserialize(input)); + } - [TestMethod] - public void DeserializeIntToDouble() { - double number = PhpSerialization.Deserialize("i:10;"); - Assert.AreEqual(10.00, number); - } + [Fact] + public void DeserializeToNullable() { + var result = PhpSerialization.Deserialize("i:1;"); + Assert.Equal(1, result.Value); + } - [TestMethod] - public void ExplictCastFormatException() { - var ex = Assert.ThrowsException(() => - PhpSerialization.Deserialize( - "s:3:\"1b1\";" - ) - ); - Assert.IsInstanceOfType(ex.InnerException, typeof(System.FormatException)); - Assert.AreEqual("Exception encountered while trying to assign '1b1' to type Int32. See inner exception for details.", ex.Message); - } + [Fact] + public void DeserializeIntToDouble() { + double number = PhpSerialization.Deserialize("i:10;"); + Assert.Equal(10.00, number); + } + [Fact] + public void ExplictCastFormatException() { + var ex = Assert.Throws(() => + PhpSerialization.Deserialize( + "s:3:\"1b1\";" + ) + ); + Assert.IsType(ex.InnerException); + Assert.Equal("Exception encountered while trying to assign '1b1' to type Int32. See inner exception for details.", ex.Message); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Deserialize/LongDeserialization.cs b/PhpSerializerNET.Test/Deserialize/LongDeserialization.cs index 811a127..3b427fa 100644 --- a/PhpSerializerNET.Test/Deserialize/LongDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/LongDeserialization.cs @@ -5,109 +5,39 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class LongDeserializationTest { +namespace PhpSerializerNET.Test.Deserialize; - [TestMethod] - public void DeserializeZero() { - Assert.AreEqual( - (long)0, - PhpSerialization.Deserialize("i:0;") - ); - } - - [TestMethod] - public void DeserializeOne() { - Assert.AreEqual( - (long)1, - PhpSerialization.Deserialize("i:1;") - ); - } - - - [TestMethod] - public void DeserializeMaxValue() { - Assert.AreEqual( - long.MaxValue, - PhpSerialization.Deserialize("i:9223372036854775807;") - ); - } - - [TestMethod] - public void DeserializeMinValue() { - Assert.AreEqual( - long.MinValue, - PhpSerialization.Deserialize("i:-9223372036854775808;") - ); - } - - [TestMethod] - public void DeserializesNullToZero() { - var result = PhpSerialization.Deserialize("N;"); - - Assert.AreEqual(0, result); - } - - [TestMethod] - public void DeserializesToNullable() { - var result = PhpSerialization.Deserialize("N;"); - - Assert.AreEqual(null, result); - - result = PhpSerialization.Deserialize("i:1;"); - - Assert.AreEqual(1, result); - } - - [TestMethod] - public void SupportsOtherNumberTypes() { - Assert.AreEqual( - short.MinValue, - PhpSerialization.Deserialize($"i:{short.MinValue};") - ); - Assert.AreEqual( - short.MaxValue, - PhpSerialization.Deserialize($"i:{short.MaxValue};") - ); - - Assert.AreEqual( - ushort.MinValue, - PhpSerialization.Deserialize($"i:{ushort.MinValue};") - ); - Assert.AreEqual( - ushort.MaxValue, - PhpSerialization.Deserialize($"i:{ushort.MaxValue};") - ); - - Assert.AreEqual( - uint.MinValue, - PhpSerialization.Deserialize($"i:{uint.MinValue};") - ); - Assert.AreEqual( - uint.MaxValue, - PhpSerialization.Deserialize($"i:{uint.MaxValue};") - ); - - Assert.AreEqual( - ulong.MinValue, - PhpSerialization.Deserialize($"i:{ulong.MinValue};") - ); - Assert.AreEqual( - ulong.MaxValue, - PhpSerialization.Deserialize($"i:{ulong.MaxValue};") - ); +public class LongDeserializationTest { + [Theory] + [InlineData("N;", null)] + [InlineData("i:1;", 1L)] + public void SupportsNull(string input, T expected) { + var result = PhpSerialization.Deserialize(input); + Assert.Equal(expected, result); + } - Assert.AreEqual( - sbyte.MinValue, - PhpSerialization.Deserialize($"i:{sbyte.MinValue};") - ); - Assert.AreEqual( - sbyte.MaxValue, - PhpSerialization.Deserialize($"i:{sbyte.MaxValue};") - ); - } + [Theory] + [InlineData("i:-32768;", short.MinValue)] + [InlineData("i:32767;", short.MaxValue)] + [InlineData("i:0;", ushort.MinValue)] + [InlineData("i:65535;", ushort.MaxValue)] + [InlineData("i:0;", uint.MinValue)] + [InlineData("i:4294967295;", uint.MaxValue)] + [InlineData("N;", 0L)] + [InlineData("i:0;", 0L)] + [InlineData("i:1;", 1L)] + [InlineData("i:-9223372036854775808;", long.MinValue)] + [InlineData("i:9223372036854775807;", long.MaxValue)] + [InlineData("i:0;", ulong.MinValue)] + [InlineData("i:18446744073709551615;", ulong.MaxValue)] + [InlineData("i:-128;", sbyte.MinValue)] + [InlineData("i:127;", sbyte.MaxValue)] + [InlineData("i:255;", byte.MaxValue)] + public void SupportsOtherNumberTypes(string input, T expected) { + var result = PhpSerialization.Deserialize(input); + Assert.IsType(result); + Assert.Equal(expected, result); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/NullDeserialization.cs b/PhpSerializerNET.Test/Deserialize/NullDeserialization.cs index eb59e19..acb3576 100644 --- a/PhpSerializerNET.Test/Deserialize/NullDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/NullDeserialization.cs @@ -6,51 +6,35 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class NullDeserializationTest { - [TestMethod] - public void DeserializesNull() { - var result = PhpSerialization.Deserialize("N;"); - - Assert.IsNull(result); - } - - [TestMethod] - public void DeserializesExplicitNull() { - var result = PhpSerialization.Deserialize("N;"); - - Assert.IsNull(result); - } - - [TestMethod] - public void DeserializesToNullableStruct() { - var result = PhpSerialization.Deserialize("N;"); - - Assert.IsNull(result); - } - - [TestMethod] - public void ExplicitToPrimitiveDefaultValues() { - Assert.AreEqual( - 0, - PhpSerialization.Deserialize("N;") - ); - Assert.AreEqual( - false, - PhpSerialization.Deserialize("N;") - ); - Assert.AreEqual( - 0, - PhpSerialization.Deserialize("N;") - ); - Assert.AreEqual( - null, - PhpSerialization.Deserialize("N;") - ); - } +namespace PhpSerializerNET.Test.Deserialize; + +public class NullDeserializationTest { + [Fact] + public void DeserializesNull() { + var result = PhpSerialization.Deserialize("N;"); + + Assert.Null(result); + } + + [Fact] + public void DeserializesExplicitNull() { + var result = PhpSerialization.Deserialize("N;"); + Assert.Null(result); + } + + [Fact] + public void DeserializesToNullableStruct() { + var result = PhpSerialization.Deserialize("N;"); + Assert.Null(result); + } + + [Fact] + public void ExplicitToPrimitiveDefaultValues() { + Assert.False(PhpSerialization.Deserialize("N;")); + Assert.Equal(0, PhpSerialization.Deserialize("N;")); + Assert.Null(PhpSerialization.Deserialize("N;")); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/ObjectDeserialization.cs b/PhpSerializerNET.Test/Deserialize/ObjectDeserialization.cs index feb3050..89eaf63 100644 --- a/PhpSerializerNET.Test/Deserialize/ObjectDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/ObjectDeserialization.cs @@ -5,23 +5,22 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class ObjectDeserializationTest { - [TestMethod] - public void IntegerKeysClass() { - var result = PhpSerialization.Deserialize( - "O:8:\"stdClass\":4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}" - ); +namespace PhpSerializerNET.Test.Deserialize; - Assert.IsNotNull(result); - Assert.AreEqual("Foo", result.Foo); - Assert.AreEqual("Bar", result.Bar); - Assert.AreEqual("A", result.Baz); - Assert.AreEqual("B", result.Dummy); - } +public class ObjectDeserializationTest { + [Fact] + public void IntegerKeysClass() { + var result = PhpSerialization.Deserialize( + "O:8:\"stdClass\":4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}" + ); + + Assert.NotNull(result); + Assert.Equal("Foo", result.Foo); + Assert.Equal("Bar", result.Bar); + Assert.Equal("A", result.Baz); + Assert.Equal("B", result.Dummy); } } diff --git a/PhpSerializerNET.Test/Deserialize/Options/AllowExcessKeys.cs b/PhpSerializerNET.Test/Deserialize/Options/AllowExcessKeys.cs index 36765c5..0178785 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/AllowExcessKeys.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/AllowExcessKeys.cs @@ -4,68 +4,67 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class AllowExcessKeysTest { const string StructTestInput = "a:3:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";s:6:\"foobar\";s:6:\"FooBar\";}"; const string ObjectTestInput = "a:2:{s:7:\"AString\";s:3:\"foo\";s:7:\"BString\";s:3:\"bar\";}"; - [TestMethod] + [Fact] public void Struct_DeserializesWithOptionEnabled() { var value = PhpSerialization.Deserialize( StructTestInput, new PhpDeserializationOptions() { AllowExcessKeys = true } ); - Assert.AreEqual( + Assert.Equal( "Foo", value.foo ); - Assert.AreEqual( + Assert.Equal( "Bar", value.bar ); } - [TestMethod] + [Fact] public void Struct_ThrowsWithOptionDisabled() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize( + var ex = Assert.Throws(() => PhpSerialization.Deserialize( StructTestInput, new PhpDeserializationOptions() { AllowExcessKeys = false } )); - Assert.AreEqual("Could not bind the key \"foobar\" to struct of type AStruct: No such field.", ex.Message); + Assert.Equal("Could not bind the key \"foobar\" to struct of type AStruct: No such field.", ex.Message); } - [TestMethod] + [Fact] public void Object_DeserializesWithOptionEnabled() { var deserializedObject = PhpSerialization.Deserialize( ObjectTestInput, new PhpDeserializationOptions() { AllowExcessKeys = true } ); - Assert.IsNotNull(deserializedObject); + Assert.NotNull(deserializedObject); } - [TestMethod] + [Fact] public void Object_ThrowsWithOptionDisabled() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize( + var ex = Assert.Throws(() => PhpSerialization.Deserialize( ObjectTestInput, new PhpDeserializationOptions() { AllowExcessKeys = false } )); - Assert.AreEqual("Could not bind the key \"BString\" to object of type SimpleClass: No such property.", ex.Message); + Assert.Equal("Could not bind the key \"BString\" to object of type SimpleClass: No such property.", ex.Message); } - [TestMethod] + [Fact] public void Enabled_ProperlyAssignsAllKeys() { // Explicit test for issue #27. var result = PhpSerialization.Deserialize( "O:11:\"SimpleClass\":3:{s:1:\"_\";b:0;s:4:\"True\";b:1;s:5:\"False\";b:0;}", new PhpDeserializationOptions() { AllowExcessKeys = true } ); - Assert.AreEqual(true, result.True); - Assert.AreEqual(false, result.False); + Assert.True(result.True); + Assert.False(result.False); } } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Options/CaseSensitiveProperties.cs b/PhpSerializerNET.Test/Deserialize/Options/CaseSensitiveProperties.cs index f44f637..e99e54b 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/CaseSensitiveProperties.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/CaseSensitiveProperties.cs @@ -4,66 +4,65 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class CaseSensitivePropertiesTest { - [TestMethod] + [Fact] public void Disabled_Array_Deserializes() { var result = PhpSerialization.Deserialize( "a:2:{s:3:\"FOO\";s:3:\"Foo\";s:3:\"BAR\";s:3:\"Bar\";}", new PhpDeserializationOptions() { CaseSensitiveProperties = false } ); - Assert.AreEqual("Foo", result.foo); - Assert.AreEqual("Bar", result.bar); + Assert.Equal("Foo", result.foo); + Assert.Equal("Bar", result.bar); } - [TestMethod] + [Fact] public void Disabled_Object_Deserializes() { var result = PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:3:\"FOO\";s:3:\"Foo\";s:3:\"BAR\";s:3:\"Bar\";}", new PhpDeserializationOptions() { CaseSensitiveProperties = false } ); - Assert.AreEqual("Foo", result.foo); - Assert.AreEqual("Bar", result.bar); + Assert.Equal("Foo", result.foo); + Assert.Equal("Bar", result.bar); } - [TestMethod] + [Fact] public void Enabled_Array_Throws() { - var exception = Assert.ThrowsException( + var exception = Assert.Throws( () => PhpSerialization.Deserialize( "a:2:{s:3:\"FOO\";s:3:\"Foo\";s:3:\"BAR\";s:3:\"Bar\";}", new PhpDeserializationOptions() { CaseSensitiveProperties = true } ) ); - Assert.AreEqual( + Assert.Equal( "Could not bind the key \"FOO\" to struct of type AStruct: No such field.", exception.Message ); } - [TestMethod] + [Fact] public void Enabled_Object_Throws() { - var exception = Assert.ThrowsException( + var exception = Assert.Throws( () => PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:3:\"FOO\";s:3:\"Foo\";s:3:\"BAR\";s:3:\"Bar\";}", new PhpDeserializationOptions() { CaseSensitiveProperties = true } ) ); - Assert.AreEqual( + Assert.Equal( "Could not bind the key \"FOO\" to struct of type AStruct: No such field.", exception.Message ); } - [TestMethod] + [Fact] public void Disabled_Object_WorksWithMapping() { var deserializedObject = PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:2:\"EN\";s:12:\"Hello World!\";s:2:\"DE\";s:11:\"Hallo Welt!\";}", @@ -71,11 +70,11 @@ public void Disabled_Object_WorksWithMapping() { ); // en and de mapped to differently named property: - Assert.AreEqual("Hello World!", deserializedObject.English); - Assert.AreEqual("Hallo Welt!", deserializedObject.German); + Assert.Equal("Hello World!", deserializedObject.English); + Assert.Equal("Hallo Welt!", deserializedObject.German); } - [TestMethod] + [Fact] public void Disabled_Array_WorksWithMapping() { var deserializedObject = PhpSerialization.Deserialize( "a:2:{s:2:\"EN\";s:12:\"Hello World!\";s:2:\"DE\";s:11:\"Hallo Welt!\";}", @@ -83,8 +82,8 @@ public void Disabled_Array_WorksWithMapping() { ); // en and de mapped to differently named property: - Assert.AreEqual("Hello World!", deserializedObject.English); - Assert.AreEqual("Hallo Welt!", deserializedObject.German); + Assert.Equal("Hello World!", deserializedObject.English); + Assert.Equal("Hallo Welt!", deserializedObject.German); } } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs b/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs index 436e35e..e8b8c46 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/EmptyStringToDefault.cs @@ -7,168 +7,167 @@ This Source Code Form is subject to the terms of the Mozilla Public using System; using System.Collections.Generic; using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class EmptyStringToDefaultTest { private const string EmptyPhpStringInput = "s:0:\"\";"; #region Enabled - [TestMethod] + [Fact] public void Enabled_EmptyStringToInt() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_EmptyStringToLong() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToDouble() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToFloat() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToDecimal() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToBool() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToChar() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToEnum() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToGuid() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToString() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToObject() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToCustomObject() { var deserializedObject = PhpSerialization.Deserialize( "a:5:{s:7:\"AString\";s:0:\"\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}" ); - Assert.AreEqual(default, deserializedObject.AString); + Assert.Equal(default, deserializedObject.AString); } - [TestMethod] + [Fact] public void Enabled_StringArrayToCharCollection() { var result = PhpSerialization.Deserialize>("a:2:{i:0;s:0:\"\";i:1;s:0:\"\";}"); - CollectionAssert.AreEqual(new List { default, default }, result); + Assert.Equal(new List { default, default }, result); } #region Nullables - [TestMethod] + [Fact] public void Enabled_EmptyStringToIntNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_EmptyStringToLongNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToDoubleNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToFloatNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToDecimalNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToBoolNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToCharNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToEnumNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } - [TestMethod] + [Fact] public void Enabled_StringToGuidNullable() { var result = PhpSerialization.Deserialize(EmptyPhpStringInput); - Assert.AreEqual(default, result); + Assert.Equal(default, result); } #endregion - [TestMethod] + [Fact] public void Enabled_StringArrayToIntList() { var result = PhpSerialization.Deserialize>("a:1:{i:0;s:0:\"\";}"); - CollectionAssert.AreEqual(new List { default }, result); + Assert.Equal(new List { default }, result); } - [TestMethod] + [Fact] public void Enabled_StringArrayToNullableIntList() { var result = PhpSerialization.Deserialize>("a:1:{i:1;s:0:\"\";}"); - CollectionAssert.AreEqual(new List { default }, result); + Assert.Equal(new List { default }, result); } - [TestMethod] + [Fact] public void Enabled_ClassArrayToClassDictionary() { var result = PhpSerialization.Deserialize>( "a:1:{s:4:\"AKey\";a:5:{s:7:\"AString\";s:0:\"\";s:9:\"AnInteger\";i:0;s:7:\"ADouble\";d:0;s:4:\"True\";b:0;s:5:\"False\";b:0;}}" @@ -192,15 +191,15 @@ public void Enabled_ClassArrayToClassDictionary() { // No easy way to assert dicts in MsTest :/ - Assert.AreEqual(expected.Count, result.Count); + Assert.Equal(expected.Count, result.Count); foreach (var ((expectedKey, expectedValue), (actualKey, actualValue)) in expected.Zip(result)) { - Assert.AreEqual(expectedKey, actualKey); - Assert.AreEqual(expectedValue.ADouble, actualValue.ADouble); - Assert.AreEqual(expectedValue.AString, actualValue.AString); - Assert.AreEqual(expectedValue.AnInteger, actualValue.AnInteger); - Assert.AreEqual(expectedValue.False, actualValue.False); - Assert.AreEqual(expectedValue.True, actualValue.True); + Assert.Equal(expectedKey, actualKey); + Assert.Equal(expectedValue.ADouble, actualValue.ADouble); + Assert.Equal(expectedValue.AString, actualValue.AString); + Assert.Equal(expectedValue.AnInteger, actualValue.AnInteger); + Assert.Equal(expectedValue.False, actualValue.False); + Assert.Equal(expectedValue.True, actualValue.True); } } @@ -208,37 +207,37 @@ public void Enabled_ClassArrayToClassDictionary() { #region Disabled - [TestMethod] + [Fact] public void Disabled_EmptyStringToInt() { - var exception = Assert.ThrowsException( + var exception = Assert.Throws( () => PhpSerialization.Deserialize(EmptyPhpStringInput, new PhpDeserializationOptions { EmptyStringToDefault = false }) ); - Assert.AreEqual( + Assert.Equal( "Exception encountered while trying to assign '' to type Int32. See inner exception for details.", exception.Message ); } - [TestMethod] + [Fact] public void Disabled_StringToBool() { - var exception = Assert.ThrowsException( + var exception = Assert.Throws( () => PhpSerialization.Deserialize(EmptyPhpStringInput, new PhpDeserializationOptions { EmptyStringToDefault = false }) ); - Assert.AreEqual( + Assert.Equal( "Exception encountered while trying to assign '' to type Boolean. See inner exception for details.", exception.Message ); } - [TestMethod] + [Fact] public void Disabled_StringToDouble() { - var exception = Assert.ThrowsException( + var exception = Assert.Throws( () => PhpSerialization.Deserialize(EmptyPhpStringInput, new PhpDeserializationOptions { EmptyStringToDefault = false }) ); - Assert.AreEqual( + Assert.Equal( "Exception encountered while trying to assign '' to type Double. See inner exception for details.", exception.Message ); diff --git a/PhpSerializerNET.Test/Deserialize/Options/EnableTypeLookup.cs b/PhpSerializerNET.Test/Deserialize/Options/EnableTypeLookup.cs index 49b512f..09b714d 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/EnableTypeLookup.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/EnableTypeLookup.cs @@ -4,44 +4,43 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] - public class EnableTypeLookupTest { - [TestMethod] - public void Enabled_Finds_Class() { - var result = PhpSerialization.Deserialize( - "O:11:\"MappedClass\":2:{s:2:\"en\";s:12:\"Hello World!\";s:2:\"de\";s:11:\"Hallo Welt!\";}", - new PhpDeserializationOptions() { EnableTypeLookup = true } - ); - - Assert.IsInstanceOfType(result, typeof(MappedClass)); - - // Check that everything was deserialized onto the properties: - var mappedClass = result as MappedClass; - Assert.AreEqual("Hello World!", mappedClass.English); - Assert.AreEqual("Hallo Welt!", mappedClass.German); - } - - [TestMethod] - public void Disabled_UseStdClass() { - var result = PhpSerialization.Deserialize( - "O:11:\"MappedClass\":2:{s:2:\"en\";s:12:\"Hello World!\";s:2:\"de\";s:11:\"Hallo Welt!\";}", - new PhpDeserializationOptions() { - EnableTypeLookup = false, - StdClass = StdClassOption.Dictionary, - } - ); - - Assert.IsInstanceOfType(result, typeof(PhpSerializerNET.PhpObjectDictionary)); - - // Check that everything was deserialized onto the properties: - var dictionary = result as PhpObjectDictionary; - Assert.AreEqual("Hello World!", dictionary["en"]); - Assert.AreEqual("Hallo Welt!", dictionary["de"]); - } +namespace PhpSerializerNET.Test.Deserialize.Options; +public class EnableTypeLookupTest { + [Fact] + public void Enabled_Finds_Class() { + var result = PhpSerialization.Deserialize( + "O:11:\"MappedClass\":2:{s:2:\"en\";s:12:\"Hello World!\";s:2:\"de\";s:11:\"Hallo Welt!\";}", + new PhpDeserializationOptions() { EnableTypeLookup = true } + ); + + Assert.IsType(result); + + // Check that everything was deserialized onto the properties: + var mappedClass = result as MappedClass; + Assert.Equal("Hello World!", mappedClass.English); + Assert.Equal("Hallo Welt!", mappedClass.German); } + + [Fact] + public void Disabled_UseStdClass() { + var result = PhpSerialization.Deserialize( + "O:11:\"MappedClass\":2:{s:2:\"en\";s:12:\"Hello World!\";s:2:\"de\";s:11:\"Hallo Welt!\";}", + new PhpDeserializationOptions() { + EnableTypeLookup = false, + StdClass = StdClassOption.Dictionary, + } + ); + + Assert.IsType(result); + + // Check that everything was deserialized onto the properties: + var dictionary = result as PhpObjectDictionary; + Assert.Equal("Hello World!", dictionary["en"]); + Assert.Equal("Hallo Welt!", dictionary["de"]); + } + } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Options/InputEncoding.cs b/PhpSerializerNET.Test/Deserialize/Options/InputEncoding.cs index 7eb03a0..fc27396 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/InputEncoding.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/InputEncoding.cs @@ -5,10 +5,9 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System.Text; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class InputEncodingTest { private static readonly string Latin1TestString = Encoding.Latin1.GetString( Encoding.Convert( @@ -18,20 +17,20 @@ public class InputEncodingTest { ) ); - [TestMethod] + [Fact] public void WrongEncodingFails() { - var ex = Assert.ThrowsException( + var ex = Assert.Throws( () => PhpSerialization.Deserialize(Latin1TestString) ); // The deserialization failed, because the length of "äöü" in bytes is 6 in UTF8 but 3 in Latin1, // which results in a misalignment and failure to find the end of the string. // I have cross-checked that the PHP implementation (at least in versions I tested) fails for the same reason. - Assert.AreEqual("Unexpected token at index 8. Expected '\"' but found '¶' instead.", ex.Message); + Assert.Equal("Unexpected token at index 8. Expected '\"' but found '¶' instead.", ex.Message); } - [TestMethod] + [Fact] public void CorrectEncodingWorks() { var result = PhpSerialization.Deserialize( Latin1TestString, @@ -40,7 +39,7 @@ public void CorrectEncodingWorks() { } ); - Assert.AreEqual("äöü", result); + Assert.Equal("äöü", result); } } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Options/NumberStringToBool.cs b/PhpSerializerNET.Test/Deserialize/Options/NumberStringToBool.cs index 0023dbd..e96ce8b 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/NumberStringToBool.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/NumberStringToBool.cs @@ -4,37 +4,36 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class NumberStringToBoolTest { - [TestMethod] + [Fact] public void Enabled_Deserializes_Implicit() { var options = new PhpDeserializationOptions() { NumberStringToBool = true }; - Assert.AreEqual(true, PhpSerialization.Deserialize("s:1:\"1\";", options)); - Assert.AreEqual(false, PhpSerialization.Deserialize("s:1:\"0\";", options)); + Assert.Equal(true, PhpSerialization.Deserialize("s:1:\"1\";", options)); + Assert.Equal(false, PhpSerialization.Deserialize("s:1:\"0\";", options)); } - [TestMethod] + [Fact] public void Enabled_Deserializes_Explicit() { var options = new PhpDeserializationOptions() { NumberStringToBool = true }; - Assert.AreEqual(true, PhpSerialization.Deserialize("s:1:\"1\";", options)); - Assert.AreEqual(false, PhpSerialization.Deserialize("s:1:\"0\";", options)); + Assert.True(PhpSerialization.Deserialize("s:1:\"1\";", options)); + Assert.False(PhpSerialization.Deserialize("s:1:\"0\";", options)); } - [TestMethod] + [Fact] public void Disabled_Throws() { - var exception = Assert.ThrowsException( + var exception = Assert.Throws( () => PhpSerialization.Deserialize( "s:1:\"0\";", new PhpDeserializationOptions() { NumberStringToBool = false } ) ); - Assert.AreEqual( + Assert.Equal( "Exception encountered while trying to assign '0' to type Boolean. See inner exception for details.", exception.Message ); diff --git a/PhpSerializerNET.Test/Deserialize/Options/StdClass.cs b/PhpSerializerNET.Test/Deserialize/Options/StdClass.cs index 236de25..a1b80b1 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/StdClass.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/StdClass.cs @@ -6,88 +6,87 @@ This Source Code Form is subject to the terms of the Mozilla Public using System.Collections; using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class StdClassTest { public struct MyStruct { public double John; public double Jane; } - [TestMethod] + [Fact] public void Option_Throw() { - var ex = Assert.ThrowsException( + var ex = Assert.Throws( () => PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:4:\"John\";d:3.14;s:4:\"Jane\";d:2.718;}", new PhpDeserializationOptions() { StdClass = StdClassOption.Throw } ) ); - Assert.AreEqual( + Assert.Equal( "Encountered 'stdClass' and the behavior 'Throw' was specified in deserialization options.", ex.Message ); } - [TestMethod] + [Fact] public void Option_Dynamic() { dynamic result = (PhpDynamicObject)PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:4:\"John\";d:3.14;s:4:\"Jane\";d:2.718;}", new PhpDeserializationOptions() { StdClass = StdClassOption.Dynamic } ); - Assert.AreEqual(3.14, result.John); - Assert.AreEqual(2.718, result.Jane); - Assert.AreEqual("stdClass", result.GetClassName()); + Assert.Equal(3.14, result.John); + Assert.Equal(2.718, result.Jane); + Assert.Equal("stdClass", result.GetClassName()); } - [TestMethod] + [Fact] public void Option_Dictionary() { var result = (IDictionary)PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:4:\"John\";d:3.14;s:4:\"Jane\";d:2.718;}", new PhpDeserializationOptions() { StdClass = StdClassOption.Dictionary } ); - Assert.AreEqual(3.14, result["John"]); - Assert.AreEqual(2.718, result["Jane"]); + Assert.Equal(3.14, result["John"]); + Assert.Equal(2.718, result["Jane"]); } - [TestMethod] + [Fact] public void Overridden_By_Class() { var result = PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", new PhpDeserializationOptions() { StdClass = StdClassOption.Dynamic } ); - Assert.IsInstanceOfType(result, typeof(NamedClass)); - Assert.AreEqual(3.14, result.Foo); - Assert.AreEqual(2.718, result.Bar); + Assert.IsType(result); + Assert.Equal(3.14, result.Foo); + Assert.Equal(2.718, result.Bar); } - [TestMethod] + [Fact] public void Overridden_By_Struct() { var result = PhpSerialization.Deserialize( "O:8:\"stdClass\":2:{s:4:\"John\";d:3.14;s:4:\"Jane\";d:2.718;}", new PhpDeserializationOptions() { StdClass = StdClassOption.Dynamic } ); - Assert.IsInstanceOfType(result, typeof(MyStruct)); - Assert.AreEqual(3.14, result.John); - Assert.AreEqual(2.718, result.Jane); + Assert.IsType(result); + Assert.Equal(3.14, result.John); + Assert.Equal(2.718, result.Jane); } - [TestMethod] + [Fact] public void Overridden_By_Dictionary() { var result = PhpSerialization.Deserialize>( "O:8:\"stdClass\":2:{s:4:\"John\";d:3.14;s:4:\"Jane\";d:2.718;}", new PhpDeserializationOptions() { StdClass = StdClassOption.Dynamic } ); - Assert.AreEqual(3.14, result["John"]); - Assert.AreEqual(2.718, result["Jane"]); + Assert.Equal(3.14, result["John"]); + Assert.Equal(2.718, result["Jane"]); } } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs b/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs index d74384e..7aa1968 100644 --- a/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs +++ b/PhpSerializerNET.Test/Deserialize/Options/UseLists.cs @@ -5,12 +5,11 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; namespace PhpSerializerNET.Test.Deserialize.Options { - [TestClass] public class UseListsTest { - [TestMethod] + [Fact] public void Option_Never() { var test = PhpSerialization.Deserialize( "a:2:{i:0;s:1:\"a\";i:1;s:1:\"b\";}", @@ -20,13 +19,13 @@ public void Option_Never() { ); var dictionary = test as Dictionary; - Assert.IsNotNull(dictionary); - Assert.AreEqual(2, dictionary.Count); - Assert.AreEqual("a", dictionary[(long)0]); - Assert.AreEqual("b", dictionary[(long)1]); + Assert.NotNull(dictionary); + Assert.Equal(2, dictionary.Count); + Assert.Equal("a", dictionary[(long)0]); + Assert.Equal("b", dictionary[(long)1]); } - [TestMethod] + [Fact] public void Option_Default() { var result = PhpSerialization.Deserialize( "a:2:{i:0;s:1:\"a\";i:1;s:1:\"b\";}", @@ -36,13 +35,13 @@ public void Option_Default() { ); var list = result as List; - Assert.IsNotNull(list); - Assert.AreEqual(2, list.Count); - Assert.AreEqual("a", list[0]); - Assert.AreEqual("b", list[1]); + Assert.NotNull(list); + Assert.Equal(2, list.Count); + Assert.Equal("a", list[0]); + Assert.Equal("b", list[1]); } - [TestMethod] + [Fact] public void Option_Default_NonConsequetive() { // Same option, non-consecutive integer keys: var result = PhpSerialization.Deserialize( @@ -52,15 +51,15 @@ public void Option_Default_NonConsequetive() { } ); - Assert.AreEqual(typeof (Dictionary), result.GetType()); + Assert.Equal(typeof (Dictionary), result.GetType()); var dictionary = result as Dictionary; - Assert.IsNotNull(dictionary); - Assert.AreEqual(2, dictionary.Count); - Assert.AreEqual("a", dictionary[(long)2]); - Assert.AreEqual("b", dictionary[(long)4]); + Assert.NotNull(dictionary); + Assert.Equal(2, dictionary.Count); + Assert.Equal("a", dictionary[(long)2]); + Assert.Equal("b", dictionary[(long)4]); } - [TestMethod] + [Fact] public void Option_OnAllIntegerKeys() { var test = PhpSerialization.Deserialize( "a:2:{i:0;s:1:\"a\";i:1;s:1:\"b\";}", @@ -70,14 +69,14 @@ public void Option_OnAllIntegerKeys() { ); var list = test as List; - Assert.IsNotNull(list); - Assert.AreEqual(2, list.Count); - Assert.AreEqual("a", list[0]); - Assert.AreEqual("b", list[1]); + Assert.NotNull(list); + Assert.Equal(2, list.Count); + Assert.Equal("a", list[0]); + Assert.Equal("b", list[1]); } - [TestMethod] + [Fact] public void Option_OnAllIntegerKeys_NonConsequetive() { // Same option, non-consecutive integer keys: var result = PhpSerialization.Deserialize( @@ -88,10 +87,10 @@ public void Option_OnAllIntegerKeys_NonConsequetive() { ); var list = result as List; - Assert.IsNotNull(list); - Assert.AreEqual(2, list.Count); - Assert.AreEqual("a", list[0]); - Assert.AreEqual("b", list[1]); + Assert.NotNull(list); + Assert.Equal(2, list.Count); + Assert.Equal("a", list[0]); + Assert.Equal("b", list[1]); } } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/PhpDateTimeDeserialization.cs b/PhpSerializerNET.Test/Deserialize/PhpDateTimeDeserialization.cs index 37348f1..b5ce99e 100644 --- a/PhpSerializerNET.Test/Deserialize/PhpDateTimeDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/PhpDateTimeDeserialization.cs @@ -4,22 +4,21 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class PhpDateTimeDeserializationTest { - [TestMethod] - public void DeserializesCorrectly() { - var result = PhpSerialization.Deserialize( - "O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2021-08-18 09:10:23.441055\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}" - ); +namespace PhpSerializerNET.Test.Deserialize; - Assert.IsInstanceOfType(result, typeof(PhpDateTime)); - var date = result as PhpDateTime; - Assert.AreEqual("UTC", date.Timezone); - Assert.AreEqual("2021-08-18 09:10:23.441055", date.Date); - Assert.AreEqual("DateTime", date.GetClassName()); - } +public class PhpDateTimeDeserializationTest { + [Fact] + public void DeserializesCorrectly() { + var result = PhpSerialization.Deserialize( + "O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2021-08-18 09:10:23.441055\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}" + ); + + Assert.IsType(result); + var date = result as PhpDateTime; + Assert.Equal("UTC", date.Timezone); + Assert.Equal("2021-08-18 09:10:23.441055", date.Date); + Assert.Equal("DateTime", date.GetClassName()); } } diff --git a/PhpSerializerNET.Test/Deserialize/StringDeserialization.cs b/PhpSerializerNET.Test/Deserialize/StringDeserialization.cs index 85c4d93..d57e80a 100644 --- a/PhpSerializerNET.Test/Deserialize/StringDeserialization.cs +++ b/PhpSerializerNET.Test/Deserialize/StringDeserialization.cs @@ -6,94 +6,65 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize { - [TestClass] - public class StringDeserializationTest { - [TestMethod] - public void SerializeHelloWorld() { - Assert.AreEqual( - "s:12:\"Hello World!\";", - PhpSerialization.Serialize("Hello World!") - ); - } +namespace PhpSerializerNET.Test.Deserialize; - [TestMethod] - public void DeserializeEmptyStringExplicit() { - - Assert.AreEqual( - "", - PhpSerialization.Deserialize("s:0:\"\";", new PhpDeserializationOptions { - EmptyStringToDefault = false - }) - ); - - Assert.AreEqual( - default, - PhpSerialization.Deserialize("s:0:\"\";", new PhpDeserializationOptions { - EmptyStringToDefault = true - }) - ); - } - - [TestMethod] - public void DeserializeHelloWorld() { - Assert.AreEqual( - "Hello World!", - PhpSerialization.Deserialize("s:12:\"Hello World!\";") - ); - } - - [TestMethod] - public void DeserializeEmptyString() { - Assert.AreEqual( - "", - PhpSerialization.Deserialize("s:0:\"\";") - ); - } - - [TestMethod] - public void DeserializeUmlauts() { - Assert.AreEqual( - "äöüßÄÖÜ", - PhpSerialization.Deserialize("s:14:\"äöüßÄÖÜ\";") - ); - } +public class StringDeserializationTest { + [Theory] + [InlineData("s:12:\"Hello World!\";", "Hello World!")] + [InlineData("s:0:\"\";", "")] + [InlineData("s:14:\"äöüßÄÖÜ\";", "äöüßÄÖÜ")] + [InlineData("s:4:\"👻\";", "👻")] + [InlineData("s:9:\"_\";s:1:\"_\";", "_\";s:1:\"_")] // // This is really how the PHP implementation behaves. + public void DeserializesCorrectly(string input, string expected) { + Assert.Equal( + expected, PhpSerialization.Deserialize(input) + ); + } - [TestMethod] - public void DeserializeEmoji() { - Assert.AreEqual( - "👻", - PhpSerialization.Deserialize("s:4:\"👻\";") - ); - } + [Theory] + [InlineData("Hello World!", "s:12:\"Hello World!\";")] + [InlineData("", "s:0:\"\";")] + [InlineData("äöüßÄÖÜ", "s:14:\"äöüßÄÖÜ\";")] + [InlineData("👻", "s:4:\"👻\";")] + [InlineData("_\";s:1:\"_", "s:9:\"_\";s:1:\"_\";")] + public void SerializesCorrectly(string input, string expected) { + Assert.Equal( + expected, PhpSerialization.Serialize(input) + ); + } - [TestMethod] - public void DeserializeHalfnesting() { - // This is really how the PHP implementation behaves. - Assert.AreEqual( - "_\";s:1:\"_", - PhpSerialization.Deserialize("s:9:\"_\";s:1:\"_\";") - ); - } + [Fact] + public void DeserializeEmptyStringExplicit() { + Assert.Equal( + "", + PhpSerialization.Deserialize("s:0:\"\";", new PhpDeserializationOptions { + EmptyStringToDefault = false + }) + ); + Assert.Null( + PhpSerialization.Deserialize("s:0:\"\";", new PhpDeserializationOptions { + EmptyStringToDefault = true + }) + ); + } - [TestMethod] - public void ExplicitToGuid() { - Guid guid = PhpSerialization.Deserialize("s:36:\"82e2ebf0-43e6-4c10-82cf-57d60383a6be\";"); - Assert.AreEqual("82e2ebf0-43e6-4c10-82cf-57d60383a6be", guid.ToString()); - } + [Fact] + public void ExplicitToGuid() { + Guid guid = PhpSerialization.Deserialize("s:36:\"82e2ebf0-43e6-4c10-82cf-57d60383a6be\";"); + Assert.Equal("82e2ebf0-43e6-4c10-82cf-57d60383a6be", guid.ToString()); + } - [TestMethod] - public void DeserializesStringToGuidProperty() { - var result = PhpSerialization.Deserialize( - "a:1:{s:4:\"Guid\";s:36:\"82e2ebf0-43e6-4c10-82cf-57d60383a6be\";}" - ); - Assert.AreEqual( - new Guid("82e2ebf0-43e6-4c10-82cf-57d60383a6be"), - result.Guid - ); - } + [Fact] + public void DeserializesStringToGuidProperty() { + var result = PhpSerialization.Deserialize( + "a:1:{s:4:\"Guid\";s:36:\"82e2ebf0-43e6-4c10-82cf-57d60383a6be\";}" + ); + Assert.Equal( + new Guid("82e2ebf0-43e6-4c10-82cf-57d60383a6be"), + result.Guid + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs index 51c7bee..cf0ddf4 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestArrayValidation.cs @@ -4,60 +4,21 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestArrayValidation { - - [TestMethod] - public void ThrowsOnMalformedArray() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("a")); - Assert.AreEqual("Unexpected end of input. Expected ':' at index 1, but input ends at index 0", ex.Message); - - } - - - [TestMethod] - public void ThrowsOnInvalidLength() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("a:-1:{};")); - Assert.AreEqual("Array at position 2 has illegal, missing or malformed length.", ex.Message); - } - - - [TestMethod] - public void ThrowsOnMissingBracket() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("a:100:};")); - Assert.AreEqual("Unexpected token at index 6. Expected '{' but found '}' instead.", ex.Message); - - - } - - [TestMethod] - public void ThrowsOnMissingColon() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("a:10000 ")); - Assert.AreEqual("Array at position 7 has illegal, missing or malformed length.", ex.Message); - - } - - [TestMethod] - public void ThrowsOnAbruptEOF() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("a:10000:")); - Assert.AreEqual("Unexpected end of input. Expected '{' at index 8, but input ends at index 7", ex.Message); - - - ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("a:1000000")); - Assert.AreEqual("Unexpected token at index 8. Expected ':' but found '0' instead.", ex.Message); - - } - - [TestMethod] - public void ThrowsOnFalseLength() { - var exception = Assert.ThrowsException( - () => PhpSerialization.Deserialize("a:2:{i:0;i:0;i:1;i:1;i:2;i:2;}") - ); - - Assert.AreEqual("Array at position 0 should be of length 2, but actual length is 3 or more.", exception.Message); - } +using Xunit; + +namespace PhpSerializerNET.Test.Deserialize.Validation; + +public class TestArrayValidation { + [Theory] + [InlineData("a", "Unexpected end of input. Expected ':' at index 1, but input ends at index 0")] + [InlineData("a:-1:{};", "Array at position 2 has illegal, missing or malformed length.")] + [InlineData("a:100:};", "Unexpected token at index 6. Expected '{' but found '}' instead.")] + [InlineData("a:10000 ", "Array at position 7 has illegal, missing or malformed length.")] + [InlineData("a:10000:", "Unexpected end of input. Expected '{' at index 8, but input ends at index 7")] + [InlineData("a:1000000", "Unexpected token at index 8. Expected ':' but found '0' instead.")] + [InlineData("a:2:{i:0;i:0;i:1;i:1;i:2;i:2;}", "Array at position 0 should be of length 2, but actual length is 3 or more.")] + public void ThrowsOnMalformedArray(string input, string exceptionMessage) { + var ex = Assert.Throws(() => PhpSerialization.Deserialize(input)); + Assert.Equal(exceptionMessage, ex.Message); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestBoolValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestBoolValidation.cs index 20f6df8..d64d1fe 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestBoolValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestBoolValidation.cs @@ -1,42 +1,21 @@ - /** 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 Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestBoolValidation { - [TestMethod] - public void ThrowsOnTruncatedInput() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("b")); - Assert.AreEqual( - "Unexpected end of input. Expected ':' at index 1, but input ends at index 0", - ex.Message - ); +using Xunit; - ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("b:1")); - Assert.AreEqual( - "Unexpected end of input. Expected ';' at index 3, but input ends at index 2", - ex.Message - ); - ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("b:")); - Assert.AreEqual( - "Unexpected end of input. Expected '0' or '1' at index 2, but input ends at index 1", - ex.Message - ); - } +namespace PhpSerializerNET.Test.Deserialize.Validation; - [TestMethod] - public void ThrowsOnInvalidValue() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("b:2;")); - Assert.AreEqual( - "Unexpected token in boolean at index 2. Expected either '1' or '0', but found '2' instead.", - ex.Message - ); - } +public class TestBoolValidation { + [Theory] + [InlineData("b", "Unexpected end of input. Expected ':' at index 1, but input ends at index 0")] + [InlineData("b:1", "Unexpected end of input. Expected ';' at index 3, but input ends at index 2")] + [InlineData("b:", "Unexpected end of input. Expected '0' or '1' at index 2, but input ends at index 1")] + [InlineData("b:2;", "Unexpected token in boolean at index 2. Expected either '1' or '0', but found '2' instead.")] + public void ThrowsOnMalformeBool(string input, string exceptionMessage) { + var ex = Assert.Throws(() => PhpSerialization.Deserialize(input)); + Assert.Equal(exceptionMessage, ex.Message); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestDoubleValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestDoubleValidation.cs index 22129b7..a1a0660 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestDoubleValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestDoubleValidation.cs @@ -4,39 +4,18 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestDoubleValidation { - [TestMethod] - public void ThrowsOnTruncatedInput() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("d")); - Assert.AreEqual("Unexpected end of input. Expected ':' at index 1, but input ends at index 0", ex.Message); - - } - - [TestMethod] - public void ThrowsOnMissingColon() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("d ")); - Assert.AreEqual("Unexpected token at index 1. Expected ':' but found ' ' instead.", ex.Message); - - } - - [TestMethod] - public void ThrowsOnMissingSemicolon() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("d:111111")); - Assert.AreEqual("Unexpected end of input. Expected ':' at index 7, but input ends at index 7", ex.Message); - } - - - [TestMethod] - public void ThrowsOnInvalidCharacter() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("d:bgg5;")); - Assert.AreEqual( - "Unexpected token at index 2. 'b' is not a valid part of a floating point number.", - ex.Message - ); - } +using Xunit; + +namespace PhpSerializerNET.Test.Deserialize.Validation; +public class TestDoubleValidation { + [Theory] + [InlineData("d", "Unexpected end of input. Expected ':' at index 1, but input ends at index 0")] + [InlineData("b ", "Unexpected token at index 1. Expected ':' but found ' ' instead.")] + [InlineData("d:111111", "Unexpected end of input. Expected ':' at index 7, but input ends at index 7")] + [InlineData("d:bgg5;", "Unexpected token at index 2. 'b' is not a valid part of a floating point number.")] + [InlineData("d:;", "Unexpected token at index 2: Expected floating point number, but found ';' instead.")] + public void ThrowsOnMalformedDouble(string input, string exceptionMessage) { + var ex = Assert.Throws(() => PhpSerialization.Deserialize(input)); + Assert.Equal(exceptionMessage, ex.Message); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestIntegerValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestIntegerValidation.cs index 658ecf0..e574c4c 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestIntegerValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestIntegerValidation.cs @@ -1,46 +1,27 @@ - /** 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 Xunit; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestIntegerValidation { - [TestMethod] - public void ThrowsOnTruncatedInput() { - // var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("i")); - // Assert.AreEqual( - // "Unexpected end of input. Expected ':' at index 1, but input ends at index 0", - // ex.Message - // ); - - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("i:1")); - Assert.AreEqual( - "Unexpected end of input. Expected ':' at index 2, but input ends at index 2", - ex.Message - ); - } +namespace PhpSerializerNET.Test.Deserialize.Validation; - [TestMethod] - public void ThrowsOnInvalidValue() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("i:12345b;")); - Assert.AreEqual( - "Unexpected token at index 7. 'b' is not a valid part of a number.", - ex.Message - ); - } - - [TestMethod] - public void ThrowOnMissingSemicolon() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("i:12345")); - Assert.AreEqual( - "Unexpected end of input. Expected ':' at index 6, but input ends at index 6", - ex.Message - ); - } +public class TestIntegerValidation { + [Theory] + [InlineData("i:;", "Unexpected token at index 2: Expected number, but found ';' instead.")] + [InlineData("i:INF;", "Unexpected token at index 2. 'I' is not a valid part of a number.")] + [InlineData("i:NaN;", "Unexpected token at index 2. 'N' is not a valid part of a number.")] + [InlineData("i:12345b:;", "Unexpected token at index 7. 'b' is not a valid part of a number.")] + [InlineData("i:12345.;", "Unexpected token at index 7. '.' is not a valid part of a number.")] + public void ThrowsOnMalformedInteger(string input, string exceptionMessage) { + var exception = Assert.Throws(() => { + PhpSerialization.Deserialize(input); + }); + Assert.Equal( + exceptionMessage, + exception.Message + ); } -} \ No newline at end of file + +} diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestNullValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestNullValidation.cs index 2792c41..164ff43 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestNullValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestNullValidation.cs @@ -5,19 +5,16 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestNullValidation { - [TestMethod] - public void ThrowsOnTruncatedInput() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("N")); - Assert.AreEqual("Unexpected end of input. Expected ';' at index 1, but input ends at index 0", ex.Message); - - ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("N?")); - Assert.AreEqual("Unexpected token at index 1. Expected ';' but found '?' instead.", ex.Message); - } +namespace PhpSerializerNET.Test.Deserialize.Validation; +public class TestNullValidation { + [Theory] + [InlineData("N", "Unexpected end of input. Expected ';' at index 1, but input ends at index 0")] + [InlineData("N?", "Unexpected token at index 1. Expected ';' but found '?' instead.")] + public void ThrowsOnTruncatedInput(string input, string exceptionMessage) { + var ex = Assert.Throws(() => PhpSerialization.Deserialize(input)); + Assert.Equal(exceptionMessage, ex.Message); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestObjectValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestObjectValidation.cs index d2d8c05..1be27e3 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestObjectValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestObjectValidation.cs @@ -4,104 +4,46 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestObjectValidation { - [TestMethod] - public void ErrorOnInvalidNameLength() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:-1:\"stdClass\":1:{s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Object at position 2 has illegal, missing or malformed length.", - ex.Message - ); - - ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:200:\"stdClass\":1:{s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Illegal length of 200. The string at position 7 points to out of bounds index 207.", - ex.Message - ); - } - - [TestMethod] - public void ErrorOnInvalidName() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:8:\"stdClass:1:{s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Unexpected token at index 13. Expected '\"' but found ':' instead.", - ex.Message - ); - - ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:2:stdClass\":1:{s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Unexpected token at index 4. Expected '\"' but found 's' instead.", - ex.Message - ); - } - - [TestMethod] - public void ThrowsOnFalseLength() { - var exception = Assert.ThrowsException( - () => PhpSerialization.Deserialize("O:1:\"a\":2:{i:0;i:0;i:1;i:1;i:2;i:2;}") - ); - - Assert.AreEqual("Object at position 0 should have 2 properties, but actually has 3 or more properties.", exception.Message); - } - - [TestMethod] - public void ErrorOnInvalidSyntax() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:8:\"stdClass\"1:{s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Unexpected token at index 14. Expected ':' but found '1' instead.", - ex.Message - ); - - ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:8:\"stdClass\":1{s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Object at position 16 has illegal, missing or malformed length.", - ex.Message - ); - - ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize( - "O:8:\"stdClass\":1:s:3:\"Foo\";N;}" - ) - ); - - Assert.AreEqual( - "Unexpected token at index 17. Expected '{' but found 's' instead.", - ex.Message - ); - } +using Xunit; + +namespace PhpSerializerNET.Test.Deserialize.Validation; + +public class TestObjectValidation { + [Theory] + [InlineData( + "O:-1:\"stdClass\":1:{s:3:\"Foo\";N;}", + "Object at position 2 has illegal, missing or malformed length." + )] + [InlineData( + "O:200:\"stdClass\":1:{s:3:\"Foo\";N;}", + "Illegal length of 200. The string at position 7 points to out of bounds index 207." + )] + [InlineData( + "O:8:\"stdClass:1:{s:3:\"Foo\";N;}", + "Unexpected token at index 13. Expected '\"' but found ':' instead." + )] + [InlineData( + "O:2:stdClass\":1:{s:3:\"Foo\";N;}", + "Unexpected token at index 4. Expected '\"' but found 's' instead." + )] + [InlineData( + "O:1:\"a\":2:{i:0;i:0;i:1;i:1;i:2;i:2;}", + "Object at position 0 should have 2 properties, but actually has 3 or more properties." + )] + [InlineData( + "O:8:\"stdClass\"1:{s:3:\"Foo\";N;}", + "Unexpected token at index 14. Expected ':' but found '1' instead." + )] + [InlineData( + "O:8:\"stdClass\":1{s:3:\"Foo\";N;}", + "Object at position 16 has illegal, missing or malformed length." + )] + [InlineData( + "O:8:\"stdClass\":1:s:3:\"Foo\";N;}", + "Unexpected token at index 17. Expected '{' but found 's' instead." + )] + public void ThrowsOnMalformedObject(string input, string exceptionMessage) { + var ex = Assert.Throws(() => PhpSerialization.Deserialize(input)); + Assert.Equal(exceptionMessage, ex.Message); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestOtherErrors.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestOtherErrors.cs index 639fee6..3e86ecc 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestOtherErrors.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestOtherErrors.cs @@ -5,73 +5,72 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestOtherErrors { - [TestMethod] - public void ThrowsOnUnexpectedToken() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("_")); - Assert.AreEqual("Unexpected token '_' at position 0.", ex.Message); - - ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("i:42;_")); - Assert.AreEqual("Unexpected token '_' at position 5.", ex.Message); - - ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("_i:42;")); - Assert.AreEqual("Unexpected token '_' at position 0.", ex.Message); - } - - [TestMethod] - public void ErrorOnTuple() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize("s:7:\"AString\";s:7:\"AString\";") - ); - - Assert.AreEqual("Unexpected token 's' at position 14.", ex.Message); - } - - [TestMethod] - public void ErrorOnEmptyInput() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize("") - ); - - const string expected = "PhpSerialization.Deserialize(): Parameter 'input' must not be null or empty. (Parameter 'input')"; - Assert.AreEqual(expected, ex.Message); - - ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize("") - ); - - Assert.AreEqual(expected, ex.Message); - - ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize("", typeof(string)) - ); - - Assert.AreEqual(expected, ex.Message); - } - - - [TestMethod] - public void ThrowOnIllegalKeyType() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize("O:8:\"stdClass\":1:{b:1;s:4:\"true\";}") - ); - Assert.AreEqual( - "Error encountered deserizalizing an object of type 'PhpSerializerNET.Test.DataTypes.MyPhpObject': " + - "The key '1' (from the token at position 18) has an unsupported type of 'Boolean'.", - ex.Message - ); - } - - [TestMethod] - public void ThrowOnIntegerKeyPhpObject() { - var ex = Assert.ThrowsException( - () => PhpSerialization.Deserialize("O:8:\"stdClass\":1:{i:0;s:4:\"true\";}") - ); - } +namespace PhpSerializerNET.Test.Deserialize.Validation; + +public class TestOtherErrors { + [Fact] + public void ThrowsOnUnexpectedToken() { + var ex = Assert.Throws(() => PhpSerialization.Deserialize("_")); + Assert.Equal("Unexpected token '_' at position 0.", ex.Message); + + ex = Assert.Throws(() => PhpSerialization.Deserialize("i:42;_")); + Assert.Equal("Unexpected token '_' at position 5.", ex.Message); + + ex = Assert.Throws(() => PhpSerialization.Deserialize("_i:42;")); + Assert.Equal("Unexpected token '_' at position 0.", ex.Message); + } + + [Fact] + public void ErrorOnTuple() { + var ex = Assert.Throws( + () => PhpSerialization.Deserialize("s:7:\"AString\";s:7:\"AString\";") + ); + + Assert.Equal("Unexpected token 's' at position 14.", ex.Message); + } + + [Fact] + public void ErrorOnEmptyInput() { + var ex = Assert.Throws( + () => PhpSerialization.Deserialize("") + ); + + const string expected = "PhpSerialization.Deserialize(): Parameter 'input' must not be null or empty. (Parameter 'input')"; + Assert.Equal(expected, ex.Message); + + ex = Assert.Throws( + () => PhpSerialization.Deserialize("") + ); + + Assert.Equal(expected, ex.Message); + + ex = Assert.Throws( + () => PhpSerialization.Deserialize("", typeof(string)) + ); + + Assert.Equal(expected, ex.Message); + } + + + [Fact] + public void ThrowOnIllegalKeyType() { + var ex = Assert.Throws( + () => PhpSerialization.Deserialize("O:8:\"stdClass\":1:{b:1;s:4:\"true\";}") + ); + Assert.Equal( + "Error encountered deserizalizing an object of type 'PhpSerializerNET.Test.DataTypes.MyPhpObject': " + + "The key '1' (from the token at position 18) has an unsupported type of 'Boolean'.", + ex.Message + ); + } + + [Fact] + public void ThrowOnIntegerKeyPhpObject() { + var ex = Assert.Throws( + () => PhpSerialization.Deserialize("O:8:\"stdClass\":1:{i:0;s:4:\"true\";}") + ); } } diff --git a/PhpSerializerNET.Test/Deserialize/Validation/TestStringValidation.cs b/PhpSerializerNET.Test/Deserialize/Validation/TestStringValidation.cs index e3c0e51..b329b18 100644 --- a/PhpSerializerNET.Test/Deserialize/Validation/TestStringValidation.cs +++ b/PhpSerializerNET.Test/Deserialize/Validation/TestStringValidation.cs @@ -4,57 +4,21 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Deserialize.Validation { - [TestClass] - public class TestStringValidation { - [TestMethod] - public void ThrowsOnTruncatedInput() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("s")); - Assert.AreEqual("Unexpected end of input. Expected ':' at index 1, but input ends at index 0", ex.Message); - } +namespace PhpSerializerNET.Test.Deserialize.Validation; - [TestMethod] - public void ThrowsOnMissingStartQuote() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("s:3:abc\";")); - Assert.AreEqual( - "Unexpected token at index 4. Expected '\"' but found 'a' instead.", - ex.Message - ); - } - - [TestMethod] - public void ThrowsOnMissingEndQuote() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("s:3:\"abc;")); - Assert.AreEqual( - "Unexpected token at index 8. Expected '\"' but found ';' instead.", - ex.Message - ); - } - - [TestMethod] - public void ThrowsOnInvalidLength() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("s:3\"abc\";")); - Assert.AreEqual( - "String at position 3 has illegal, missing or malformed length.", - ex.Message - ); - } - - [TestMethod] - public void ThrowsOnOutOfBoundsLength() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("s:10:\"abc\";")); - Assert.AreEqual( - "Illegal length of 10. The string at position 6 points to out of bounds index 16.", - ex.Message - ); - } - - [TestMethod] - public void ThrowsOnMissingSemicolon() { - var ex = Assert.ThrowsException(() => PhpSerialization.Deserialize("s:3:\"abc\"")); - Assert.AreEqual("Unexpected end of input. Expected ';' at index 9, but input ends at index 8", ex.Message); - } +public class TestStringValidation { + [Theory] + [InlineData("s", "Unexpected end of input. Expected ':' at index 1, but input ends at index 0")] + [InlineData("s:3:abc\";", "Unexpected token at index 4. Expected '\"' but found 'a' instead.")] + [InlineData("s:3:\"abc;", "Unexpected token at index 8. Expected '\"' but found ';' instead.")] + [InlineData("s:3\"abc\";", "String at position 3 has illegal, missing or malformed length.")] + [InlineData("s:_:\"abc\";", "String at position 2 has illegal, missing or malformed length.")] + [InlineData("s:10:\"abc\";", "Illegal length of 10. The string at position 6 points to out of bounds index 16.")] + [InlineData("s:3:\"abc\"", "Unexpected end of input. Expected ';' at index 9, but input ends at index 8")] + public void ThrowsOnMalformedString(string input, string exceptionMessage) { + var ex = Assert.Throws(() => PhpSerialization.Deserialize(input)); + Assert.Equal(exceptionMessage, ex.Message); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Other/DeserializeWithRuntimeType.cs b/PhpSerializerNET.Test/Other/DeserializeWithRuntimeType.cs index 9b921dc..a1baf85 100644 --- a/PhpSerializerNET.Test/Other/DeserializeWithRuntimeType.cs +++ b/PhpSerializerNET.Test/Other/DeserializeWithRuntimeType.cs @@ -4,47 +4,46 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test { - [TestClass] - public partial class DeserializeWithRuntimeTypeTest { - - [TestMethod] - public void DeserializeObjectWithRuntimeType() { - var expectedType = typeof(NamedClass); - var result = PhpSerialization.Deserialize("O:8:\"stdClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", expectedType); - - Assert.IsNotNull(result); - Assert.IsInstanceOfType(result, expectedType); - - Assert.AreEqual( - 3.14, - ((NamedClass)result).Foo - ); - Assert.AreEqual( - 2.718, - ((NamedClass)result).Bar - ); - } - - [TestMethod] - public void DeserializeArrayWithRuntimeType() { - var expectedType = typeof(NamedClass); - var result = PhpSerialization.Deserialize("a:2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", expectedType); - - Assert.IsNotNull(result); - Assert.IsInstanceOfType(result, expectedType); - - Assert.AreEqual( - 3.14, - ((NamedClass)result).Foo - ); - Assert.AreEqual( - 2.718, - ((NamedClass)result).Bar - ); - } +namespace PhpSerializerNET.Test; + +public partial class DeserializeWithRuntimeTypeTest { + + [Fact] + public void DeserializeObjectWithRuntimeType() { + var expectedType = typeof(NamedClass); + var result = PhpSerialization.Deserialize("O:8:\"stdClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", expectedType); + + Assert.NotNull(result); + Assert.IsType(expectedType, result); + + Assert.Equal( + 3.14, + ((NamedClass)result).Foo + ); + Assert.Equal( + 2.718, + ((NamedClass)result).Bar + ); + } + + [Fact] + public void DeserializeArrayWithRuntimeType() { + var expectedType = typeof(NamedClass); + var result = PhpSerialization.Deserialize("a:2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", expectedType); + + Assert.NotNull(result); + Assert.IsType(expectedType, result); + + Assert.Equal( + 3.14, + ((NamedClass)result).Foo + ); + Assert.Equal( + 2.718, + ((NamedClass)result).Bar + ); } } diff --git a/PhpSerializerNET.Test/Other/PhpDateTimeTest.cs b/PhpSerializerNET.Test/Other/PhpDateTimeTest.cs index 2399c75..3a9228f 100644 --- a/PhpSerializerNET.Test/Other/PhpDateTimeTest.cs +++ b/PhpSerializerNET.Test/Other/PhpDateTimeTest.cs @@ -5,20 +5,19 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Other { - [TestClass] - public class PhpDateTimeTest { - [TestMethod] - public void ThrowsOnSetClassName() { - var testObject = new PhpDateTime(); +namespace PhpSerializerNET.Test.Other; - var ex = Assert.ThrowsException(() => { - testObject.SetClassName("stdClass"); - }); - Assert.AreEqual("Cannot set name on object of type PhpDateTime name is of constant DateTime", ex.Message); - } +public class PhpDateTimeTest { + [Fact] + public void ThrowsOnSetClassName() { + var testObject = new PhpDateTime(); + var ex = Assert.Throws(() => { + testObject.SetClassName("stdClass"); + }); + Assert.Equal("Cannot set name on object of type PhpDateTime name is of constant DateTime", ex.Message); } + } diff --git a/PhpSerializerNET.Test/Other/PhpDynamicObjectTest.cs b/PhpSerializerNET.Test/Other/PhpDynamicObjectTest.cs index c6d4201..f1385c7 100644 --- a/PhpSerializerNET.Test/Other/PhpDynamicObjectTest.cs +++ b/PhpSerializerNET.Test/Other/PhpDynamicObjectTest.cs @@ -4,25 +4,24 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Other { - [TestClass] - public class PhpDynamicObjectTest { - [TestMethod] - public void CanReadAndWriteProps() { - dynamic testObject = new PhpDynamicObject(); +namespace PhpSerializerNET.Test.Other; - testObject.foo = "Foo"; - Assert.AreEqual("Foo", testObject.foo); - } +public class PhpDynamicObjectTest { + [Fact] + public void CanReadAndWriteProps() { + dynamic testObject = new PhpDynamicObject(); - [TestMethod] - public void GetAndSetClassname() { - dynamic testObject = new PhpDynamicObject(); + testObject.foo = "Foo"; + Assert.Equal("Foo", testObject.foo); + } + + [Fact] + public void GetAndSetClassname() { + dynamic testObject = new PhpDynamicObject(); - testObject.SetClassName("MyClass"); - Assert.AreEqual("MyClass", testObject.GetClassName()); - } + testObject.SetClassName("MyClass"); + Assert.Equal("MyClass", testObject.GetClassName()); } } diff --git a/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj b/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj index ea2fd58..7359fc0 100644 --- a/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj +++ b/PhpSerializerNET.Test/PhpSerializerNET.Test.csproj @@ -4,15 +4,18 @@ false - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + - + \ No newline at end of file diff --git a/PhpSerializerNET.Test/Serialize/ArraySerialization.cs b/PhpSerializerNET.Test/Serialize/ArraySerialization.cs index 7908610..39bb442 100644 --- a/PhpSerializerNET.Test/Serialize/ArraySerialization.cs +++ b/PhpSerializerNET.Test/Serialize/ArraySerialization.cs @@ -4,24 +4,23 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; namespace PhpSerializerNET.Test.Serialize { - [TestClass] public class ArraySerialization { - [TestMethod] + [Fact] + public void StringArraySerializaton() { - string[] data = new string[3] { "a", "b", "c" }; + string[] data = ["a", "b", "c"]; - Assert.AreEqual( + Assert.Equal( "a:3:{i:0;s:1:\"a\";i:1;s:1:\"b\";i:2;s:1:\"c\";}", PhpSerialization.Serialize(data) ); } - [TestMethod] + [Fact] public void ObjectIntoMixedKeyArray() { var data = new MixedKeysObject() { Foo = "Foo", @@ -30,10 +29,10 @@ public void ObjectIntoMixedKeyArray() { Dummy = "B", }; - Assert.AreEqual( + Assert.Equal( "a:4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}", PhpSerialization.Serialize(data) ); } } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/BooleanSerialization.cs b/PhpSerializerNET.Test/Serialize/BooleanSerialization.cs index bfa4c65..09e8b7b 100644 --- a/PhpSerializerNET.Test/Serialize/BooleanSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/BooleanSerialization.cs @@ -5,25 +5,15 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class BooleanSerializationTest { - [TestMethod] - public void SerializesTrue() { - Assert.AreEqual( - "b:1;", - PhpSerialization.Serialize(true) - ); - } - - [TestMethod] - public void SerializesFalse() { - Assert.AreEqual( - "b:0;", - PhpSerialization.Serialize(false) - ); - } +namespace PhpSerializerNET.Test.Serialize; + +public class BooleanSerializationTest { + [Theory] + [InlineData(true, "b:1;")] + [InlineData(false, "b:0;")] + public void Serializes(bool input, string output) { + Assert.Equal(output, PhpSerialization.Serialize(input)); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/CircularReferences.cs b/PhpSerializerNET.Test/Serialize/CircularReferences.cs index 43b7028..344c087 100644 --- a/PhpSerializerNET.Test/Serialize/CircularReferences.cs +++ b/PhpSerializerNET.Test/Serialize/CircularReferences.cs @@ -6,61 +6,60 @@ This Source Code Form is subject to the terms of the Mozilla Public using System; using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class CircularReferencesTest { - private class CircularClass { - public string Foo { get; set; } - public CircularClass Bar { get; set; } - } +namespace PhpSerializerNET.Test.Serialize; - [TestMethod] - public void SerializeCircularObject() { - var testObject = new CircularClass() { - Foo = "First" - }; - testObject.Bar = new CircularClass() { - Foo = "Second", - Bar = testObject - }; +public class CircularReferencesTest { + private class CircularClass { + public string Foo { get; set; } + public CircularClass Bar { get; set; } + } + + [Fact] + public void SerializeCircularObject() { + var testObject = new CircularClass() { + Foo = "First" + }; + testObject.Bar = new CircularClass() { + Foo = "Second", + Bar = testObject + }; - Assert.AreEqual( - "a:2:{s:3:\"Foo\";s:5:\"First\";s:3:\"Bar\";a:2:{s:3:\"Foo\";s:6:\"Second\";s:3:\"Bar\";N;}}", - PhpSerialization.Serialize(testObject) - ); - } + Assert.Equal( + "a:2:{s:3:\"Foo\";s:5:\"First\";s:3:\"Bar\";a:2:{s:3:\"Foo\";s:6:\"Second\";s:3:\"Bar\";N;}}", + PhpSerialization.Serialize(testObject) + ); + } - [TestMethod] - public void ThrowOnCircularReferencesOption() { - var testObject = new CircularClass() { - Foo = "First" - }; - testObject.Bar = new CircularClass() { - Foo = "Second", - Bar = testObject - }; + [Fact] + public void ThrowOnCircularReferencesOption() { + var testObject = new CircularClass() { + Foo = "First" + }; + testObject.Bar = new CircularClass() { + Foo = "Second", + Bar = testObject + }; - var ex = Assert.ThrowsException( - () => PhpSerialization.Serialize(testObject, new PhpSerializiationOptions(){ ThrowOnCircularReferences = true}) - ); - Assert.AreEqual( - "Input object has a circular reference.", - ex.Message - ); - } + var ex = Assert.Throws( + () => PhpSerialization.Serialize(testObject, new PhpSerializiationOptions() { ThrowOnCircularReferences = true }) + ); + Assert.Equal( + "Input object has a circular reference.", + ex.Message + ); + } - [TestMethod] - public void SerializeCircularList() { - List listA = new() { "A", "B" }; - List listB = new() { "C", "D", listA }; - listA.Add(listB); + [Fact] + public void SerializeCircularList() { + List listA = new() { "A", "B" }; + List listB = new() { "C", "D", listA }; + listA.Add(listB); - Assert.AreEqual( // strings: - "a:3:{i:0;s:1:\"A\";i:1;s:1:\"B\";i:2;a:3:{i:0;s:1:\"C\";i:1;s:1:\"D\";i:2;N;}}", - PhpSerialization.Serialize(listA) - ); - } + Assert.Equal( // strings: + "a:3:{i:0;s:1:\"A\";i:1;s:1:\"B\";i:2;a:3:{i:0;s:1:\"C\";i:1;s:1:\"D\";i:2;N;}}", + PhpSerialization.Serialize(listA) + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/DictionarySerialization.cs b/PhpSerializerNET.Test/Serialize/DictionarySerialization.cs index 9552d74..c17e3f5 100644 --- a/PhpSerializerNET.Test/Serialize/DictionarySerialization.cs +++ b/PhpSerializerNET.Test/Serialize/DictionarySerialization.cs @@ -5,14 +5,14 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class DictionarySerializationTest { - [TestMethod] - public void SerializesMixedData() { - var dictionary = new Dictionary(){ +namespace PhpSerializerNET.Test.Serialize; + +public class DictionarySerializationTest { + [Fact] + public void SerializesMixedData() { + var dictionary = new Dictionary(){ { "AString", "this is a string value" }, { "AnInteger", (long)10 }, { "ADouble", 1.2345 }, @@ -20,59 +20,58 @@ public void SerializesMixedData() { { "False", false } }; - var result = PhpSerialization.Serialize( - dictionary - ); - Assert.AreEqual( - "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}", - result - ); - } + var result = PhpSerialization.Serialize( + dictionary + ); + Assert.Equal( + "a:5:{s:7:\"AString\";s:22:\"this is a string value\";s:9:\"AnInteger\";i:10;s:7:\"ADouble\";d:1.2345;s:4:\"True\";b:1;s:5:\"False\";b:0;}", + result + ); + } - [TestMethod] - public void SerializesWithDoubleKeys() { - var dictionary = new Dictionary(){ + [Fact] + public void SerializesWithDoubleKeys() { + var dictionary = new Dictionary(){ {1.1, "a"}, {1.2, "b"}, {1.3, "c"} }; - Assert.AreEqual( - "a:3:{d:1.1;s:1:\"a\";d:1.2;s:1:\"b\";d:1.3;s:1:\"c\";}", - PhpSerialization.Serialize(dictionary) - ); - } + Assert.Equal( + "a:3:{d:1.1;s:1:\"a\";d:1.2;s:1:\"b\";d:1.3;s:1:\"c\";}", + PhpSerialization.Serialize(dictionary) + ); + } - [TestMethod] - public void SerializesWithBooleanKeys() { - var dictionary = new Dictionary(){ + [Fact] + public void SerializesWithBooleanKeys() { + var dictionary = new Dictionary(){ {true, "True"}, {false, "False"} }; - var result = PhpSerialization.Serialize( - dictionary - ); + var result = PhpSerialization.Serialize( + dictionary + ); - Assert.AreEqual( - "a:2:{b:1;s:4:\"True\";b:0;s:5:\"False\";}", - result - ); - } + Assert.Equal( + "a:2:{b:1;s:4:\"True\";b:0;s:5:\"False\";}", + result + ); + } - [TestMethod] - public void TerminatesCircularReference() { - var dictionary = new Dictionary(){ + [Fact] + public void TerminatesCircularReference() { + var dictionary = new Dictionary(){ {"1", "a"} }; - dictionary.Add("2", dictionary); + dictionary.Add("2", dictionary); - var result = PhpSerialization.Serialize(dictionary); + var result = PhpSerialization.Serialize(dictionary); - Assert.AreEqual( - "a:2:{s:1:\"1\";s:1:\"a\";s:1:\"2\";N;}", - result - ); - } + Assert.Equal( + "a:2:{s:1:\"1\";s:1:\"a\";s:1:\"2\";N;}", + result + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/DoubleSerialization.cs b/PhpSerializerNET.Test/Serialize/DoubleSerialization.cs index 6cc615f..f918bd6 100644 --- a/PhpSerializerNET.Test/Serialize/DoubleSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/DoubleSerialization.cs @@ -4,65 +4,64 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class DoubleSerializationTest { - [TestMethod] - public void SerializesDecimalValue() { - Assert.AreEqual( - "d:1.23456789;", - PhpSerialization.Serialize(1.23456789) - ); - } +namespace PhpSerializerNET.Test.Serialize; - [TestMethod] - public void SerializesOne() { - Assert.AreEqual( - "d:1;", - PhpSerialization.Serialize((double)1) - ); - } +public class DoubleSerializationTest { + [Fact] + public void SerializesDecimalValue() { + Assert.Equal( + "d:1.23456789;", + PhpSerialization.Serialize(1.23456789) + ); + } - [TestMethod] - public void SerializesMinValue() { - Assert.AreEqual( - "d:-1.7976931348623157E+308;", - PhpSerialization.Serialize(double.MinValue) - ); - } + [Fact] + public void SerializesOne() { + Assert.Equal( + "d:1;", + PhpSerialization.Serialize((double)1) + ); + } - [TestMethod] - public void SerializesMaxValue() { - Assert.AreEqual( - "d:1.7976931348623157E+308;", - PhpSerialization.Serialize(double.MaxValue) - ); - } + [Fact] + public void SerializesMinValue() { + Assert.Equal( + "d:-1.7976931348623157E+308;", + PhpSerialization.Serialize(double.MinValue) + ); + } - [TestMethod] - public void SerializesInfinity() { - Assert.AreEqual( - "d:INF;", - PhpSerialization.Serialize(double.PositiveInfinity) - ); - } + [Fact] + public void SerializesMaxValue() { + Assert.Equal( + "d:1.7976931348623157E+308;", + PhpSerialization.Serialize(double.MaxValue) + ); + } - [TestMethod] - public void SerializesNegativeInfinity() { - Assert.AreEqual( - "d:-INF;", - PhpSerialization.Serialize(double.NegativeInfinity) - ); - } + [Fact] + public void SerializesInfinity() { + Assert.Equal( + "d:INF;", + PhpSerialization.Serialize(double.PositiveInfinity) + ); + } + + [Fact] + public void SerializesNegativeInfinity() { + Assert.Equal( + "d:-INF;", + PhpSerialization.Serialize(double.NegativeInfinity) + ); + } - [TestMethod] - public void SerializesNaN() { - Assert.AreEqual( - "d:NAN;", - PhpSerialization.Serialize(double.NaN) - ); - } + [Fact] + public void SerializesNaN() { + Assert.Equal( + "d:NAN;", + PhpSerialization.Serialize(double.NaN) + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs b/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs index 68fa496..b5c7b5c 100644 --- a/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/DynamicSerialization.cs @@ -6,47 +6,45 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System.Dynamic; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class DynamicSerializationTest { - [TestMethod] - public void SerializesPhpDynamicObject() { - dynamic data = new PhpDynamicObject(); - data.Foo = "a"; - data.Bar = 3.1415; - - Assert.AreEqual( - "O:8:\"stdClass\":2:{s:3:\"Foo\";s:1:\"a\";s:3:\"Bar\";d:3.1415;}", - PhpSerialization.Serialize(data) - ); - } - - [TestMethod] - public void SerializesPhpDynamicObjectWithClassname() { - dynamic data = new PhpDynamicObject(); - 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) - ); - } - - [TestMethod] - public void SerializesExpandoObject() { - dynamic data = new ExpandoObject(); - data.Foo = "a"; - data.Bar = 3.1415; - - Assert.AreEqual( - "O:8:\"stdClass\":2:{s:3:\"Foo\";s:1:\"a\";s:3:\"Bar\";d:3.1415;}", - PhpSerialization.Serialize(data) - ); - } +using Xunit; +namespace PhpSerializerNET.Test.Serialize; + +public class DynamicSerializationTest { + [Fact] + public void SerializesPhpDynamicObject() { + dynamic data = new PhpDynamicObject(); + data.Foo = "a"; + data.Bar = 3.1415; + + Assert.Equal( + "O:8:\"stdClass\":2:{s:3:\"Foo\";s:1:\"a\";s:3:\"Bar\";d:3.1415;}", + PhpSerialization.Serialize(data) + ); + } + + [Fact] + public void SerializesPhpDynamicObjectWithClassname() { + dynamic data = new PhpDynamicObject(); + data.SetClassName("phpDynamicObject"); + data.Foo = "a"; + data.Bar = 3.1415; + System.Console.WriteLine(data.Bar); + Assert.Equal( + "O:16:\"phpDynamicObject\":2:{s:3:\"Foo\";s:1:\"a\";s:3:\"Bar\";d:3.1415;}", + PhpSerialization.Serialize(data) + ); + } + + [Fact] + public void SerializesExpandoObject() { + dynamic data = new ExpandoObject(); + data.Foo = "a"; + data.Bar = 3.1415; + + Assert.Equal( + "O:8:\"stdClass\":2:{s:3:\"Foo\";s:1:\"a\";s:3:\"Bar\";d:3.1415;}", + PhpSerialization.Serialize(data) + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Serialize/EnumSerialization.cs b/PhpSerializerNET.Test/Serialize/EnumSerialization.cs index d239e52..a636a6a 100644 --- a/PhpSerializerNET.Test/Serialize/EnumSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/EnumSerialization.cs @@ -5,27 +5,25 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Serialize { +namespace PhpSerializerNET.Test.Serialize; - [TestClass] - public class EnumSerializationTest { - [TestMethod] - public void SerializeOne() { - Assert.AreEqual( - "i:1;", - PhpSerialization.Serialize(IntEnum.A) - ); - } +public class EnumSerializationTest { + [Fact] + public void SerializeOne() { + Assert.Equal( + "i:1;", + PhpSerialization.Serialize(IntEnum.A) + ); + } - [TestMethod] - public void SerializeToString() { - Assert.AreEqual( - "s:1:\"A\";", - PhpSerialization.Serialize(IntEnum.A, new PhpSerializiationOptions{NumericEnums = false}) - ); - } + [Fact] + public void SerializeToString() { + Assert.Equal( + "s:1:\"A\";", + PhpSerialization.Serialize(IntEnum.A, new PhpSerializiationOptions { NumericEnums = false }) + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/IPhpObjectSerialization.cs b/PhpSerializerNET.Test/Serialize/IPhpObjectSerialization.cs index d7364d8..5dfd27b 100644 --- a/PhpSerializerNET.Test/Serialize/IPhpObjectSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/IPhpObjectSerialization.cs @@ -3,31 +3,30 @@ 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test { - [TestClass] - public class IPhpObjectSerializationTest { +namespace PhpSerializerNET.Test; - [TestMethod] - public void SerializeIPhpObject() { - var data = new MyPhpObject() { Foo = "" }; - data.SetClassName("MyPhpObject"); - Assert.AreEqual( // strings: - "O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}", - PhpSerialization.Serialize(data) - ); - } +public class IPhpObjectSerializationTest { - [TestMethod] - public void SerializePhpObjectDictionary() { - var data = new PhpObjectDictionary() { { "Foo", "" } }; - data.SetClassName("MyPhpObject"); - Assert.AreEqual( // strings: - "O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}", - PhpSerialization.Serialize(data) - ); - } + [Fact] + public void SerializeIPhpObject() { + var data = new MyPhpObject() { Foo = "" }; + data.SetClassName("MyPhpObject"); + Assert.Equal( // strings: + "O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}", + PhpSerialization.Serialize(data) + ); + } + + [Fact] + public void SerializePhpObjectDictionary() { + var data = new PhpObjectDictionary() { { "Foo", "" } }; + data.SetClassName("MyPhpObject"); + Assert.Equal( // strings: + "O:11:\"MyPhpObject\":1:{s:3:\"Foo\";s:0:\"\";}", + PhpSerialization.Serialize(data) + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Serialize/IntegerSerialization.cs b/PhpSerializerNET.Test/Serialize/IntegerSerialization.cs index ca6537e..125418c 100644 --- a/PhpSerializerNET.Test/Serialize/IntegerSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/IntegerSerialization.cs @@ -5,41 +5,40 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class IntegerSerializationTest { - [TestMethod] - public void SerializeZero() { - Assert.AreEqual( - "i:0;", - PhpSerialization.Serialize(0) - ); - } +namespace PhpSerializerNET.Test.Serialize; - [TestMethod] - public void SerializeOne() { - Assert.AreEqual( - "i:1;", - PhpSerialization.Serialize(1) - ); - } +public class IntegerSerializationTest { + [Fact] + public void SerializeZero() { + Assert.Equal( + "i:0;", + PhpSerialization.Serialize(0) + ); + } + + [Fact] + public void SerializeOne() { + Assert.Equal( + "i:1;", + PhpSerialization.Serialize(1) + ); + } - [TestMethod] - public void SerializeIntMaxValue() { - Assert.AreEqual( - "i:2147483647;", - PhpSerialization.Serialize(int.MaxValue) - ); - } + [Fact] + public void SerializeIntMaxValue() { + Assert.Equal( + "i:2147483647;", + PhpSerialization.Serialize(int.MaxValue) + ); + } - [TestMethod] - public void SerializeIntMinValue() { - Assert.AreEqual( - "i:-2147483648;", - PhpSerialization.Serialize(int.MinValue) - ); - } + [Fact] + public void SerializeIntMinValue() { + Assert.Equal( + "i:-2147483648;", + PhpSerialization.Serialize(int.MinValue) + ); } } \ No newline at end of file diff --git a/PhpSerializerNET.Test/Serialize/ListSerialization.cs b/PhpSerializerNET.Test/Serialize/ListSerialization.cs index 5a53b4a..46d2430 100644 --- a/PhpSerializerNET.Test/Serialize/ListSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/ListSerialization.cs @@ -5,33 +5,32 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class ListSerializationTest { - [TestMethod] - public void SerializeListOfStrings() { - Assert.AreEqual( // strings: - "a:2:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";}", - PhpSerialization.Serialize(new List() { "Hello", "World" }) - ); - } +namespace PhpSerializerNET.Test.Serialize; - [TestMethod] - public void SerializeListOfBools() { - Assert.AreEqual( // booleans: - "a:2:{i:0;b:1;i:1;b:0;}", - PhpSerialization.Serialize(new List() { true, false }) - ); - } +public class ListSerializationTest { + [Fact] + public void SerializeListOfStrings() { + Assert.Equal( // strings: + "a:2:{i:0;s:5:\"Hello\";i:1;s:5:\"World\";}", + PhpSerialization.Serialize(new List() { "Hello", "World" }) + ); + } + + [Fact] + public void SerializeListOfBools() { + Assert.Equal( // booleans: + "a:2:{i:0;b:1;i:1;b:0;}", + PhpSerialization.Serialize(new List() { true, false }) + ); + } - [TestMethod] - public void SerializeMixedList() { - Assert.AreEqual( // mixed types: - "a:5:{i:0;b:1;i:1;i:1;i:2;d:1.23;i:3;s:3:\"end\";i:4;N;}", - PhpSerialization.Serialize(new List() { true, 1, 1.23, "end", null }) - ); - } + [Fact] + public void SerializeMixedList() { + Assert.Equal( // mixed types: + "a:5:{i:0;b:1;i:1;i:1;i:2;d:1.23;i:3;s:3:\"end\";i:4;N;}", + PhpSerialization.Serialize(new List() { true, 1, 1.23, "end", null }) + ); } } diff --git a/PhpSerializerNET.Test/Serialize/LongSerialization.cs b/PhpSerializerNET.Test/Serialize/LongSerialization.cs index 3145945..8c2019c 100644 --- a/PhpSerializerNET.Test/Serialize/LongSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/LongSerialization.cs @@ -4,24 +4,23 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class LongSerializationTest { - [TestMethod] - public void SerializeIntMaxValue() { - Assert.AreEqual( - "i:9223372036854775807;", - PhpSerialization.Serialize(long.MaxValue) - ); - } - [TestMethod] - public void SerializeMinValue() { - Assert.AreEqual( - "i:-9223372036854775808;", - PhpSerialization.Serialize(long.MinValue) - ); - } +namespace PhpSerializerNET.Test.Serialize; + +public class LongSerializationTest { + [Fact] + public void SerializeIntMaxValue() { + Assert.Equal( + "i:9223372036854775807;", + PhpSerialization.Serialize(long.MaxValue) + ); + } + [Fact] + public void SerializeMinValue() { + Assert.Equal( + "i:-9223372036854775808;", + PhpSerialization.Serialize(long.MinValue) + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/NullSerialization.cs b/PhpSerializerNET.Test/Serialize/NullSerialization.cs index 97d7a58..a00ae8d 100644 --- a/PhpSerializerNET.Test/Serialize/NullSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/NullSerialization.cs @@ -4,17 +4,16 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class NullSerializationTest { - [TestMethod] - public void SerializesNull() { - Assert.AreEqual( - "N;", - PhpSerialization.Serialize(null) - ); - } +namespace PhpSerializerNET.Test.Serialize; + +public class NullSerializationTest { + [Fact] + public void SerializesNull() { + Assert.Equal( + "N;", + PhpSerialization.Serialize(null) + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/ObjectSerialization.cs b/PhpSerializerNET.Test/Serialize/ObjectSerialization.cs index 548d795..9af7da9 100644 --- a/PhpSerializerNET.Test/Serialize/ObjectSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/ObjectSerialization.cs @@ -5,76 +5,75 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class ObjectSerializationTest { - [TestMethod] - public void SerializesToStdClass() { - var testObject = new UnnamedClass() { - Foo = 3.14, - Bar = 2.718, - }; - Assert.AreEqual( - "O:8:\"stdClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", - PhpSerialization.Serialize(testObject) - ); - } +namespace PhpSerializerNET.Test.Serialize; - [TestMethod] - public void SerializesToSpecificClass() { - var testObject = new NamedClass() { - Foo = 3.14, - Bar = 2.718, - }; - Assert.AreEqual( - "O:7:\"myClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", - PhpSerialization.Serialize(testObject) - ); - } +public class ObjectSerializationTest { + [Fact] + public void SerializesToStdClass() { + var testObject = new UnnamedClass() { + Foo = 3.14, + Bar = 2.718, + }; + Assert.Equal( + "O:8:\"stdClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", + PhpSerialization.Serialize(testObject) + ); + } - [TestMethod] - public void SerializeObjectToArray() { - var testObject = new MappedClass() { - English = "Hello world!", - German = "Hallo Welt!", - It = "Ciao mondo!" - }; + [Fact] + public void SerializesToSpecificClass() { + var testObject = new NamedClass() { + Foo = 3.14, + Bar = 2.718, + }; + Assert.Equal( + "O:7:\"myClass\":2:{s:3:\"Foo\";d:3.14;s:3:\"Bar\";d:2.718;}", + PhpSerialization.Serialize(testObject) + ); + } - Assert.AreEqual( - "a:3:{s:2:\"en\";s:12:\"Hello world!\";s:2:\"de\";s:11:\"Hallo Welt!\";s:4:\"Guid\";a:1:{s:5:\"Empty\";N;}}", - PhpSerialization.Serialize(testObject) - ); - } + [Fact] + public void SerializeObjectToArray() { + var testObject = new MappedClass() { + English = "Hello world!", + German = "Hallo Welt!", + It = "Ciao mondo!" + }; - [TestMethod] - public void SerializeObjectToObject() { - var testObject = new UnnamedClass() { - Foo = 1, - Bar = 2, - }; + Assert.Equal( + "a:3:{s:2:\"en\";s:12:\"Hello world!\";s:2:\"de\";s:11:\"Hallo Welt!\";s:4:\"Guid\";a:1:{s:5:\"Empty\";N;}}", + PhpSerialization.Serialize(testObject) + ); + } - Assert.AreEqual( - "O:8:\"stdClass\":2:{s:3:\"Foo\";d:1;s:3:\"Bar\";d:2;}", - PhpSerialization.Serialize(testObject) - ); - } + [Fact] + public void SerializeObjectToObject() { + var testObject = new UnnamedClass() { + Foo = 1, + Bar = 2, + }; + + Assert.Equal( + "O:8:\"stdClass\":2:{s:3:\"Foo\";d:1;s:3:\"Bar\";d:2;}", + PhpSerialization.Serialize(testObject) + ); + } - [TestMethod] - public void ObjectIntoMixedKeyArray() { - var data = new MixedKeysPhpClass() { - Foo = "Foo", - Bar = "Bar", - Baz = "A", - Dummy = "B", - }; + [Fact] + public void ObjectIntoMixedKeyArray() { + var data = new MixedKeysPhpClass() { + Foo = "Foo", + Bar = "Bar", + Baz = "A", + Dummy = "B", + }; - Assert.AreEqual( - "O:8:\"stdClass\":4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}", - PhpSerialization.Serialize(data) - ); - } + Assert.Equal( + "O:8:\"stdClass\":4:{i:0;s:3:\"Foo\";i:1;s:3:\"Bar\";s:1:\"a\";s:1:\"A\";s:1:\"b\";s:1:\"B\";}", + PhpSerialization.Serialize(data) + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/PhpDateTimeSerialization.cs b/PhpSerializerNET.Test/Serialize/PhpDateTimeSerialization.cs index c4ae929..c7b0826 100644 --- a/PhpSerializerNET.Test/Serialize/PhpDateTimeSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/PhpDateTimeSerialization.cs @@ -5,22 +5,21 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class PhpDateTimeSerializationTest { - [TestMethod] - public void Serializes1() { - var testObject = new PhpDateTime() { - Date = "2021-12-15 19:32:38.980103", - TimezoneType = 3, - Timezone = "UTC", - }; - Assert.AreEqual( - "O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2021-12-15 19:32:38.980103\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}", - PhpSerialization.Serialize(testObject) - ); - } +namespace PhpSerializerNET.Test.Serialize; + +public class PhpDateTimeSerializationTest { + [Fact] + public void Serializes1() { + var testObject = new PhpDateTime() { + Date = "2021-12-15 19:32:38.980103", + TimezoneType = 3, + Timezone = "UTC", + }; + Assert.Equal( + "O:8:\"DateTime\":3:{s:4:\"date\";s:26:\"2021-12-15 19:32:38.980103\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}", + PhpSerialization.Serialize(testObject) + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/StringSerialization.cs b/PhpSerializerNET.Test/Serialize/StringSerialization.cs index 1b8956d..1192d37 100644 --- a/PhpSerializerNET.Test/Serialize/StringSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/StringSerialization.cs @@ -5,42 +5,39 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class StringSerializationTest { - [TestMethod] - public void SerializeHelloWorld() { - Assert.AreEqual( - "s:12:\"Hello World!\";", - PhpSerialization.Serialize("Hello World!") - ); - } - - [TestMethod] - public void SerializeEmptyString() { - Assert.AreEqual( - "s:0:\"\";", - PhpSerialization.Serialize("") - ); - } +namespace PhpSerializerNET.Test.Serialize; +public class StringSerializationTest { + [Fact] + public void SerializeHelloWorld() { + Assert.Equal( + "s:12:\"Hello World!\";", + PhpSerialization.Serialize("Hello World!") + ); + } - [TestMethod] - public void SerializeUmlauts() { - Assert.AreEqual( - "s:14:\"äöüßÄÖÜ\";", - PhpSerialization.Serialize("äöüßÄÖÜ") - ); - } + [Fact] + public void SerializeEmptyString() { + Assert.Equal( + "s:0:\"\";", + PhpSerialization.Serialize("") + ); + } - [TestMethod] - public void SerializeEmoji() { - Assert.AreEqual( - "s:4:\"👻\";", - PhpSerialization.Serialize("👻") - ); - } + [Fact] + public void SerializeUmlauts() { + Assert.Equal( + "s:14:\"äöüßÄÖÜ\";", + PhpSerialization.Serialize("äöüßÄÖÜ") + ); + } + [Fact] + public void SerializeEmoji() { + Assert.Equal( + "s:4:\"👻\";", + PhpSerialization.Serialize("👻") + ); } -} \ No newline at end of file +} diff --git a/PhpSerializerNET.Test/Serialize/StructSerialization.cs b/PhpSerializerNET.Test/Serialize/StructSerialization.cs index 1495c6c..1432c22 100644 --- a/PhpSerializerNET.Test/Serialize/StructSerialization.cs +++ b/PhpSerializerNET.Test/Serialize/StructSerialization.cs @@ -4,30 +4,29 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; using PhpSerializerNET.Test.DataTypes; -namespace PhpSerializerNET.Test.Serialize { - [TestClass] - public class StructSerializationTest { - [TestMethod] - public void SerializeStruct() { - Assert.AreEqual( - "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}", - PhpSerialization.Serialize( - new AStruct() { foo = "Foo", bar = "Bar" } - ) - ); - } +namespace PhpSerializerNET.Test.Serialize; - [TestMethod] - public void SerializeStructWithIgnore() { - Assert.AreEqual( - "a:1:{s:3:\"foo\";s:3:\"Foo\";}", - PhpSerialization.Serialize( - new AStructWithIgnore() { foo = "Foo", bar = "Bar" } - ) - ); - } +public class StructSerializationTest { + [Fact] + public void SerializeStruct() { + Assert.Equal( + "a:2:{s:3:\"foo\";s:3:\"Foo\";s:3:\"bar\";s:3:\"Bar\";}", + PhpSerialization.Serialize( + new AStruct() { foo = "Foo", bar = "Bar" } + ) + ); } -} \ No newline at end of file + + [Fact] + public void SerializeStructWithIgnore() { + Assert.Equal( + "a:1:{s:3:\"foo\";s:3:\"Foo\";}", + PhpSerialization.Serialize( + new AStructWithIgnore() { foo = "Foo", bar = "Bar" } + ) + ); + } +} From 64e67a892ce3fb6b92f8d947fef62625929798d9 Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Thu, 25 Jul 2024 19:31:38 +0200 Subject: [PATCH 09/11] Deserialization: Some more performance improvements. - Do not get the PhpDataType and switch on that, intead directly switch on the relevant input byte. - In the validator: Increment the token count only in the array and object methods. - Tokenizer: Add AggressiveOptimization hint to GetToken(). --- .../Deserialization/PhpDeserializer.cs | 2 +- PhpSerializerNET/Deserialization/PhpToken.cs | 6 +- .../Deserialization/PhpTokenValidator.cs | 50 +++++++-------- .../Deserialization/PhpTokenizer.cs | 62 ++++++++----------- 4 files changed, 56 insertions(+), 64 deletions(-) diff --git a/PhpSerializerNET/Deserialization/PhpDeserializer.cs b/PhpSerializerNET/Deserialization/PhpDeserializer.cs index ac113df..84d2c41 100644 --- a/PhpSerializerNET/Deserialization/PhpDeserializer.cs +++ b/PhpSerializerNET/Deserialization/PhpDeserializer.cs @@ -374,7 +374,7 @@ private object MakeArray(Type targetType, PhpToken token) { } private object MakeList(Type targetType, PhpToken token) { - for (int i = 0; i < token.Length; i += 2) { + for (int i = 0; i < token.Length * 2; i+=2) { if (this._tokens[_currentToken+i].Type != PhpDataType.Integer) { var badToken = this._tokens[_currentToken+i]; throw new DeserializationException( diff --git a/PhpSerializerNET/Deserialization/PhpToken.cs b/PhpSerializerNET/Deserialization/PhpToken.cs index 30301f0..9fd710c 100644 --- a/PhpSerializerNET/Deserialization/PhpToken.cs +++ b/PhpSerializerNET/Deserialization/PhpToken.cs @@ -5,6 +5,8 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ namespace PhpSerializerNET; +#nullable enable + /// /// PHP data token. Holds the type, position (in the input string), length and value. /// @@ -12,8 +14,8 @@ internal readonly struct PhpToken { internal readonly PhpDataType Type; internal readonly int Position; internal readonly int Length; - internal readonly string Value; - internal PhpToken(PhpDataType type, int position, string value = "", int length = 0) { + internal readonly string? Value; + internal PhpToken(PhpDataType type, int position, string? value = null, int length = 0) { this.Type = type; this.Position = position; this.Value = value; diff --git a/PhpSerializerNET/Deserialization/PhpTokenValidator.cs b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs index 5607a03..52c11e4 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenValidator.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenValidator.cs @@ -9,39 +9,32 @@ This Source Code Form is subject to the terms of the Mozilla Public namespace PhpSerializerNET; +#nullable enable + internal ref struct PhpTokenValidator { private int _position; - private int _tokenCount = 0; + private int _tokenCount; private readonly ReadOnlySpan _input; private readonly int _lastIndex; internal PhpTokenValidator(in ReadOnlySpan input) { + this._tokenCount = 1; this._input = input; this._position = 0; this._lastIndex = this._input.Length - 1; } internal void GetToken() { - PhpDataType dataType = this._input[this._position++] switch { - (byte)'N' => PhpDataType.Null, - (byte)'b' => PhpDataType.Boolean, - (byte)'s' => PhpDataType.String, - (byte)'i' => PhpDataType.Integer, - (byte)'d' => PhpDataType.Floating, - (byte)'a' => PhpDataType.Array, - (byte)'O' => PhpDataType.Object, - _ => throw new DeserializationException($"Unexpected token '{this.GetCharAt(this._position - 1)}' at position {this._position - 1}.") - }; - switch (dataType) { - case PhpDataType.Boolean: + switch ( this._input[this._position++]) { + case (byte)'b': this.GetCharacter(':'); this.GetBoolean(); this.GetCharacter(';'); break; - case PhpDataType.Null: + case (byte)'N': this.GetCharacter(';'); break; - case PhpDataType.String: + case (byte)'s': this.GetCharacter(':'); int length = this.GetLength(PhpDataType.String); this.GetCharacter(':'); @@ -50,24 +43,27 @@ internal void GetToken() { this.GetCharacter('"'); this.GetCharacter(';'); break; - case PhpDataType.Integer: + case (byte)'i': this.GetCharacter(':'); this.GetInteger(); this.GetCharacter(';'); break; - case PhpDataType.Floating: + case (byte)'d': this.GetCharacter(':'); this.GetFloat(); this.GetCharacter(';'); break; - case PhpDataType.Array: + case (byte)'a': this.GetArrayToken(); break; - case PhpDataType.Object: + case (byte)'O': this.GetObjectToken(); break; + default: + throw new DeserializationException( + $"Unexpected token '{this.GetCharAt(this._position - 1)}' at position {this._position - 1}." + ); }; - this._tokenCount++; } private char GetCharAt(int position) { @@ -119,6 +115,7 @@ private void GetFloat() { ); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetInteger() { int i = this._position; @@ -204,15 +201,17 @@ private void GetObjectToken() { this.GetCharacter('{'); int i = 0; while (this._input[this._position] != '}') { + this.GetToken(); this.GetToken(); i++; - if (i > propertyCount * 2) { + if (i > propertyCount) { throw new DeserializationException( $"Object at position {position} should have {propertyCount} properties, " + - $"but actually has {(i + 1) / 2} or more properties." + $"but actually has {i} or more properties." ); } } + this._tokenCount += propertyCount * 2; this.GetCharacter('}'); } @@ -223,18 +222,19 @@ private void GetArrayToken() { int length = this.GetLength(PhpDataType.Array); this.GetCharacter(':'); this.GetCharacter('{'); - int maxTokenCount = length * 2; int i = 0; while (this._input[this._position] != '}') { + this.GetToken(); this.GetToken(); i++; - if (i > maxTokenCount) { + if (i > length) { throw new DeserializationException( $"Array at position {position} should be of length {length}, " + - $"but actual length is {(i + 1) / 2} or more." + $"but actual length is {i} or more." ); } } + this._tokenCount += length * 2; this.GetCharacter('}'); } diff --git a/PhpSerializerNET/Deserialization/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenizer.cs index 150d774..1156274 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenizer.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenizer.cs @@ -5,10 +5,12 @@ This Source Code Form is subject to the terms of the Mozilla Public **/ using System; -using System.Diagnostics; +using System.Globalization; using System.Runtime.CompilerServices; using System.Text; +#nullable enable + namespace PhpSerializerNET; public ref struct PhpTokenizer { @@ -26,24 +28,11 @@ private PhpTokenizer(ReadOnlySpan input, Encoding inputEncoding, Span PhpDataType.Null, - (byte)'b' => PhpDataType.Boolean, - (byte)'s' => PhpDataType.String, - (byte)'i' => PhpDataType.Integer, - (byte)'d' => PhpDataType.Floating, - (byte)'a' => PhpDataType.Array, - (byte)'O' => PhpDataType.Object, - _ => throw new UnreachableException(), - }; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Advance() { this._position++; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Advance(int positons) { this._position += positons; @@ -55,42 +44,44 @@ private string GetNumbers() { while (this._input[this._position] != (byte)';') { this._position++; } - return this._inputEncoding.GetString(this._input.Slice(start, this._position-start)); + return this._inputEncoding.GetString(this._input.Slice(start, this._position - start)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetLength() { - if (this._input[this._position+1] == ':') { + if (this._input[this._position + 1] == ':') { return _input[_position++] - 48; } int start = this._position; while (this._input[this._position] != (byte)':') { this._position++; } - return int.Parse(this._input.Slice(start, this._position-start)); + return int.Parse(this._input.Slice(start, this._position - start), CultureInfo.InvariantCulture); } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] internal void GetToken() { - switch (this.GetDataType()) { - case PhpDataType.Boolean: + switch (this._input[this._position++]) { + case (byte)'b': this.GetBooleanToken(); break; - case PhpDataType.Null: - this.GetNullToken(); + case (byte)'N': + this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position - 1); + this.Advance(); break; - case PhpDataType.String: + case (byte)'s': this.GetStringToken(); break; - case PhpDataType.Integer: + case (byte)'i': this.GetIntegerToken(); break; - case PhpDataType.Floating: + case (byte)'d': this.GetFloatingToken(); break; - case PhpDataType.Array: + case (byte)'a': this.GetArrayToken(); break; - case PhpDataType.Object: + case (byte)'O': this.GetObjectToken(); break; }; @@ -98,7 +89,7 @@ internal void GetToken() { [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetNullToken() { - this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position-1); + this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position - 1); this.Advance(); } @@ -110,15 +101,14 @@ private void GetBooleanToken() { _position - 2, this._input[this._position++] == (byte)'1' ? "1" - : "0", - 0 + : "0" ); this.Advance(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetStringToken() { - int position = _position -1; + int position = _position - 1; this.Advance(); int length = this.GetLength(); this.Advance(2); @@ -127,7 +117,7 @@ private void GetStringToken() { position, _inputEncoding.GetString(this._input.Slice(this._position, length)) ); - this.Advance(2+length); + this.Advance(2 + length); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -135,7 +125,7 @@ private void GetIntegerToken() { this.Advance(); this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Integer, - this._position-2, + this._position - 2, this.GetNumbers() ); this.Advance(); @@ -160,7 +150,7 @@ private void GetArrayToken() { this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Array, position, - "", + null, length ); this.Advance(2); @@ -172,12 +162,12 @@ private void GetArrayToken() { [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetObjectToken() { - int position = _position -1; + int position = _position - 1; this.Advance(); int classNameLength = this.GetLength(); this.Advance(2); string className = _inputEncoding.GetString(this._input.Slice(this._position, classNameLength)); - this.Advance(2+classNameLength); + this.Advance(2 + classNameLength); int propertyCount = this.GetLength(); this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Object, From 5e6032e226b51f3c991b87ed0334182fb36ba273 Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Thu, 25 Jul 2024 19:54:34 +0200 Subject: [PATCH 10/11] Performance: Do the input re-encoding faster. Also: For small inputs, we can stackalloc the byte array. Updated changelog to reflect the performance status. --- CHANGELOG.md | 19 ++++++++----------- PhpSerializerNET/PhpSerialization.cs | 13 ++++++------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e07f52..e7fc0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,24 @@ # Future ## Breaking -- `PhpTokenizer` class is now internal -- Removed support for `net6.0` and `net7.0` +- `PhpTokenizer` class is now internal. +- Removed support for `net6.0` and `net7.0`. ## Regular changes - Integers and doubles without a value now give a better error message (`i:;` and `d:;`). +## Performance +- Reduced time to decode / re-encode the input string. +- Reduced memory allocations both in the input re-encoding and the deserialization. + ## Internal Split the deserialization into 3 phases: 1. Validation of the input and counting of the data tokens. 2. Parsing of the input into tokens 3. Deserializations of the tokens into the target C# objects/structs. -In version 1.4 and prior, this was a 2 step process. The new approach is slightly slower in some benchmarks, but needs -less memory. On my machine, deserializing an array of 24 integers: - -| | Time | Heap allocation | -|------------|---------:|----------------:| -| **Before** | 1.799 us | 4.54 KB | -| **After** | 1.883 us | 4.13 KB | - -Other benchmarks also indicate a roughly 5-10% performance penalty on arrays, objects and strings. +In version 1.4 and prior, this was a 2 step process. This is slightly slower on some inputs, but overall a little +neater because we're cleanly separating the tasks. # 1.4.0 - Now targets .NET 6.0, 7.0 and 8.0 diff --git a/PhpSerializerNET/PhpSerialization.cs b/PhpSerializerNET/PhpSerialization.cs index 0e1be75..8ccf008 100644 --- a/PhpSerializerNET/PhpSerialization.cs +++ b/PhpSerializerNET/PhpSerialization.cs @@ -9,18 +9,17 @@ This Source Code Form is subject to the terms of the Mozilla Public using System; using System.Collections.Generic; -using System.Reflection.Metadata.Ecma335; using System.Text; namespace PhpSerializerNET; public static class PhpSerialization { - private static Span Tokenize(string input, Encoding inputEncoding) { - ReadOnlySpan inputBytes = Encoding.Convert( - Encoding.Default, - inputEncoding, - Encoding.Default.GetBytes(input) - ); + private static Span Tokenize(ReadOnlySpan input, Encoding inputEncoding) { + int size = inputEncoding.GetByteCount(input); + Span inputBytes = size < 256 + ? stackalloc byte[size] + : new byte[size]; + inputEncoding.GetBytes(input, inputBytes); int tokenCount = PhpTokenValidator.Validate(inputBytes); Span tokens = new PhpToken[tokenCount]; PhpTokenizer.Tokenize(inputBytes, inputEncoding, tokens); From f7ea9cd480d52bc2ed331864a30dfa479f2dc451 Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Thu, 25 Jul 2024 23:29:18 +0200 Subject: [PATCH 11/11] Performance: Don't copy the input bytes as string to the PhpToken. This reduces memory allocation for doubles, bools and floats, as they can be directly evaluated from the raw bytestream. That also speeds up deserialization by a lot. We also no longer have to give the Tokenizer the input encoding. There are probably some further optimizations to be had in the deserializer -> DeserializeTokenFromSimpleType() method too. --- CHANGELOG.md | 2 + .../Deserialization/PhpDeserializer.cs | 117 +++++++++++------- PhpSerializerNET/Deserialization/PhpToken.cs | 8 +- .../Deserialization/PhpTokenizer.cs | 31 ++--- PhpSerializerNET/Deserialization/ValueSpan.cs | 46 +++++++ PhpSerializerNET/PhpSerialization.cs | 43 ++++--- PhpSerializerNET/PhpSerializerNET.csproj | 2 +- 7 files changed, 163 insertions(+), 86 deletions(-) create mode 100644 PhpSerializerNET/Deserialization/ValueSpan.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index e7fc0a3..258d944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ## Performance - Reduced time to decode / re-encode the input string. - Reduced memory allocations both in the input re-encoding and the deserialization. +- Delay the materialization of strings when deserializing. This can avoid string allocations entirely for integers, + doubles and floats. ## Internal Split the deserialization into 3 phases: diff --git a/PhpSerializerNET/Deserialization/PhpDeserializer.cs b/PhpSerializerNET/Deserialization/PhpDeserializer.cs index 84d2c41..f047fb6 100644 --- a/PhpSerializerNET/Deserialization/PhpDeserializer.cs +++ b/PhpSerializerNET/Deserialization/PhpDeserializer.cs @@ -9,17 +9,22 @@ This Source Code Form is subject to the terms of the Mozilla Public using System.Globalization; using System.Linq; using System.Reflection; +using System.Text; namespace PhpSerializerNET; internal ref struct PhpDeserializer { private readonly PhpDeserializationOptions _options; + private Encoding _inputEncoding; private readonly Span _tokens; + private readonly ReadOnlySpan _input; private int _currentToken = 0; - internal PhpDeserializer(Span tokens, PhpDeserializationOptions options) { + internal PhpDeserializer(Span tokens, ReadOnlySpan input, PhpDeserializationOptions options) { _options = options; + _input = input; _tokens = tokens; + _inputEncoding = _options.InputEncoding; } internal object Deserialize() { @@ -39,16 +44,18 @@ private object DeserializeToken() { this._currentToken++; switch (token.Type) { case PhpDataType.Boolean: - return token.Value.PhpToBool(); + return token.Value.GetBool(this._input); case PhpDataType.Integer: - return token.Value.PhpToLong(); + return token.Value.GetLong(this._input); case PhpDataType.Floating: - return token.Value.PhpToDouble(); + return token.Value.GetDouble(this._input); case PhpDataType.String: - if (this._options.NumberStringToBool && (token.Value == "0" || token.Value == "1")) { - return token.Value.PhpToBool(); + if (this._options.NumberStringToBool) { + if (_input[token.Value.Start] == (byte)'1' || _input[token.Value.Start] == (byte)'0') { + return token.Value.GetBool(this._input); + } } - return token.Value; + return this.GetString(token); case PhpDataType.Array: return MakeCollection(token); case PhpDataType.Object: @@ -75,7 +82,12 @@ private object DeserializeToken(Type targetType) { case PhpDataType.Floating: return DeserializeDouble(targetType, token); case PhpDataType.String: - return DeserializeTokenFromSimpleType(targetType, token.Type, token.Value, token.Position); + return DeserializeTokenFromSimpleType( + targetType, + token.Type, + this.GetString(token), + token.Position + ); case PhpDataType.Object: { object result; if (typeof(IDictionary).IsAssignableFrom(targetType)) { @@ -86,7 +98,7 @@ private object DeserializeToken(Type targetType) { result = MakeStruct(targetType, token); } if (result is IPhpObject phpObject and not PhpDateTime) { - phpObject.SetClassName(token.Value); + phpObject.SetClassName(this.GetString(token)); } return result; } @@ -111,35 +123,41 @@ private object DeserializeToken(Type targetType) { } } - private object DeserializeInteger(Type targetType, PhpToken token) { + private object DeserializeInteger(Type targetType, in PhpToken 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.Type, token.Value, token.Position), + TypeCode.Int16 => short.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + TypeCode.Int32 => int.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + TypeCode.Int64 => long.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + TypeCode.UInt16 => ushort.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + TypeCode.UInt32 => uint.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + TypeCode.UInt64 => ulong.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + TypeCode.SByte => sbyte.Parse(token.Value.GetSlice(_input), CultureInfo.InvariantCulture), + _ => this.DeserializeTokenFromSimpleType( + targetType, + token.Type, + this.GetString(token), + token.Position + ), }; } - private object DeserializeDouble(Type targetType, PhpToken token) { + private object DeserializeDouble(Type targetType, in PhpToken token) { if (targetType == typeof(double) || targetType == typeof(float)) { - return token.Value.PhpToDouble(); + return token.Value.GetDouble(_input); } - string value = token.Value switch { - "INF" => double.PositiveInfinity.ToString(CultureInfo.InvariantCulture), - "-INF" => double.NegativeInfinity.ToString(CultureInfo.InvariantCulture), - _ => token.Value, - }; + string value = this.GetString(token); + if (value == "INF") { + value = double.PositiveInfinity.ToString(CultureInfo.InvariantCulture); + } else if (value == "-INF") { + value = double.NegativeInfinity.ToString(CultureInfo.InvariantCulture); + } return this.DeserializeTokenFromSimpleType(targetType, token.Type, value, token.Position); } - private static object DeserializeBoolean(Type targetType, PhpToken token) { + private object DeserializeBoolean(Type targetType, in PhpToken token) { if (targetType == typeof(bool) || targetType == typeof(bool?)) { - return token.Value.PhpToBool(); + return token.Value.GetBool(_input); } Type underlyingType = targetType; if (targetType.IsNullableReferenceType()) { @@ -147,10 +165,10 @@ private static object DeserializeBoolean(Type targetType, PhpToken token) { } if (underlyingType.IsIConvertible()) { - return ((IConvertible)token.Value.PhpToBool()).ToType(underlyingType, CultureInfo.InvariantCulture); + return ((IConvertible)token.Value.GetBool(_input)).ToType(underlyingType, CultureInfo.InvariantCulture); } else { throw new DeserializationException( - $"Can not assign value \"{token.Value}\" (at position {token.Position}) to target type of {targetType.Name}." + $"Can not assign value \"{this.GetString(token)}\" (at position {token.Position}) to target type of {targetType.Name}." ); } } @@ -236,8 +254,8 @@ int tokenPosition throw new DeserializationException($"Can not assign value \"{value}\" (at position {tokenPosition}) to target type of {targetType.Name}."); } -private object MakeClass(PhpToken token) { - var typeName = token.Value; + private object MakeClass(in PhpToken token) { + var typeName = this.GetString(token); object constructedObject; Type targetType = null; if (typeName != "stdClass" && this._options.EnableTypeLookup) { @@ -272,14 +290,15 @@ private object MakeClass(PhpToken token) { return constructedObject; } - private object MakeStruct(Type targetType, PhpToken token) { + private object MakeStruct(Type targetType, in PhpToken token) { var result = Activator.CreateInstance(targetType); Dictionary fields = TypeLookup.GetFieldInfos(targetType, this._options); for (int i = 0; i < token.Length; i++) { - var fieldName = this._options.CaseSensitiveProperties - ? this._tokens[this._currentToken++].Value - : this._tokens[this._currentToken++].Value.ToLower(); + var fieldName = this._tokens[this._currentToken++].Value.GetString(_input, _options.InputEncoding); + if (!this._options.CaseSensitiveProperties) { + fieldName = fieldName.ToLower(); + } if (!fields.ContainsKey(fieldName)) { if (!this._options.AllowExcessKeys) { @@ -306,7 +325,7 @@ private object MakeStruct(Type targetType, PhpToken token) { return result; } - private object MakeObject(Type targetType, PhpToken token) { + private object MakeObject(Type targetType, in PhpToken token) { var result = Activator.CreateInstance(targetType); Dictionary properties = TypeLookup.GetPropertyInfos(targetType, this._options); @@ -315,14 +334,14 @@ private object MakeObject(Type targetType, PhpToken token) { var nameToken = this._tokens[_currentToken++]; if (nameToken.Type == PhpDataType.String) { propertyName = this._options.CaseSensitiveProperties - ? nameToken.Value - : nameToken.Value.ToLower(); + ? this.GetString(nameToken) + : this.GetString(nameToken).ToLower(); } else if (nameToken.Type == PhpDataType.Integer) { - propertyName = nameToken.Value.PhpToLong(); + propertyName = nameToken.Value.GetLong(_input); } else { throw new DeserializationException( $"Error encountered deserizalizing an object of type '{targetType.FullName}': " + - $"The key '{nameToken.Value}' (from the token at position {nameToken.Position}) has an unsupported type of '{nameToken.Type}'." + $"The key '{this.GetString(nameToken)}' (from the token at position {nameToken.Position}) has an unsupported type of '{nameToken.Type}'." ); } if (!properties.ContainsKey(propertyName)) { @@ -344,7 +363,7 @@ private object MakeObject(Type targetType, PhpToken token) { } catch (Exception exception) { var valueToken = _tokens[_currentToken-1]; throw new DeserializationException( - $"Exception encountered while trying to assign '{valueToken.Value}' to {targetType.Name}.{property.Name}. See inner exception for details.", + $"Exception encountered while trying to assign '{this.GetString(valueToken)}' to {targetType.Name}.{property.Name}. See inner exception for details.", exception ); } @@ -355,7 +374,7 @@ private object MakeObject(Type targetType, PhpToken token) { return result; } - private object MakeArray(Type targetType, PhpToken token) { + private object MakeArray(Type targetType, in PhpToken token) { var elementType = targetType.GetElementType() ?? throw new InvalidOperationException("targetType.GetElementType() returned null"); Array result = Array.CreateInstance(elementType, token.Length); @@ -373,13 +392,13 @@ private object MakeArray(Type targetType, PhpToken token) { return result; } - private object MakeList(Type targetType, PhpToken token) { + private object MakeList(Type targetType, in PhpToken token) { for (int i = 0; i < token.Length * 2; i+=2) { if (this._tokens[_currentToken+i].Type != PhpDataType.Integer) { var badToken = this._tokens[_currentToken+i]; throw new DeserializationException( $"Can not deserialize array at position {token.Position} to list: " + - $"It has a non-integer key '{badToken.Value}' at element {i} (position {badToken.Position})." + $"It has a non-integer key '{this.GetString(badToken)}' at element {i} (position {badToken.Position})." ); } } @@ -409,7 +428,7 @@ private object MakeList(Type targetType, PhpToken token) { return result; } - private object MakeDictionary(Type targetType, PhpToken token) { + private object MakeDictionary(Type targetType, in PhpToken token) { var result = (IDictionary)Activator.CreateInstance(targetType); if (result == null) { throw new NullReferenceException($"Activator.CreateInstance({targetType.FullName}) returned null"); @@ -439,7 +458,7 @@ private object MakeDictionary(Type targetType, PhpToken token) { return result; } - private object MakeCollection(PhpToken token) { + private object MakeCollection(in PhpToken token) { if (this._options.UseLists == ListOptions.Never) { return this.MakeDictionary(typeof(Dictionary), token); } @@ -451,7 +470,7 @@ private object MakeCollection(PhpToken token) { isList = false; break; } else { - var key = this._tokens[_currentToken+i].Value.PhpToLong(); + var key = this._tokens[_currentToken+i].Value.GetLong(_input); if (i == 0 || key == previousKey + 1) { previousKey = key; } else { @@ -477,4 +496,8 @@ private object MakeCollection(PhpToken token) { return result; } } + + private string GetString(in PhpToken token) { + return token.Value.GetString(this._input, this._options.InputEncoding); + } } \ No newline at end of file diff --git a/PhpSerializerNET/Deserialization/PhpToken.cs b/PhpSerializerNET/Deserialization/PhpToken.cs index 9fd710c..510d299 100644 --- a/PhpSerializerNET/Deserialization/PhpToken.cs +++ b/PhpSerializerNET/Deserialization/PhpToken.cs @@ -4,7 +4,6 @@ This Source Code Form is subject to the terms of the Mozilla Public file, You can obtain one at http://mozilla.org/MPL/2.0/. **/ namespace PhpSerializerNET; - #nullable enable /// @@ -14,11 +13,12 @@ internal readonly struct PhpToken { internal readonly PhpDataType Type; internal readonly int Position; internal readonly int Length; - internal readonly string? Value; - internal PhpToken(PhpDataType type, int position, string? value = null, int length = 0) { + + internal readonly ValueSpan Value; + internal PhpToken(PhpDataType type, int position, in ValueSpan value, int length = 0) { this.Type = type; this.Position = position; this.Value = value; this.Length = length; } -} \ No newline at end of file +} diff --git a/PhpSerializerNET/Deserialization/PhpTokenizer.cs b/PhpSerializerNET/Deserialization/PhpTokenizer.cs index 1156274..cccd90b 100644 --- a/PhpSerializerNET/Deserialization/PhpTokenizer.cs +++ b/PhpSerializerNET/Deserialization/PhpTokenizer.cs @@ -7,21 +7,18 @@ This Source Code Form is subject to the terms of the Mozilla Public using System; using System.Globalization; using System.Runtime.CompilerServices; -using System.Text; #nullable enable namespace PhpSerializerNET; public ref struct PhpTokenizer { - private readonly Encoding _inputEncoding; - private readonly ReadOnlySpan _input; private Span _tokens; + private readonly ReadOnlySpan _input; private int _position; private int _tokenPosition; - private PhpTokenizer(ReadOnlySpan input, Encoding inputEncoding, Span array) { - this._inputEncoding = inputEncoding; + private PhpTokenizer(ReadOnlySpan input, Span array) { this._input = input; this._tokens = array; this._position = 0; @@ -39,12 +36,12 @@ private void Advance(int positons) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetNumbers() { + private ValueSpan GetNumbers() { int start = this._position; while (this._input[this._position] != (byte)';') { this._position++; } - return this._inputEncoding.GetString(this._input.Slice(start, this._position - start)); + return new ValueSpan(start, this._position-start); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -66,7 +63,7 @@ internal void GetToken() { this.GetBooleanToken(); break; case (byte)'N': - this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position - 1); + this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position - 1, ValueSpan.Empty); this.Advance(); break; case (byte)'s': @@ -89,7 +86,7 @@ internal void GetToken() { [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GetNullToken() { - this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position - 1); + this._tokens[this._tokenPosition++] = new PhpToken(PhpDataType.Null, _position - 1, ValueSpan.Empty); this.Advance(); } @@ -99,9 +96,7 @@ private void GetBooleanToken() { this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Boolean, _position - 2, - this._input[this._position++] == (byte)'1' - ? "1" - : "0" + new ValueSpan(this._position++, 1) ); this.Advance(); } @@ -115,7 +110,7 @@ private void GetStringToken() { this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.String, position, - _inputEncoding.GetString(this._input.Slice(this._position, length)) + new ValueSpan(this._position, length) ); this.Advance(2 + length); } @@ -150,7 +145,7 @@ private void GetArrayToken() { this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Array, position, - null, + ValueSpan.Empty, length ); this.Advance(2); @@ -166,13 +161,13 @@ private void GetObjectToken() { this.Advance(); int classNameLength = this.GetLength(); this.Advance(2); - string className = _inputEncoding.GetString(this._input.Slice(this._position, classNameLength)); + ValueSpan classNameSpan = new ValueSpan(this._position, classNameLength); this.Advance(2 + classNameLength); int propertyCount = this.GetLength(); this._tokens[this._tokenPosition++] = new PhpToken( PhpDataType.Object, position, - className, + classNameSpan, propertyCount ); this.Advance(2); @@ -182,7 +177,7 @@ private void GetObjectToken() { this.Advance(); } - internal static void Tokenize(ReadOnlySpan inputBytes, Encoding inputEncoding, Span tokens) { - new PhpTokenizer(inputBytes, inputEncoding, tokens).GetToken(); + internal static void Tokenize(ReadOnlySpan inputBytes, Span tokens) { + new PhpTokenizer(inputBytes, tokens).GetToken(); } } \ No newline at end of file diff --git a/PhpSerializerNET/Deserialization/ValueSpan.cs b/PhpSerializerNET/Deserialization/ValueSpan.cs new file mode 100644 index 0000000..d0b5aa4 --- /dev/null +++ b/PhpSerializerNET/Deserialization/ValueSpan.cs @@ -0,0 +1,46 @@ +/** + 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/. +**/ +namespace PhpSerializerNET; + +using System; +using System.Globalization; +using System.Text; + +internal readonly struct ValueSpan { + private static ValueSpan _empty = new ValueSpan(0,0); + internal readonly int Start; + internal readonly int Length; + public ValueSpan(int start, int length) { + this.Start = start; + this.Length = length; + } + + public static ValueSpan Empty => _empty; + + public ReadOnlySpan GetSlice(ReadOnlySpan input) => input.Slice(this.Start, this.Length); + + + internal double GetDouble(ReadOnlySpan input) { + var value = input.Slice(Start, Length); + return value switch { + [(byte)'I', (byte)'N', (byte)'F'] => double.PositiveInfinity, + [(byte)'-', (byte)'I', (byte)'N', (byte)'F'] => double.NegativeInfinity, + [(byte)'N', (byte)'A', (byte)'N'] => double.NaN, + _ => double.Parse(value, CultureInfo.InvariantCulture), + }; + } + + internal bool GetBool(ReadOnlySpan input) => input[this.Start] == '1'; + + internal long GetLong(ReadOnlySpan input) => long.Parse( + input.Slice(this.Start, this.Length), + CultureInfo.InvariantCulture + ); + + internal string GetString(ReadOnlySpan input, Encoding inputEncoding) { + return inputEncoding.GetString(input.Slice(this.Start, this.Length)); + } +} \ No newline at end of file diff --git a/PhpSerializerNET/PhpSerialization.cs b/PhpSerializerNET/PhpSerialization.cs index 8ccf008..4d8a02f 100644 --- a/PhpSerializerNET/PhpSerialization.cs +++ b/PhpSerializerNET/PhpSerialization.cs @@ -9,23 +9,10 @@ This Source Code Form is subject to the terms of the Mozilla Public using System; using System.Collections.Generic; -using System.Text; namespace PhpSerializerNET; public static class PhpSerialization { - private static Span Tokenize(ReadOnlySpan input, Encoding inputEncoding) { - int size = inputEncoding.GetByteCount(input); - Span inputBytes = size < 256 - ? stackalloc byte[size] - : new byte[size]; - inputEncoding.GetBytes(input, inputBytes); - int tokenCount = PhpTokenValidator.Validate(inputBytes); - Span tokens = new PhpToken[tokenCount]; - PhpTokenizer.Tokenize(inputBytes, inputEncoding, tokens); - return tokens; - } - /// /// Reset the type lookup cache. /// Can be useful for scenarios in which new types are loaded at runtime in between deserialization tasks. @@ -64,7 +51,15 @@ private static Span Tokenize(ReadOnlySpan input, Encoding inputE if (options == null) { options = PhpDeserializationOptions.DefaultOptions; } - return new PhpDeserializer(Tokenize(input, options.InputEncoding), options).Deserialize(); + int size = options.InputEncoding.GetByteCount(input); + Span inputBytes = size < 256 + ? stackalloc byte[size] + : new byte[size]; + options.InputEncoding.GetBytes(input, inputBytes); + int tokenCount = PhpTokenValidator.Validate(inputBytes); + Span tokens = new PhpToken[tokenCount]; + PhpTokenizer.Tokenize(inputBytes, tokens); + return new PhpDeserializer(tokens, inputBytes, options).Deserialize(); } /// @@ -93,7 +88,15 @@ public static T Deserialize( if (options == null) { options = PhpDeserializationOptions.DefaultOptions; } - return new PhpDeserializer(Tokenize(input, options.InputEncoding), options).Deserialize(); + int size = options.InputEncoding.GetByteCount(input); + Span inputBytes = size < 256 + ? stackalloc byte[size] + : new byte[size]; + options.InputEncoding.GetBytes(input, inputBytes); + int tokenCount = PhpTokenValidator.Validate(inputBytes); + Span tokens = new PhpToken[tokenCount]; + PhpTokenizer.Tokenize(inputBytes, tokens); + return new PhpDeserializer(tokens, inputBytes, options).Deserialize(); } /// @@ -126,7 +129,15 @@ public static T Deserialize( if (options == null) { options = PhpDeserializationOptions.DefaultOptions; } - return new PhpDeserializer(Tokenize(input, options.InputEncoding), options).Deserialize(type); + int size = options.InputEncoding.GetByteCount(input); + Span inputBytes = size < 256 + ? stackalloc byte[size] + : new byte[size]; + options.InputEncoding.GetBytes(input, inputBytes); + int tokenCount = PhpTokenValidator.Validate(inputBytes); + Span tokens = new PhpToken[tokenCount]; + PhpTokenizer.Tokenize(inputBytes, tokens); + return new PhpDeserializer(tokens, inputBytes, options).Deserialize(type); } /// diff --git a/PhpSerializerNET/PhpSerializerNET.csproj b/PhpSerializerNET/PhpSerializerNET.csproj index 63994d6..c168b97 100644 --- a/PhpSerializerNET/PhpSerializerNET.csproj +++ b/PhpSerializerNET/PhpSerializerNET.csproj @@ -2,7 +2,7 @@ PhpSerializerNET net8.0 - 10.0 + 12.0 1.4.0 StringEpsilon A library for working with the PHP serialization format.