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
51 changes: 51 additions & 0 deletions S1API.Tests/Internal/Utils/ReflectionUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,57 @@ public void DerivedTypeScanDoesNotFollowSameNameAssembliesWithDifferentIdentitie
AppDomain.CurrentDomain.GetAssemblies()));
}

[Theory]
[InlineData("S1API, Version=3.0.1.0, Culture=neutral, PublicKeyToken=null")]
[InlineData("S1API, Version=2.9.2.0, Culture=neutral, PublicKeyToken=null")]
[InlineData("S1API, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null")]
[InlineData("s1api, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null")]
public void AReferenceToAnyVersionOfTheBaseAssemblyBindsToIt(string reference)
{
var loaded = new AssemblyName("S1API, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null");

Assert.True(ReflectionUtils.ReferenceBindsToDefinition(new AssemblyName(reference), loaded));
}

[Fact]
public void AReferenceToADifferentAssemblyNameDoesNotBind()
{
var loaded = new AssemblyName("S1API, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null");

Assert.False(ReflectionUtils.ReferenceBindsToDefinition(
new AssemblyName("S1APILoader, Version=3.2.1.0, Culture=neutral, PublicKeyToken=null"),
loaded));
}

[Fact]
public void AReferenceWithADifferentPublicKeyTokenDoesNotBind()
{
var signed = new AssemblyName("Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
var unsigned = new AssemblyName("Example, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");

Assert.False(ReflectionUtils.ReferenceBindsToDefinition(signed, unsigned));
Assert.False(ReflectionUtils.ReferenceBindsToDefinition(unsigned, signed));
Assert.True(ReflectionUtils.ReferenceBindsToDefinition(signed, signed));
}

[Fact]
public void ALoadedAssemblyOfADifferentVersionIsFollowedOnlyWhenItIsTheOnlyOneOfThatName()
{
var reference = new AssemblyName("Library, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null");
var newer = new AssemblyName("Library, Version=1.6.0.0, Culture=neutral, PublicKeyToken=null");

Assert.True(ReflectionUtils.ShouldFollowLoadedReference(newer, reference, loadedAssembliesWithThatName: 1));
Assert.False(ReflectionUtils.ShouldFollowLoadedReference(newer, reference, loadedAssembliesWithThatName: 2));
}

[Fact]
public void ALoadedAssemblyWithTheExactIdentityIsAlwaysFollowed()
{
var reference = new AssemblyName("Library, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null");

Assert.True(ReflectionUtils.ShouldFollowLoadedReference(reference, reference, loadedAssembliesWithThatName: 2));
}

private static AssemblyBuilder CreateDynamicAssembly(string name, Version version)
{
var assemblyName = new AssemblyName(name)
Expand Down
46 changes: 44 additions & 2 deletions S1API/Internal/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private static bool ReferencesAssemblyTransitively(

foreach (AssemblyName referencedAssembly in referencedAssemblies)
{
if (AssemblyIdentityMatches(referencedAssembly, baseAssemblyName))
if (ReferenceBindsToDefinition(referencedAssembly, baseAssemblyName))
return true;

string referencedName = referencedAssembly.Name ?? string.Empty;
Expand All @@ -147,7 +147,10 @@ private static bool ReferencesAssemblyTransitively(

foreach (Assembly loadedReference in loadedReferences)
{
if (!AssemblyIdentityMatches(loadedReference.GetName(), referencedAssembly))
if (!ShouldFollowLoadedReference(
loadedReference.GetName(),
referencedAssembly,
loadedReferences.Length))
continue;

if (ReferencesAssemblyTransitively(
Expand All @@ -164,6 +167,45 @@ private static bool ReferencesAssemblyTransitively(
return false;
}

/// <summary>
/// INTERNAL: Does this reference bind to that assembly? The simple name and public key token must match; the
/// version need not, because the runtime binds a reference to whichever assembly of that name is loaded. A mod
/// built against an older S1API still runs against the loaded one, so it must still be scanned for the types
/// that derive from it.
/// </summary>
internal static bool ReferenceBindsToDefinition(
AssemblyName referenceAssemblyName,
AssemblyName definitionAssemblyName)
{
if (!string.Equals(
referenceAssemblyName.Name,
definitionAssemblyName.Name,
StringComparison.OrdinalIgnoreCase))
return false;

byte[] referenceToken = referenceAssemblyName.GetPublicKeyToken() ?? Array.Empty<byte>();
byte[] definitionToken = definitionAssemblyName.GetPublicKeyToken() ?? Array.Empty<byte>();
return referenceToken.SequenceEqual(definitionToken);
}

/// <summary>
/// INTERNAL: Should the scan follow a reference into this loaded assembly? An exact identity always; a different
/// version only when it is the one assembly of that name loaded, since then the reference can only bind to it.
/// With several same-named assemblies loaded it is unknown which one the reference means, so only an exact
/// match is followed.
/// </summary>
internal static bool ShouldFollowLoadedReference(
AssemblyName loadedAssemblyName,
AssemblyName referenceAssemblyName,
int loadedAssembliesWithThatName)
{
if (AssemblyIdentityMatches(loadedAssemblyName, referenceAssemblyName))
return true;

return loadedAssembliesWithThatName == 1
&& ReferenceBindsToDefinition(referenceAssemblyName, loadedAssemblyName);
}

private static bool AssemblyIdentityMatches(
AssemblyName referenceAssemblyName,
AssemblyName definitionAssemblyName) =>
Expand Down
Loading