From 2d4c48fcb6aded904e5d7a9d0c8d109314119d09 Mon Sep 17 00:00:00 2001 From: StringEpsilon Date: Sun, 17 Apr 2022 22:33:16 +0200 Subject: [PATCH] PoC: Allow integer property keys. --- .../Serialize/ArraySerialization.cs | 49 ++++++++++++++++++- PhpSerializerNET/Attributes/PhpProperty.cs | 11 +++++ .../Extensions/ArrayExtensions.cs | 16 +++--- PhpSerializerNET/PhpDeserializer.cs | 17 +++++-- PhpSerializerNET/PhpSerializer.cs | 6 ++- 5 files changed, 86 insertions(+), 13 deletions(-) diff --git a/PhpSerializerNET.Test/Serialize/ArraySerialization.cs b/PhpSerializerNET.Test/Serialize/ArraySerialization.cs index a831a3e..e2094be 100644 --- a/PhpSerializerNET.Test/Serialize/ArraySerialization.cs +++ b/PhpSerializerNET.Test/Serialize/ArraySerialization.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.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace PhpSerializerNET.Test.Serialize { @@ -11,12 +12,58 @@ namespace PhpSerializerNET.Test.Serialize { public class ArraySerialization { [TestMethod] public void StringArraySerializaton() { - string[] data = new string[3] {"a", "b", "c"}; + string[] data = new string[3] { "a", "b", "c" }; Assert.AreEqual( "a:3:{i:0;s:1:\"a\";i:1;s:1:\"b\";i:2;s:1:\"c\";}", PhpSerialization.Serialize(data) ); } + + // TODO: Move to separate file. + public class MixedKeysObject { + [PhpProperty(0)] + public string Foo { get; set; } + [PhpProperty(1)] + public string Bar { get; set; } + [PhpProperty("a")] + public string Baz { get; set; } + [PhpProperty("b")] + public string Dummy { get; set; } + } + + [TestMethod] + public void ObjectIntoMixedKeyArray() { + var data = new MixedKeysObject() { + Foo = "Foo", + Bar = "Bar", + Baz = "A", + Dummy = "B", + }; + + Assert.AreEqual( + "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) + ); + } + + [TestMethod] + public void MixedKeyArrayIntoObject() { + var expected = new MixedKeysObject() { + Foo = "Foo", + Bar = "Bar", + Baz = "A", + Dummy = "B", + }; + + 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); + } } } \ No newline at end of file diff --git a/PhpSerializerNET/Attributes/PhpProperty.cs b/PhpSerializerNET/Attributes/PhpProperty.cs index 8856a5e..9cd88a8 100644 --- a/PhpSerializerNET/Attributes/PhpProperty.cs +++ b/PhpSerializerNET/Attributes/PhpProperty.cs @@ -11,9 +11,20 @@ namespace PhpSerializerNET { [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class PhpPropertyAttribute : Attribute { public string Name { get; set; } + public long Key { get; set; } + public bool IsInteger { get; private set; } = false; public PhpPropertyAttribute(string name) { this.Name = name; } + + /// + /// Define an integer key for a given property. + /// Note: This also affects serialization into object notation, as that is a legal way of representing an object. + /// + public PhpPropertyAttribute(long key) { + this.Key = key; + this.IsInteger = true; + } } } \ No newline at end of file diff --git a/PhpSerializerNET/Extensions/ArrayExtensions.cs b/PhpSerializerNET/Extensions/ArrayExtensions.cs index 3360aea..1cc64f8 100644 --- a/PhpSerializerNET/Extensions/ArrayExtensions.cs +++ b/PhpSerializerNET/Extensions/ArrayExtensions.cs @@ -29,8 +29,8 @@ public static string Utf8Substring(this byte[] array, int start, int length, Enc } } - public static Dictionary GetAllProperties(this PropertyInfo[] properties, PhpDeserializationOptions options) { - var result = new Dictionary(properties.Length); + public static Dictionary GetAllProperties(this PropertyInfo[] properties, PhpDeserializationOptions options) { + var result = new Dictionary(properties.Length); foreach (var property in properties) { var isIgnored = false; var attributes = PhpPropertyAttribute.GetCustomAttributes(property, false); @@ -45,10 +45,14 @@ public static Dictionary GetAllProperties(this PropertyInf } } if (phpPropertyAttribute != null) { - var attributeName = options.CaseSensitiveProperties - ? phpPropertyAttribute.Name - : phpPropertyAttribute.Name.ToLower(); - result.Add(attributeName, isIgnored ? null : property); + if (phpPropertyAttribute.IsInteger) { + result.Add(phpPropertyAttribute.Key, isIgnored ? null : property); + } else { + var attributeName = options.CaseSensitiveProperties + ? phpPropertyAttribute.Name + : phpPropertyAttribute.Name.ToLower(); + result.Add(attributeName, isIgnored ? null : property); + } } var propertyName = options.CaseSensitiveProperties ? property.Name diff --git a/PhpSerializerNET/PhpDeserializer.cs b/PhpSerializerNET/PhpDeserializer.cs index b9a9a95..0c49d79 100644 --- a/PhpSerializerNET/PhpDeserializer.cs +++ b/PhpSerializerNET/PhpDeserializer.cs @@ -21,7 +21,7 @@ internal class PhpDeserializer { }; private static readonly object TypeLookupCacheSyncObject = new(); - private static readonly Dictionary> PropertyInfoCache = new(); + private static readonly Dictionary> PropertyInfoCache = new(); private static readonly object PropertyInfoCacheSyncObject = new(); private static Dictionary> FieldInfoCache { get; set; } = new(); @@ -103,8 +103,7 @@ private object MakeClass(PhpSerializeToken token) { object constructedObject; Type targetType = null; if (typeName != "stdClass" && this._options.EnableTypeLookup) { - lock (TypeLookupCacheSyncObject) - { + lock (TypeLookupCacheSyncObject) { if (TypeLookupCache.ContainsKey(typeName)) { targetType = TypeLookupCache[typeName]; } else { @@ -393,7 +392,7 @@ private object MakeStruct(Type targetType, PhpSerializeToken token) { private object MakeObject(Type targetType, PhpSerializeToken token) { var result = Activator.CreateInstance(targetType); - Dictionary properties; + Dictionary properties; lock (PropertyInfoCacheSyncObject) { if (PropertyInfoCache.ContainsKey(targetType)) { properties = PropertyInfoCache[targetType]; @@ -406,7 +405,15 @@ private object MakeObject(Type targetType, PhpSerializeToken token) { } for (int i = 0; i < token.Children.Length; i += 2) { - var propertyName = this._options.CaseSensitiveProperties ? token.Children[i].Value : token.Children[i].Value.ToLower(); + object propertyName = null; + 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].ToLong(); + } else { + // todo: throw proper exception. + } + var valueToken = token.Children[i + 1]; if (!properties.ContainsKey(propertyName)) { diff --git a/PhpSerializerNET/PhpSerializer.cs b/PhpSerializerNET/PhpSerializer.cs index e290981..57077eb 100644 --- a/PhpSerializerNET/PhpSerializer.cs +++ b/PhpSerializerNET/PhpSerializer.cs @@ -242,13 +242,17 @@ private string SerializeToObject(object input) { return output.ToString(); } - private string SerializeMember(MemberInfo member, object input) { + private string SerializeMember(MemberInfo member, object input, bool isObjectMember = false) { PhpPropertyAttribute attribute = (PhpPropertyAttribute)Attribute.GetCustomAttribute( member, typeof(PhpPropertyAttribute), false ); + if (attribute?.IsInteger == true) { + return $"{this.Serialize(attribute.Key)}{this.Serialize(member.GetValue(input))}"; + } + var propertyName = attribute != null ? attribute.Name : member.Name;