Skip to content

Commit

Permalink
Support filtering interop members with predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Aug 27, 2021
1 parent 20dcf69 commit 00420a1
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 59 deletions.
24 changes: 15 additions & 9 deletions Jint.Tests/Runtime/Domain/HiddenMembers.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
namespace Jint.Tests.Runtime.Domain
using System;

namespace Jint.Tests.Runtime.Domain
{
public class HiddenMembers
{
[Obsolete]
public string Field1 = "Field1";

public string Field2 = "Field2";

[Obsolete]
public string Member1 { get; set; } = "Member1";

public string Member2 { get; set; } = "Member2";
public string Method1()
{
return "Method1";
}
public string Method2()
{
return "Method2";
}

[Obsolete]
public string Method1() => "Method1";

public string Method2() => "Method2";
}
}
92 changes: 92 additions & 0 deletions Jint.Tests/Runtime/InteropTests.MemberAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using Jint.Native;
using Jint.Tests.Runtime.Domain;
using Xunit;

namespace Jint.Tests.Runtime
{
public partial class InteropTests
{
[Fact]
public void ShouldHideSpecificMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers)
{
if (member == nameof(HiddenMembers.Member2) || member == nameof(HiddenMembers.Method2))
{
return JsValue.Undefined;
}
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Member1", engine.Evaluate("m.Member1").ToString());
Assert.Equal("undefined", engine.Evaluate("m.Member2").ToString());
Assert.Equal("Method1", engine.Evaluate("m.Method1()").ToString());
// check the method itself, not its invokation as it would mean invoking "undefined"
Assert.Equal("undefined", engine.Evaluate("m.Method2").ToString());
}

[Fact]
public void ShouldOverrideMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers && member == nameof(HiddenMembers.Member1))
{
return "Orange";
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Orange", engine.Evaluate("m.Member1").ToString());
}

[Fact]
public void ShouldBeAbleToFilterMembers()
{
var engine = new Engine(options => options
.SetMemberFilter((member, state) => !Attribute.IsDefined(member, typeof(ObsoleteAttribute)))
);

engine.SetValue("m", new HiddenMembers());

Assert.True(engine.Evaluate("m.Field1").IsUndefined());
Assert.True(engine.Evaluate("m.Member1").IsUndefined());
Assert.True(engine.Evaluate("m.Method1").IsUndefined());

Assert.True(engine.Evaluate("m.Field2").IsString());
Assert.True(engine.Evaluate("m.Member2").IsString());
Assert.True(engine.Evaluate("m.Method2()").IsString());
}

[Fact]
public void ShouldBeAbleToHideGetType()
{
var engine = new Engine(options => options
.SetMemberFilter((member, state) => !Attribute.IsDefined(member, typeof(ObsoleteAttribute)))
);
engine.SetValue("m", new HiddenMembers());

Assert.True(engine.Evaluate("m.Method1").IsUndefined());

// reflection could bypass some safeguards
Assert.Equal("Method1", engine.Evaluate("m.GetType().GetMethod('Method1').Invoke(m, [])").AsString());

// but not when we forbid GetType
var hiddenGetTypeEngine = new Engine(options => options
.SetMemberFilter((member, state) => member.Name != nameof(GetType))
);
hiddenGetTypeEngine.SetValue("m", new HiddenMembers());
Assert.True(hiddenGetTypeEngine.Evaluate("m.GetType").IsUndefined());
}
}
}
45 changes: 1 addition & 44 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

