Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ValveKeyValue/ValveKeyValue/DefaultObjectReflector.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace ValveKeyValue
{
sealed class DefaultObjectReflector : IObjectReflector
{
IEnumerable<IObjectMember> IObjectReflector.GetMembers(object @object)
IEnumerable<IObjectMember> IObjectReflector.GetMembers([DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, object @object)
{
Require.NotNull(objectType, nameof(objectType));
Require.NotNull(@object, nameof(@object));

var objectType = @object.GetType();

if (IsValueTupleType(objectType))
{
var fields = objectType.GetFields(BindingFlags.Instance | BindingFlags.Public);
Expand Down
3 changes: 3 additions & 0 deletions ValveKeyValue/ValveKeyValue/FieldMember.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace ValveKeyValue
Expand All @@ -20,6 +21,8 @@ public FieldMember(FieldInfo fieldInfo, object @object)

public string Name => fieldInfo.Name;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2073", Justification = "FieldType")]
[DynamicallyAccessedMembers(Trimming.Properties)]
public Type MemberType => fieldInfo.FieldType;

public object Value
Expand Down
3 changes: 3 additions & 0 deletions ValveKeyValue/ValveKeyValue/IObjectMember.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;

namespace ValveKeyValue
{
interface IObjectMember
Expand All @@ -6,6 +8,7 @@ interface IObjectMember

string Name { get; }

[DynamicallyAccessedMembers(Trimming.Properties)]
Type MemberType { get; }

object Value { get; set; }
Expand Down
4 changes: 3 additions & 1 deletion ValveKeyValue/ValveKeyValue/IObjectReflector.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics.CodeAnalysis;

namespace ValveKeyValue
{
interface IObjectReflector
{
IEnumerable<IObjectMember> GetMembers(object @object);
IEnumerable<IObjectMember> GetMembers([DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, object @object);
}
}
5 changes: 3 additions & 2 deletions ValveKeyValue/ValveKeyValue/KVSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using ValveKeyValue.Abstraction;
using ValveKeyValue.Deserialization;
using ValveKeyValue.Deserialization.KeyValues1;
Expand Down Expand Up @@ -52,7 +53,7 @@ public KVDocument Deserialize(Stream stream, KVSerializerOptions options = null)
/// <param name="options">Options to use that can influence the deserialization process.</param>
/// <returns>A <typeparamref name="TObject" /> instance representing the KeyValues structure in the stream.</returns>
/// <typeparam name="TObject">The type of object to deserialize.</typeparam>;
public TObject Deserialize<TObject>(Stream stream, KVSerializerOptions options = null)
public TObject Deserialize<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(Stream stream, KVSerializerOptions options = null)
{
Require.NotNull(stream, nameof(stream));

Expand Down Expand Up @@ -91,7 +92,7 @@ public void Serialize(Stream stream, KVDocument data, KVSerializerOptions option
/// <param name="name">The top-level object name</param>
/// <param name="options">Options to use that can influence the serialization process.</param>
/// <typeparam name="TData">The type of object to serialize.</typeparam>
public void Serialize<TData>(Stream stream, TData data, string name, KVSerializerOptions options = null)
public void Serialize<[DynamicallyAccessedMembers(Trimming.Properties)] TData>(Stream stream, TData data, string name, KVSerializerOptions options = null)
{
Require.NotNull(stream, nameof(stream));

Expand Down
111 changes: 77 additions & 34 deletions ValveKeyValue/ValveKeyValue/ObjectCopier.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
using System.Collections;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;

namespace ValveKeyValue
{
// TODO: Migrate to IVisitationListener
static class ObjectCopier
{
public static TObject MakeObject<TObject>(KVObject keyValueObject)
public static TObject MakeObject<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(KVObject keyValueObject)
=> MakeObject<TObject>(keyValueObject, new DefaultObjectReflector());

public static object MakeObject(Type objectType, KVObject keyValueObject, IObjectReflector reflector)
public static object MakeObject(
[DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, KVObject keyValueObject, IObjectReflector reflector)
=> InvokeGeneric(nameof(MakeObject), objectType, new object[] { keyValueObject, reflector });

public static TObject MakeObject<TObject>(KVObject keyValueObject, IObjectReflector reflector)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2062", Justification = "If the lookup value type exists at runtime then it should have enough for us to introspect.")]
public static TObject MakeObject<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TObject>(KVObject keyValueObject, IObjectReflector reflector)
{
Require.NotNull(keyValueObject, nameof(keyValueObject));
Require.NotNull(reflector, nameof(reflector));
Expand All @@ -43,8 +46,8 @@ public static TObject MakeObject<TObject>(KVObject keyValueObject, IObjectReflec

// The object must remain boxed until it is fully initiallized, as this is the only way
// that we can build a struct due to the nature of struct copying.
var typedObject = FormatterServices.GetUninitializedObject(typeof(TObject));
CopyObject(keyValueObject, typedObject, reflector);
var typedObject = RuntimeHelpers.GetUninitializedObject(typeof(TObject));
CopyObject(keyValueObject, typeof(TObject), typedObject, reflector);
return (TObject)typedObject;
}
else if (TryConvertValueTo<TObject>(keyValueObject.Name, keyValueObject.Value, out var converted))
Expand All @@ -58,10 +61,18 @@ public static TObject MakeObject<TObject>(KVObject keyValueObject, IObjectReflec
}
}

public static KVObject FromObject(Type objectType, object managedObject, string topLevelName)
public static KVObject FromObject(
[DynamicallyAccessedMembers(Trimming.Properties)] Type objectType,
object managedObject,
string topLevelName)
=> FromObjectCore(objectType, managedObject, topLevelName, new DefaultObjectReflector(), new HashSet<object>());

static KVObject FromObjectCore(Type objectType, object managedObject, string topLevelName, IObjectReflector reflector, HashSet<object> visitedObjects)
static KVObject FromObjectCore(
[DynamicallyAccessedMembers(Trimming.Properties)] Type objectType,
object managedObject,
string topLevelName,
IObjectReflector reflector,
HashSet<object> visitedObjects)
{
if (managedObject == null)
{
Expand All @@ -76,7 +87,12 @@ static KVObject FromObjectCore(Type objectType, object managedObject, string top
return new KVObject(topLevelName, transformedValue);
}

static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObjectReflector reflector, HashSet<object> visitedObjects)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "If the IDictionary's value object already exists at runtime then its properties will too.")]
static KVValue ConvertObjectToValue(
[DynamicallyAccessedMembers(Trimming.Properties)] Type objectType,
object managedObject,
IObjectReflector reflector,
HashSet<object> visitedObjects)
{
if (!objectType.IsValueType && objectType != typeof(string) && !visitedObjects.Add(managedObject))
{
Expand Down Expand Up @@ -108,38 +124,35 @@ static KVValue ConvertObjectToValue(Type objectType, object managedObject, IObje
var counter = 0;
foreach (var child in (IEnumerable)managedObject)
{
var childKVObject = CopyObject(child, counter.ToString(), reflector, visitedObjects);
var childKVObject = FromObjectCore(child.GetType(), child, counter.ToString(), reflector, visitedObjects);
childObjects.Add(childKVObject);

counter++;
}
}
else
{
foreach (var member in reflector.GetMembers(managedObject).OrderBy(p => p.Name, StringComparer.InvariantCulture))
foreach (var member in reflector.GetMembers(objectType, managedObject).OrderBy(p => p.Name, StringComparer.InvariantCulture))
{
if (!member.MemberType.IsValueType && member.Value is null)
{
continue;
}

childObjects.Add(CopyObject(member.Value, member.Name, reflector, visitedObjects));
childObjects.Add(FromObjectCore(member.Value.GetType(), member.Value, member.Name, reflector, visitedObjects));
}
}

return childObjects;
}

static KVObject CopyObject(object @object, string name, IObjectReflector reflector, HashSet<object> visitedObjects)
=> FromObjectCore(@object.GetType(), @object, name, reflector, visitedObjects);

static void CopyObject(KVObject kv, object obj, IObjectReflector reflector)
static void CopyObject(KVObject kv, [DynamicallyAccessedMembers(Trimming.Properties)] Type objectType, object obj, IObjectReflector reflector)
{
Require.NotNull(kv, nameof(kv));
Require.NotNull(obj, nameof(obj));
Require.NotNull(reflector, nameof(reflector));

var members = reflector.GetMembers(obj).ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase);
var members = reflector.GetMembers(objectType, obj).ToDictionary(m => m.Name, m => m, StringComparer.OrdinalIgnoreCase);

foreach (var item in kv.Children)
{
Expand All @@ -148,7 +161,8 @@ static void CopyObject(KVObject kv, object obj, IObjectReflector reflector)
continue;
}

member.Value = MakeObject(member.MemberType, item, reflector);
var convertedValue = MakeObject(member.MemberType, item, reflector);
member.Value = convertedValue;
}
}

Expand Down Expand Up @@ -208,11 +222,18 @@ static bool IsLookupWithStringKey(Type type, out Type valueType)
return true;
}

static object MakeLookup(Type valueType, IEnumerable<KVObject> items, IObjectReflector reflector)
static object MakeLookup(
[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] Type valueType,
IEnumerable<KVObject> items,
IObjectReflector reflector)
=> InvokeGeneric(nameof(MakeLookupCore), valueType, new object[] { items, reflector });

static ILookup<string, TValue> MakeLookupCore<TValue>(IEnumerable<KVObject> items, IObjectReflector reflector)
=> items.ToLookup(kv => kv.Name, kv => ConvertValue<TValue>(kv.Value, reflector));
static ILookup<string, TValue> MakeLookupCore<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(IEnumerable<KVObject> items, IObjectReflector reflector)
{
TValue valueConversionFunc(KVObject kv) => ConvertValue<TValue>(kv.Value, reflector);

return items.ToLookup(kv => kv.Name, valueConversionFunc);
}

static readonly Dictionary<Type, Func<Type, object[], IObjectReflector, object>> EnumerableBuilders = new()
{
Expand All @@ -223,7 +244,13 @@ static ILookup<string, TValue> MakeLookupCore<TValue>(IEnumerable<KVObject> item
[typeof(ObservableCollection<>)] = (type, values, reflector) => InvokeGeneric(nameof(MakeObservableCollection), type.GetGenericArguments()[0], new object[] { values, reflector }),
};

static bool ConstructTypedEnumerable(Type type, object[] values, IObjectReflector reflector, out object typedEnumerable)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "If our T[] array exists then so much the element T.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "If our T[] array exists then so much the element T.")]
static bool ConstructTypedEnumerable(
Type type,
object[] values,
IObjectReflector reflector,
out object typedEnumerable)
{
object listObject = null;

Expand Down Expand Up @@ -274,6 +301,9 @@ static bool IsConstructibleEnumerableType(Type type)
return false;
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Analysis cannot follow MakeGenericMethod. All callers validated manually.")]
static object InvokeGeneric(string methodName, Type genericType, params object[] parameters)
{
var method = typeof(ObjectCopier)
Expand All @@ -292,18 +322,22 @@ static object InvokeGeneric(string methodName, Type genericType, params object[]
}
}

static List<TElement> MakeList<TElement>(object[] items, IObjectReflector reflector)
static List<TElement> MakeList<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector)
{
return items.Select(i => ConvertValue<TElement>(i, reflector))
.ToList();
var list = new List<TElement>(capacity: items.Length);
foreach (var item in items)
{
list.Add(ConvertValue<TElement>(item, reflector));
}
return list;
}

static Collection<TElement> MakeCollection<TElement>(object[] items, IObjectReflector reflector)
static Collection<TElement> MakeCollection<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector)
{
return new Collection<TElement>(MakeList<TElement>(items, reflector));
}

static ObservableCollection<TElement> MakeObservableCollection<TElement>(object[] items, IObjectReflector reflector)
static ObservableCollection<TElement> MakeObservableCollection<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TElement>(object[] items, IObjectReflector reflector)
{
return new ObservableCollection<TElement>(MakeList<TElement>(items, reflector));
}
Expand Down Expand Up @@ -334,20 +368,25 @@ static bool IsDictionary(Type type)
return true;
}

static object MakeDictionary(Type type, KVObject kv, IObjectReflector reflector)
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", Justification = "Analysis cannot follow MakeGenericMethod but we should be clear by here anyway.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Analysis cannot follow MakeGenericMethod but we should be clear by here anyway.")]
static object MakeDictionary(
[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] Type type,
KVObject kv,
IObjectReflector reflector)
{
var dictionary = Activator.CreateInstance(type);
var genericArguments = type.GetGenericArguments();

typeof(ObjectCopier)
.GetMethod(nameof(FillDictionary), BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(genericArguments)
var method = typeof(ObjectCopier)
.GetMethod(nameof(FillDictionary), BindingFlags.Static | BindingFlags.NonPublic);
method.MakeGenericMethod(genericArguments)
.Invoke(null, new[] { dictionary, kv, reflector });

return dictionary;
}

static void FillDictionary<TKey, TValue>(Dictionary<TKey, TValue> dictionary, KVObject kv, IObjectReflector reflector)
static void FillDictionary<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TKey, [DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(Dictionary<TKey, TValue> dictionary, KVObject kv, IObjectReflector reflector)
{
foreach (var item in kv.Children)
{
Expand All @@ -363,9 +402,13 @@ static void FillDictionary<TKey, TValue>(Dictionary<TKey, TValue> dictionary, KV
}
}

static TValue ConvertValue<TValue>(object value, IObjectReflector reflector) => (TValue)ConvertValue(value, typeof(TValue), reflector);
static TValue ConvertValue<[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] TValue>(object value, IObjectReflector reflector)
=> (TValue)ConvertValue(value, typeof(TValue), reflector);

static object ConvertValue(object value, Type valueType, IObjectReflector reflector)
static object ConvertValue(
object value,
[DynamicallyAccessedMembers(Trimming.Constructors | Trimming.Properties)] Type valueType,
IObjectReflector reflector)
{
if (value is KVCollectionValue collectionValue)
{
Expand Down
3 changes: 3 additions & 0 deletions ValveKeyValue/ValveKeyValue/PropertyMember.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace ValveKeyValue
Expand All @@ -21,6 +22,8 @@ public PropertyMember(PropertyInfo propertyInfo, object @object)
string IObjectMember.Name
=> PropertyAttribute?.PropertyName ?? propertyInfo.Name;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2073", Justification = "PropertyType")]
[DynamicallyAccessedMembers(Trimming.Properties)]
Type IObjectMember.MemberType => propertyInfo.PropertyType;

object IObjectMember.Value
Expand Down
17 changes: 17 additions & 0 deletions ValveKeyValue/ValveKeyValue/Trimming.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Diagnostics.CodeAnalysis;

namespace ValveKeyValue
{
internal static class Trimming
{
public const DynamicallyAccessedMemberTypes Constructors =
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor |
DynamicallyAccessedMemberTypes.PublicConstructors |
DynamicallyAccessedMemberTypes.NonPublicConstructors;

public const DynamicallyAccessedMemberTypes Properties =
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties |
DynamicallyAccessedMemberTypes.PublicFields;
}
}
3 changes: 2 additions & 1 deletion ValveKeyValue/ValveKeyValue/ValveKeyValue.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<Product>Valve KeyValue Library</Product>
<Description>Library to parse and write Valve KeyValue formats</Description>
Expand All @@ -10,6 +10,7 @@
<PackageTags>steam valve keyvalues keyvalue kv kv3 csgo dota2 tf2</PackageTags>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>ValveKeyValue.snk</AssemblyOriginatorKeyFile>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\"/>
Expand Down
Loading