Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use PayloadReader in System.Resources.Extensions #102379

Merged
merged 40 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
241157e
add System.Runtime.Serialization.BinaryFormat with tests
adamsitnik May 17, 2024
fdbaf50
set IsPackable to false so the package is not published anywhere, mak…
adamsitnik May 17, 2024
1da4a10
reference System.Runtime.Serialization.BinaryFormat in System.Resourc…
adamsitnik May 17, 2024
ba62c25
copy-paste the System.Windows.Forms.BinaryFormat code as is with no m…
adamsitnik May 17, 2024
f0c20fc
fix compiler errors caused by lack of global usings and supporting ol…
adamsitnik May 17, 2024
39dd889
change the namespace from System.Windows.Forms.BinaryFormat to System…
adamsitnik May 17, 2024
00608e4
switch
adamsitnik May 17, 2024
6354a91
try to handle the type and assembly name mangling
adamsitnik May 20, 2024
ba41ff0
Revert "try to handle the type and assembly name mangling"
adamsitnik May 20, 2024
cde8100
use BinaryFormatter as a fallback when we get NotSupportedException e…
adamsitnik May 20, 2024
3a90452
fix the bugs I've introduced when porting the code to older monikers
adamsitnik May 20, 2024
37200dc
fix the build error and remove outdated comment
adamsitnik May 20, 2024
6a36687
add switch and Compat tests that simply reference existing tests
adamsitnik May 20, 2024
d4de6ee
copy WinForms tests
adamsitnik May 22, 2024
1cb10c1
make the WinForms tests compile and pass:
adamsitnik May 22, 2024
eb0270f
fix the build (when creating a trimmed list, the serialized size must…
adamsitnik May 23, 2024
3b4cda3
change public API: ArrayRecord can represent multi-dimensional array …
adamsitnik May 23, 2024
cabd585
address code review feedback:
adamsitnik May 23, 2024
a92a528
remove the fallback to BF
adamsitnik May 24, 2024
6f6d03f
handle the type name mangling
adamsitnik May 24, 2024
891365f
fix the build?
adamsitnik May 24, 2024
7a60a3f
Apply suggestions from code review
adamsitnik May 27, 2024
ec629c9
Apply suggestions from code review
adamsitnik May 27, 2024
6fb3a54
Apply suggestions from code review
adamsitnik May 27, 2024
0b10f6b
address code review feedback:
adamsitnik May 27, 2024
a8c3948
don't run drawing tests on systems where drawing is not supported
adamsitnik May 28, 2024
b57bc2a
add WebSocketException to the exclusion list (because it's implementa…
adamsitnik May 28, 2024
a7134ef
address code review feedback:
adamsitnik May 29, 2024
42e8199
Apply suggestions from code review
adamsitnik Jun 3, 2024
00cb0a8
Apply suggestions from code review
adamsitnik Jun 3, 2024
243ec2e
address code review feedback:
adamsitnik Jun 3, 2024
c485e4a
optimize perf for reading arrays of primitive types, but keep it safe…
adamsitnik Jun 4, 2024
3c1e72c
rename ToArray to GetArray and avoid allocating new array if possible…
adamsitnik Jun 4, 2024
f175ff6
solve last TODO: fix max size array support by avoiding Int32 overflo…
adamsitnik Jun 5, 2024
d524a92
fix the build
adamsitnik Jun 5, 2024
489c66a
Apply suggestions from code review
adamsitnik Jun 6, 2024
697cb0a
address code review feedback:
adamsitnik Jun 6, 2024
e3d1a7b
Merge remote-tracking branch 'upstream/main' into plan
adamsitnik Jun 6, 2024
fdbd424
improve arrays support:
adamsitnik Jun 7, 2024
3eefd4e
Don't check if serialized DataTable and DataSet produce exact same by…
adamsitnik Jun 10, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.BinaryFormat;

namespace System.Resources.Extensions.BinaryFormat;

internal sealed partial class BinaryFormattedObject
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Parsing state.
/// </summary>
internal interface IParseState
{
BinaryReader Reader { get; }
IReadOnlyDictionary<int, SerializationRecord> RecordMap { get; }
Options Options { get; }
ITypeResolver TypeResolver { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;

namespace System.Resources.Extensions.BinaryFormat;

internal sealed partial class BinaryFormattedObject
{
/// <summary>
/// Resolver for types.
/// </summary>
internal interface ITypeResolver
{
/// <summary>
/// Resolves the given type name against the specified library.
/// </summary>
[RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")]
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
Type GetType(TypeName typeName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;

namespace System.Resources.Extensions.BinaryFormat;

#pragma warning disable SYSLIB0050 // Type or member is obsolete

internal sealed partial class BinaryFormattedObject
{
internal sealed class Options
{
/// <summary>
/// How exactly assembly names need to match for deserialization.
/// </summary>

adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
public FormatterAssemblyStyle AssemblyMatching { get; set; } = FormatterAssemblyStyle.Simple;

/// <summary>
/// Type name binder.
/// </summary>
public SerializationBinder? Binder { get; set; }

/// <summary>
/// Optional type <see cref="ISerializationSurrogate"/> provider.
/// </summary>
public ISurrogateSelector? SurrogateSelector { get; set; }

/// <summary>
/// Streaming context.
/// </summary>
public StreamingContext StreamingContext { get; set; } = new(StreamingContextStates.All);
}
}

#pragma warning restore SYSLIB0050 // Type or member is obsolete
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.BinaryFormat;

namespace System.Resources.Extensions.BinaryFormat;

internal sealed partial class BinaryFormattedObject
{
/// <summary>
/// Parsing state for <see cref="BinaryFormattedObject"/>.
/// </summary>
internal sealed class ParseState : IParseState
{
private readonly BinaryFormattedObject _format;

public ParseState(BinaryReader reader, BinaryFormattedObject format)
{
Reader = reader;
_format = format;
}

public BinaryReader Reader { get; }
public IReadOnlyDictionary<int, SerializationRecord> RecordMap => _format.RecordMap;
public Options Options => _format._options;
public ITypeResolver TypeResolver => _format.TypeResolver;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;

#pragma warning disable SYSLIB0050 // Type or member is obsolete

namespace System.Resources.Extensions.BinaryFormat;

internal sealed partial class BinaryFormattedObject
{
internal sealed class DefaultTypeResolver : ITypeResolver
{
private readonly FormatterAssemblyStyle _assemblyMatching;
private readonly SerializationBinder? _binder;

private readonly Dictionary<string, Assembly> _assemblies = [];
private readonly Dictionary<string, Type> _types = [];

internal DefaultTypeResolver(Options options)
{
_assemblyMatching = options.AssemblyMatching;
_binder = options.Binder;
}

/// <summary>
/// Resolves the given type name against the specified library.
/// </summary>
[RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")]
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
Type ITypeResolver.GetType(TypeName typeName)
{
Debug.Assert(typeName.AssemblyName is not null);

if (_types.TryGetValue(typeName.AssemblyQualifiedName, out Type? cachedType))
{
return cachedType;
}

if (_binder?.BindToType(typeName.AssemblyName.FullName, typeName.FullName) is Type binderType)
{
// BinaryFormatter is inconsistent about what caching behavior you get with binders.
// It would always cache the last item from the binder, but wouldn't put the result
// in the type cache. This could lead to inconsistent results if the binder didn't
// always return the same result for a given set of strings. Choosing to always cache
// for performance.

_types[typeName.AssemblyQualifiedName] = binderType;
return binderType;
}

if (!_assemblies.TryGetValue(typeName.AssemblyName.FullName, out Assembly? assembly))
{
AssemblyName assemblyName = typeName.AssemblyName.ToAssemblyName();
try
{
assembly = Assembly.Load(assemblyName);
}
catch
{
if (_assemblyMatching != FormatterAssemblyStyle.Simple)
{
throw;
}

assembly = Assembly.Load(assemblyName.Name!);
}

_assemblies.Add(typeName.AssemblyName.FullName, assembly);
}

Type? type = _assemblyMatching != FormatterAssemblyStyle.Simple
? assembly.GetType(typeName.FullName)
: GetSimplyNamedTypeFromAssembly(assembly, typeName);

_types[typeName.AssemblyQualifiedName] = type ?? throw new SerializationException($"Could not find type '{typeName}'.");
return type;
}

[RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String, Boolean, Boolean)")]
private static Type? GetSimplyNamedTypeFromAssembly(Assembly assembly, TypeName typeName)
Copy link
Member

@JeremyKuhne JeremyKuhne May 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is effectively a straight copy of the existing BinaryFormatter logic. All catch blocks in this class match the existing behavior.

{
// Catching any exceptions that could be thrown from a failure on assembly load
// This is necessary, for example, if there are generic parameters that are qualified
// with a version of the assembly that predates the one available.

try
{
return assembly.GetType(typeName.FullName, throwOnError: false, ignoreCase: false);
}
catch (TypeLoadException) { }
catch (FileNotFoundException) { }
catch (FileLoadException) { }
catch (BadImageFormatException) { }

return Type.GetType(typeName.FullName, ResolveSimpleAssemblyName, new TopLevelAssemblyTypeResolver(assembly).ResolveType, throwOnError: false);

static Assembly? ResolveSimpleAssemblyName(AssemblyName assemblyName)
{
try
{
return Assembly.Load(assemblyName);
}
catch { }

try
{
return Assembly.Load(assemblyName.Name!);
}
catch { }

return null;
}
}

private sealed class TopLevelAssemblyTypeResolver
{
private readonly Assembly _topLevelAssembly;

public TopLevelAssemblyTypeResolver(Assembly topLevelAssembly) => _topLevelAssembly = topLevelAssembly;

[RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String, Boolean, Boolean)")]
public Type? ResolveType(Assembly? assembly, string simpleTypeName, bool ignoreCase)
{
assembly ??= _topLevelAssembly;
return assembly.GetType(simpleTypeName, throwOnError: false, ignoreCase);
}
}
}
}

#pragma warning restore SYSLIB0050 // Type or member is obsolete
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.BinaryFormat;

namespace System.Resources.Extensions.BinaryFormat;

/// <summary>
/// Object model for the binary format put out by BinaryFormatter. It parses and creates a model but does not
/// instantiate any reference types outside of string.
/// </summary>
/// <remarks>
/// <para>
/// This is useful for explicitly controlling the rehydration of binary formatted data.
/// </para>
/// </remarks>
internal sealed partial class BinaryFormattedObject
{
#pragma warning disable SYSLIB0050 // Type or member is obsolete
internal static FormatterConverter DefaultConverter { get; } = new();
#pragma warning restore SYSLIB0050

private static readonly Options s_defaultOptions = new();
private readonly Options _options;

private ITypeResolver? _typeResolver;
private ITypeResolver TypeResolver => _typeResolver ??= new DefaultTypeResolver(_options);

/// <summary>
/// Creates <see cref="BinaryFormattedObject"/> by parsing <paramref name="stream"/>.
/// </summary>
public BinaryFormattedObject(Stream stream, Options? options = null)
{
_options = options ?? s_defaultOptions;

try
{
RootRecord = PayloadReader.Read(stream, out var readonlyRecordMap, leaveOpen: true);
RecordMap = readonlyRecordMap;
}
catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException)
{
// Make the exception easier to catch, but retain the original stack trace.
throw ex.ConvertToSerializationException();
}
catch (TargetInvocationException ex)
{
throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException();
}
}

/// <summary>
/// Deserializes the <see cref="BinaryFormattedObject"/> back to an object.
/// </summary>
[RequiresUnreferencedCode("Ultimately calls Assembly.GetType for type names in the data.")]
public object Deserialize()
{
try
{
return Deserializer.Deserializer.Deserialize(RootRecord.ObjectId, RecordMap, TypeResolver, _options);
}
catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException)
{
// Make the exception easier to catch, but retain the original stack trace.
throw ex.ConvertToSerializationException();
}
catch (TargetInvocationException ex)
{
throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException();
}
}

/// <summary>
/// The Id of the root record of the object graph.
/// </summary>
public SerializationRecord RootRecord { get; }

/// <summary>
/// Gets a record by it's identifier. Not all records have identifiers, only ones that
/// can be referenced by other records.
/// </summary>
public SerializationRecord this[Id id] => RecordMap[id];

public IReadOnlyDictionary<int, SerializationRecord> RecordMap { get; }
}
Loading