namespace Jint.Tests.Runtime
{
public class InteropTests : IDisposable
public partial class InteropTests : IDisposable
{
private readonly Engine _engine;

Expand Down Expand Up @@ -2278,49 +2278,6 @@ public void ShouldBeAbleToJsonStringifyClrObjects()
Assert.Equal(jsValue, clrValue);
}

[Fact]
public void ShouldHideSpecificMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers)
{
if (member == nameof(HiddenMembers.Member2) || member == nameof(HiddenMembers.Method2))
{
return JsValue.Undefined;
}
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Member1", engine.Evaluate("m.Member1").ToString());
Assert.Equal("undefined", engine.Evaluate("m.Member2").ToString());
Assert.Equal("Method1", engine.Evaluate("m.Method1()").ToString());
// check the method itself, not its invokation as it would mean invoking "undefined"
Assert.Equal("undefined", engine.Evaluate("m.Method2").ToString());
}

[Fact]
public void ShouldOverrideMembers()
{
var engine = new Engine(options => options.SetMemberAccessor((e, target, member) =>
{
if (target is HiddenMembers && member == nameof(HiddenMembers.Member1))
{
return "Orange";
}
return null;
}));

engine.SetValue("m", new HiddenMembers());

Assert.Equal("Orange", engine.Evaluate("m.Member1").ToString());
}

[Fact]
public void SettingValueViaIntegerIndexer()
{
Expand Down
2 changes: 1 addition & 1 deletion Jint/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public partial class Engine
internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerConfigurable;
internal readonly PropertyDescriptor _callerCalleeArgumentsThrowerNonConfigurable;

internal static Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor> ReflectionAccessors = new();
internal Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor> ReflectionAccessors = new();

internal readonly JintCallStack CallStack;

Expand Down
12 changes: 12 additions & 0 deletions Jint/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public sealed class Options
private readonly List<IObjectConverter> _objectConverters = new();
private Func<Engine, object, ObjectInstance> _wrapObjectHandler;
private MemberAccessorDelegate _memberAccessor;
private MemberFilter _memberFilter = (_, _) => true;
private int _maxRecursionDepth = -1;
private TimeSpan _regexTimeoutInterval = TimeSpan.FromSeconds(10);
private CultureInfo _culture = CultureInfo.CurrentCulture;
Expand Down Expand Up @@ -195,6 +196,16 @@ public Options SetMemberAccessor(MemberAccessorDelegate accessor)
return this;
}

/// <summary>
/// Registers a filter that determines whether given member is wrapped to interop or returned as undefined.
/// </summary>
/// <param name="filter">The filter to use, if filter returns false, member is skipped.</param>
public Options SetMemberFilter(MemberFilter filter)
{
_memberFilter = filter;
return this;
}

/// <summary>
/// Allows scripts to call CLR types directly like <example>System.IO.File</example>
/// </summary>
Expand Down Expand Up @@ -366,6 +377,7 @@ internal void Apply(Engine engine)

internal Func<Engine, object, ObjectInstance> _WrapObjectHandler => _wrapObjectHandler;
internal MemberAccessorDelegate _MemberAccessor => _memberAccessor;
internal MemberFilter _MemberFilter => _memberFilter;

internal int MaxRecursionDepth => _maxRecursionDepth;

