Skip to content

Commit

Permalink
BUGFIX: Address edge-cases for IsAccessibleFromType methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Washi1337 committed Dec 16, 2024
1 parent eb7a735 commit 71d345f
Show file tree
Hide file tree
Showing 7 changed files with 651 additions and 25 deletions.
41 changes: 41 additions & 0 deletions docs/guides/dotnet/member-tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,44 @@ MethodDefinition adder = property.AddMethod;
MethodDefinition remover = property.RemoveMethod;
MethodDefinition fire = property.FireMethod;
```

## Accessibility Tests

Most member definitions are marked with accessibility modifiers
(e.g., `public` or `private`). These modifiers puts limits on
which entities in an assembly can use the definition.

AsmResolver provides built-in methods to test whether a given
definition is accessible by another.

For example, given the following two classes:

```csharp
public class Class1
{
private int _field;

public void Method() { ... }
}

public class Class2 { ... }
```

AsmResolver can programmatically determine whether the private field
`_field` is accessible by another entity using the `IsAccessibleFromType`
and`CanAccessDefinition` methods:

```csharp
var module = ModuleDefinition.FromFile(...);

var class1 = module.TopLevelTypes.First(t => t.Name == "Class1");
var field = class1.Fields.First(f => f.Name == "_field");
var method = class1.Methods.First(m => m.Name == "Method");

var class2 = module.TopLevelTypes.First(t => t.Name == "Class2");