Expand Down
44 changes: 40 additions & 4 deletions Jint/Runtime/Interop/ObjectWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ private static ReflectionAccessor GetAccessor(Engine engine, Type type, string m
{
var key = new ClrPropertyDescriptorFactoriesKey(type, member);

var factories = Engine.ReflectionAccessors;
var factories = engine.ReflectionAccessors;
if (factories.TryGetValue(key, out var accessor))
{
return accessor;
Expand All @@ -270,7 +270,7 @@ private static ReflectionAccessor GetAccessor(Engine engine, Type type, string m
accessor = accessorFactory?.Invoke() ?? ResolvePropertyDescriptorFactory(engine, type, member);

// racy, we don't care, worst case we'll catch up later
Interlocked.CompareExchange(ref Engine.ReflectionAccessors,
Interlocked.CompareExchange(ref engine.ReflectionAccessors,
new Dictionary<ClrPropertyDescriptorFactoriesKey, ReflectionAccessor>(factories)
{
[key] = accessor
Expand All @@ -279,15 +279,20 @@ private static ReflectionAccessor GetAccessor(Engine engine, Type type, string m
return accessor;
}

private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine, Type type, string memberName)
private static ReflectionAccessor ResolvePropertyDescriptorFactory(
Engine engine,
Type type,
string memberName)
{
var isNumber = uint.TryParse(memberName, out _);

var filter = engine.Options._MemberFilter;

// we can always check indexer if there's one, and then fall back to properties if indexer returns null
IndexerAccessor.TryFindIndexer(engine, type, memberName, out var indexerAccessor, out var indexer);

// properties and fields cannot be numbers
if (!isNumber && TryFindStringPropertyAccessor(type, memberName, indexer, out var temp))
if (!isNumber && TryFindStringPropertyAccessor(type, memberName, indexer, filter, out var temp))
{
return temp;
}
Expand All @@ -309,6 +314,11 @@ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine
{
foreach (var iprop in iface.GetProperties())
{
if (!filter(iprop, null))
{
continue;
}

if (iprop.Name == "Item" && iprop.GetIndexParameters().Length == 1)
{
// never take indexers, should use the actual indexer
Expand All @@ -334,6 +344,11 @@ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine
{
foreach (var imethod in iface.GetMethods())
{
if (!filter(imethod, null))
{
continue;
}

if (EqualsIgnoreCasing(imethod.Name, memberName))
{
explicitMethods ??= new List<MethodInfo>();
Expand Down Expand Up @@ -361,6 +376,11 @@ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine
var matches = new List<MethodInfo>();
foreach (var method in extensionMethods)
{
if (!filter(method, null))
{
continue;
}

if (EqualsIgnoreCasing(method.Name, memberName))
{
matches.Add(method);
Expand All @@ -379,12 +399,18 @@ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine
Type type,
string memberName,
PropertyInfo indexerToTry,
MemberFilter filter,
out ReflectionAccessor wrapper)
{
// look for a property, bit be wary of indexers, we don't want indexers which have name "Item" to take precedence
PropertyInfo property = null;
foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
{
if (!filter(p, null))
{
continue;
}

// only if it's not an indexer, we can do case-ignoring matches
var isStandardIndexer = p.GetIndexParameters().Length == 1 && p.Name == "Item";
if (!isStandardIndexer && EqualsIgnoreCasing(p.Name, memberName))
Expand All @@ -404,6 +430,11 @@ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine
FieldInfo field = null;
foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
{
if (!filter(f, null))
{
continue;
}

if (EqualsIgnoreCasing(f.Name, memberName))
{
field = f;
Expand All @@ -421,6 +452,11 @@ private static ReflectionAccessor ResolvePropertyDescriptorFactory(Engine engine
List<MethodInfo> methods = null;
foreach (var m in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
{
if (!filter(m, null))
{
continue;
}

if (EqualsIgnoreCasing(m.Name, memberName))
{
methods ??= new List<MethodInfo>();
Expand Down
9 changes: 8 additions & 1 deletion Jint/Runtime/Interop/Reflection/IndexerAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ IndexerAccessor ComposeIndexerFactory(PropertyInfo candidate, Type paramType)
return null;
}

var filter = engine.Options._MemberFilter;

// default indexer wins
if (typeof(IList).IsAssignableFrom(targetType))
if (typeof(IList).IsAssignableFrom(targetType) && filter(_iListIndexer, null))
{
indexerAccessor = ComposeIndexerFactory(_iListIndexer, typeof(int));
if (indexerAccessor != null)
Expand All @@ -74,6 +76,11 @@ IndexerAccessor ComposeIndexerFactory(PropertyInfo candidate, Type paramType)
// try to find first indexer having either public getter or setter with matching argument type
foreach (var candidate in targetType.GetProperties())
{
if (!filter(candidate, null))
{
continue;
}

var indexParameters = candidate.GetIndexParameters();
if (indexParameters.Length != 1)
{
Expand Down

0 comments on commit 00420a1

Please sign in to comment.