Console.WriteLine(field.IsAccessibleFromType(class1)); // True
Console.WriteLine(field.IsAccessibleFromType(class2)); // False
Console.WriteLine(method.CanAccessDefinition(field)); // True
Console.WriteLine(class2.CanAccessDefinition(field)); // False
```
28 changes: 22 additions & 6 deletions src/AsmResolver.DotNet/FieldDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,16 +405,32 @@ public bool IsImportedInModule(ModuleDefinition module)
/// <inheritdoc />
public bool IsAccessibleFromType(TypeDefinition type)
{
// The field is only accessible if its declaring type is accessible.
if (DeclaringType is not { } declaringType || !declaringType.IsAccessibleFromType(type))
return false;

var comparer = new SignatureComparer();
bool isInSameAssembly = comparer.Equals(declaringType.Module, type.Module);
// Public fields are always accessible.
if (IsPublic)
return true;

return IsPublic
|| isInSameAssembly && IsAssembly
|| comparer.Equals(DeclaringType, type);
// TODO: check if in the same family of declaring types.
// Types can always access their own fields.
if (SignatureComparer.Default.Equals(declaringType, type))
return true;

bool isInSameAssembly = SignatureComparer.Default.Equals(declaringType.Module, type.Module);

// Assembly (internal in C#) fields are accessible by types in the same assembly.
if (IsAssembly || IsFamilyOrAssembly)
return isInSameAssembly;

// Family (protected in C#) fields are accessible by any base type.
if ((IsFamily || IsFamilyOrAssembly || IsFamilyAndAssembly)
&& type.BaseType?.Resolve() is { } baseType)
{
return (!IsFamilyAndAssembly || isInSameAssembly) && IsAccessibleFromType(baseType);
}

return false;
}

/// <summary>
Expand Down
44 changes: 38 additions & 6 deletions src/AsmResolver.DotNet/MethodDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -803,19 +803,51 @@ public bool IsImportedInModule(ModuleDefinition module)

IMemberDefinition IMemberDescriptor.Resolve() => this;

/// <summary>
/// Determines whether the provided definition can be accessed by the method.
/// </summary>
/// <param name="definition">The definition to access.</param>
/// <returns><c>true</c> if this method can access <paramref name="definition"/>, <c>false</c> otherwise.</returns>
public bool CanAccessDefinition(IMemberDefinition definition)
{
if (definition is TypeDefinition type)
return IsAccessibleFromType(type);

if (definition.DeclaringType is { } declaringType)
return IsAccessibleFromType(declaringType);

return false;
}

/// <inheritdoc />
public bool IsAccessibleFromType(TypeDefinition type)
{
// The method is only accessible if its declaring type is accessible.
if (DeclaringType is not { } declaringType || !declaringType.IsAccessibleFromType(type))
return false;

var comparer = new SignatureComparer();
bool isInSameAssembly = comparer.Equals(declaringType.Module, type.Module);
// Public methods are always accessible.
if (IsPublic)
return true;

// Types can always access their own methods.
if (SignatureComparer.Default.Equals(declaringType, type))
return true;

bool isInSameAssembly = SignatureComparer.Default.Equals(declaringType.Module, type.Module);

// Assembly (internal in C#) methods are accessible by types in the same assembly.
if (IsAssembly || IsFamilyOrAssembly)
return isInSameAssembly;

// Family (protected in C#) methods are accessible by any base type.
if ((IsFamily || IsFamilyOrAssembly || IsFamilyAndAssembly)
&& type.BaseType?.Resolve() is { } baseType)
{
return (!IsFamilyAndAssembly || isInSameAssembly) && IsAccessibleFromType(baseType);
}

return IsPublic
|| isInSameAssembly && IsAssembly
|| comparer.Equals(DeclaringType, type);
// TODO: check if in the same family of declaring types.
return false;
}

/// <summary>
Expand Down
59 changes: 46 additions & 13 deletions src/AsmResolver.DotNet/TypeDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -769,29 +769,62 @@ public TypeSignature ToTypeSignature(bool isValueType)
/// <inheritdoc />
IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer);

/// <summary>
/// Determines whether the provided definition can be accessed by the type.
/// </summary>
/// <param name="definition">The definition to access.</param>
/// <returns><c>true</c> if this type can access <paramref name="definition"/>, <c>false</c> otherwise.</returns>
public bool CanAccessDefinition(IMemberDefinition definition)
{
return definition.IsAccessibleFromType(this);
}

/// <inheritdoc />
public bool IsAccessibleFromType(TypeDefinition type)
{
// TODO: Check types of the same family.
if (SignatureComparer.Default.Equals(this, type))
return true;

bool isInSameAssembly = SignatureComparer.Default.Equals(Module, type.Module);

if (this == type)
// Most common case: A top-level types is accessible by all other types in the same assembly, or types in
// a different assembly if this top-level type is public.
if (DeclaringType is not { } declaringType)
return IsPublic || isInSameAssembly;

// The current type is a nested type, which means in order to be accessible, `type` needs to be able to
// access the declaring type first before it can reach the current type.
if (!declaringType.IsAccessibleFromType(type))
return false;

// Types can always access their direct nested types.
if (SignatureComparer.Default.Equals(declaringType, type))
return true;

var comparer = new SignatureComparer();
bool isInSameAssembly = comparer.Equals(Module, type.Module);
// Assuming declaring type is accessible, then public nested types are always accessible.
if (IsNestedPublic)
return true;

if (IsNested)
// If the current type is marked assembly (internal in C#), `type` must be in the same assembly.
if (IsNestedAssembly || IsNestedFamilyOrAssembly)
return isInSameAssembly;

// If the current type is marked family (protected in C#), `type` must be in the type hierarchy.
//
// class A
// {
// protected class B {} // <-- `this`
// }
//
// class C : A {} // <-- `type` ( can access A+B )
//
if ((IsNestedFamily || IsNestedFamilyOrAssembly || IsNestedFamilyAndAssembly)
&& type.BaseType?.Resolve() is { } baseType)
{
if (DeclaringType is not { } declaringType || !declaringType.IsAccessibleFromType(type))
return false;

return IsNestedPublic
|| isInSameAssembly && IsNestedAssembly
|| DeclaringType == type;
return (!IsNestedFamilyAndAssembly || isInSameAssembly) && IsAccessibleFromType(baseType);
}

return IsPublic
|| isInSameAssembly;
return false;
}

/// <summary>
Expand Down
147 changes: 147 additions & 0 deletions test/AsmResolver.DotNet.Tests/AccessibilityTest.Fields.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using AsmResolver.PE.DotNet.Metadata.Tables;
using Xunit;

namespace AsmResolver.DotNet.Tests;

public partial class AccessibilityTest
{
private static FieldDefinition AddField(TypeDefinition declaringType, string name, FieldAttributes attributes)
{
var field = new FieldDefinition(name, attributes, declaringType.Module!.CorLibTypeFactory.Object);
declaringType.Fields.Add(field);
return field;
}

[Theory]
[InlineData(FieldAttributes.Public)]
[InlineData(FieldAttributes.Family)]
[InlineData(FieldAttributes.Assembly)]
[InlineData(FieldAttributes.FamilyAndAssembly)]
[InlineData(FieldAttributes.FamilyOrAssembly)]
[InlineData(FieldAttributes.Private)]
public void TypeCanAccessOwnField(FieldAttributes attributes)
{
var module = CreateDummyModule("Module1");
var type = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var field = AddField(type, "Field1", attributes);

Assert.True(field.IsAccessibleFromType(type));
}

[Fact]
public void TypeCanAccessPublicFieldOtherType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var type2 = AddTopLevelType(module, "Type2", TypeAttributes.Public);
var field = AddField(type2, "Field1", FieldAttributes.Public);

Assert.True(field.IsAccessibleFromType(type1));
}

[Fact]
public void TypeCanAccessAssemblyFieldOtherTypeSameAssembly()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var type2 = AddTopLevelType(module, "Type2", TypeAttributes.Public);
var field = AddField(type2, "Field1", FieldAttributes.Assembly);

Assert.True(field.IsAccessibleFromType(type1));
}

[Fact]
public void TypeCannotAccessAssemblyFieldOtherTypeDifferentAssembly()
{
var module1 = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module1, "Type1", TypeAttributes.Public);

var module2 = CreateDummyModule("Module2");
var type2 = AddTopLevelType(module2, "Type2", TypeAttributes.Public);
var field = AddField(type2, "Field1", FieldAttributes.Assembly);

Assert.False(field.IsAccessibleFromType(type1));
}

[Fact]
public void TypeCannotAccessPrivateFieldOtherType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var type2 = AddTopLevelType(module, "Type2", TypeAttributes.Public);
var field = AddField(type2, "Field1", FieldAttributes.Private);

Assert.False(field.IsAccessibleFromType(type1));
}

[Fact]
public void TypeCannotAccessPublicFieldOtherNonAccessibleType()
{
var module1 = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module1, "Type1", TypeAttributes.Public);

var module2 = CreateDummyModule("Module2");
var type2 = AddTopLevelType(module2, "Type2", TypeAttributes.NotPublic);
var field = AddField(type2, "Field1", FieldAttributes.Public);

Assert.False(field.IsAccessibleFromType(type1));
}

[Fact]
public void TypeCanAccessFamilyFieldInBaseType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var field = AddField(type1, "Field1", FieldAttributes.Family);
var type2 = AddTopLevelType(module, "Type2", TypeAttributes.Public, type1);

Assert.True(field.IsAccessibleFromType(type2));
}

[Fact]
public void TypeCannotAccessFamilyFieldInNonBaseType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var field = AddField(type1, "Field1", FieldAttributes.Family);
var type2 = AddTopLevelType(module, "Type2", TypeAttributes.Public);

Assert.False(field.IsAccessibleFromType(type2));
}

[Fact]
public void TypeCanAccessFamilyFieldInFarBaseType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var field = AddField(type1, "Field1", FieldAttributes.Family);
var type2 = AddTopLevelType(module, "Type2", TypeAttributes.Public, type1);
var type3 = AddTopLevelType(module, "Type3", TypeAttributes.Public, type2);
var type4 = AddTopLevelType(module, "Type4", TypeAttributes.Public, type3);
var type5 = AddTopLevelType(module, "Type5", TypeAttributes.Public, type4);

Assert.True(field.IsAccessibleFromType(type5));
}

[Fact]
public void TypeCannotAccessPrivateFieldInOwnNestedType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var type2 = AddNestedType(type1, "Type2", TypeAttributes.NestedPrivate);
var field = AddField(type2, "Field1", FieldAttributes.Private);

Assert.False(field.IsAccessibleFromType(type1));
}

[Fact]
public void NestedTypeCanAccessPrivateFieldInDeclaringType()
{
var module = CreateDummyModule("Module1");
var type1 = AddTopLevelType(module, "Type1", TypeAttributes.Public);
var field = AddField(type1, "Field1", FieldAttributes.Private);
var type2 = AddNestedType(type1, "Type2", TypeAttributes.NestedPrivate);

Assert.False(field.IsAccessibleFromType(type2));
}
}
Loading

0 comments on commit 71d345f

Please sign in to comment.