| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2fe63b5 commit e08a730
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,161 @@ | |||
| 1 | + using System; | ||
| 2 | + using System.Collections.Generic; | ||
| 3 | + | ||
| 4 | + using NUnit.Framework; | ||
| 5 | + | ||
| 6 | + using Python.Runtime; | ||
| 7 | + | ||
| 8 | + namespace Python.EmbeddingTest | ||
| 9 | + { | ||
| 10 | + public class TestClrTypeFilter | ||
| 11 | + { | ||
| 12 | + static readonly string RefusedName = typeof(FilterRefusedType).FullName!; | ||
| 13 | + | ||
| 14 | + readonly RefuseOneType _filter = new(typeof(FilterRefusedType)); | ||
| 15 | + | ||
| 16 | + // The engine is already running, so the filter is added to the live | ||
| 17 | + // configuration, as Inheritance does with PythonBaseTypeProviders. That | ||
| 18 | + // is safe here because nothing else ever reflects FilterRefusedType, so | ||
| 19 | + // it cannot already be cached | ||
| 20 | + [OneTimeSetUp] | ||
| 21 | + public void SetUp() => PythonEngine.InteropConfiguration.ClrTypeFilters.Add(_filter); | ||
| 22 | + | ||
| 23 | + [OneTimeTearDown] | ||
| 24 | + public void TearDown() => PythonEngine.InteropConfiguration.ClrTypeFilters.Remove(_filter); | ||
| 25 | + | ||
| 26 | + [TestCase("host.Method()", TestName = "MethodReturn")] | ||
| 27 | + [TestCase("host.OutParam(None)", TestName = "OutParameterAndReturn")] | ||
| 28 | + [TestCase("host.OutOnly(None)", TestName = "OutParameterOnly")] | ||
| 29 | + [TestCase("list(host.Iterate())", TestName = "IteratorItem")] | ||
| 30 | + [TestCase("host.Property", TestName = "InstanceProperty")] | ||
| 31 | + [TestCase("type(host).StaticProperty", TestName = "StaticProperty")] | ||
| 32 | + [TestCase("host.Field", TestName = "InstanceField")] | ||
| 33 | + [TestCase("type(host).StaticField", TestName = "StaticField")] | ||
| 34 | + [TestCase("host.Array1[0]", TestName = "ArrayItem")] | ||
| 35 | + [TestCase("host.Array2[0, 0]", TestName = "MultidimensionalArrayItem")] | ||
| 36 | + [TestCase("host[0]", TestName = "IndexerResult")] | ||
| 37 | + [TestCase("host.AsInterface().__implementation__", TestName = "InterfaceImplementation")] | ||
| 38 | + [TestCase("host.AsInterface().__raw_implementation__", TestName = "InterfaceRawImplementation")] | ||
| 39 | + public void RefusedTypeRaisesCatchableTypeError(string expression) | ||
| 40 | + { | ||
| 41 | + using var scope = Py.CreateScope(); | ||
| 42 | + scope.Set("host", new FilterHost()); | ||
| 43 | + scope.Exec($@" | ||
| 44 | + def attempt(): | ||
| 45 | + try: | ||
| 46 | + {expression} | ||
| 47 | + except TypeError as e: | ||
| 48 | + return str(e) | ||
| 49 | + return 'nothing raised' | ||
| 50 | + "); | ||
| 51 | + Assert.That(scope.Eval<string>("attempt()"), Does.Contain(RefusedName)); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + [Test] | ||
| 55 | + public void PermittedTypesAreUnaffected() | ||
| 56 | + { | ||
| 57 | + using var scope = Py.CreateScope(); | ||
| 58 | + scope.Set("host", new FilterHost()); | ||
| 59 | + Assert.That(scope.Eval<int>("host.Permitted().Value"), Is.EqualTo(42)); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + [Test] | ||
| 63 | + public void UncaughtRefusalReachesTheCallerAsTypeError() | ||
| 64 | + { | ||
| 65 | + using var scope = Py.CreateScope(); | ||
| 66 | + scope.Set("host", new FilterHost()); | ||
| 67 | + | ||
| 68 | + var error = Assert.Throws<PythonException>(() => scope.Exec("host.Method()")); | ||
| 69 | + Assert.That(error.Type.Name, Is.EqualTo("TypeError")); | ||
| 70 | + Assert.That(error.Message, Does.Contain(RefusedName)); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + [Test] | ||
| 74 | + public void RefusedTypeIsCheckedOnEveryAttempt() | ||
| 75 | + { | ||
| 76 | + var counter = new CountingFilter(typeof(FilterRefusedType)); | ||
| 77 | + var filters = PythonEngine.InteropConfiguration.ClrTypeFilters; | ||
| 78 | + filters.Insert(0, counter); | ||
| 79 | + try | ||
| 80 | + { | ||
| 81 | + using var scope = Py.CreateScope(); | ||
| 82 | + scope.Set("host", new FilterHost()); | ||
| 83 | + for (var i = 0; i < 2; i++) | ||
| 84 | + Assert.Throws<PythonException>(() => scope.Exec("host.Method()")); | ||
| 85 | + | ||
| 86 | + // A refusal is never cached, so the filters see the type again | ||
| 87 | + Assert.That(counter.Count, Is.EqualTo(2)); | ||
| 88 | + } | ||
| 89 | + finally | ||
| 90 | + { | ||
| 91 | + filters.Remove(counter); | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + [Test] | ||
| 96 | + public void NoFiltersByDefault() | ||
| 97 | + { | ||
| 98 | + using var configuration = InteropConfiguration.MakeDefault(); | ||
| 99 | + Assert.That(configuration.ClrTypeFilters, Is.Empty); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + [Test] | ||
| 103 | + public void TypeIsReflectedOnlyIfEveryFilterPermitsIt() | ||
| 104 | + { | ||
| 105 | + var group = new ClrTypeFilterGroup(); | ||
| 106 | + Assert.That(group.ShouldReflect(typeof(object)), Is.True, "an empty group permits everything"); | ||
| 107 | + | ||
| 108 | + group.Add(new RefuseOneType(typeof(string))); | ||
| 109 | + Assert.That(group.ShouldReflect(typeof(object)), Is.True); | ||
| 110 | + Assert.That(group.ShouldReflect(typeof(string)), Is.False); | ||
| 111 | + | ||
| 112 | + group.Add(new RefuseOneType(typeof(object))); | ||
| 113 | + Assert.That(group.ShouldReflect(typeof(object)), Is.False); | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + sealed class RefuseOneType(Type refused) : IClrTypeFilter | ||
| 117 | + { | ||
| 118 | + public bool ShouldReflect(Type type) => type != refused; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + sealed class CountingFilter(Type counted) : IClrTypeFilter | ||
| 122 | + { | ||
| 123 | + public int Count { get; private set; } | ||
| 124 | + | ||
| 125 | + public bool ShouldReflect(Type type) | ||
| 126 | + { | ||
| 127 | + if (type == counted) | ||
| 128 | + Count++; | ||
| 129 | + return true; | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + /// <summary>Refused by <see cref="TestClrTypeFilter"/>; reflected nowhere else.</summary> | ||
| 135 | + public class FilterRefusedType : IFilterTarget { } | ||
| 136 | + | ||
| 137 | + public interface IFilterTarget { } | ||
| 138 | + | ||
| 139 | + public class FilterPermittedType | ||
| 140 | + { | ||
| 141 | + public int Value => 42; | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /// <summary>Hands a refused type to Python through each boundary.</summary> | ||
| 145 | + public class FilterHost | ||
| 146 | + { | ||
| 147 | + public FilterRefusedType Method() => new(); | ||
| 148 | + public int OutParam(out FilterRefusedType value) { value = new(); return 1; } | ||
| 149 | + public void OutOnly(out FilterRefusedType value) => value = new(); | ||
| 150 | + public IEnumerable<FilterRefusedType> Iterate() { yield return new(); } | ||
| 151 | + public FilterRefusedType Property => new(); | ||
| 152 | + public static FilterRefusedType StaticProperty => new(); | ||
| 153 | + public FilterRefusedType Field = new(); | ||
| 154 | + public static FilterRefusedType StaticField = new(); | ||
| 155 | + public FilterRefusedType[] Array1 = { new() }; | ||
| 156 | + public FilterRefusedType[,] Array2 = { { new() } }; | ||
| 157 | + public FilterRefusedType this[int index] => new(); | ||
| 158 | + public IFilterTarget AsInterface() => new FilterRefusedType(); | ||
| 159 | + public FilterPermittedType Permitted() => new(); | ||
| 160 | + } | ||
| 161 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,25 @@ | |||
| 1 | + using System; | ||
| 2 | + using System.Collections.Generic; | ||
| 3 | + using System.Linq; | ||
| 4 | + | ||
| 5 | + namespace Python.Runtime | ||
| 6 | + { | ||
| 7 | + /// <summary> | ||
| 8 | + /// Composes the registered filters: a type is reflected only if every filter | ||
| 9 | + /// permits it. An empty group permits everything, which is the default. | ||
| 10 | + /// </summary> | ||
| 11 | + class ClrTypeFilterGroup : List<IClrTypeFilter>, IClrTypeFilter | ||
| 12 | + { | ||
| 13 | + public bool ShouldReflect(Type type) | ||
| 14 | + { | ||
| 15 | + if (type is null) | ||
| 16 | + throw new ArgumentNullException(nameof(type)); | ||
| 17 | + | ||
| 18 | + foreach (var filter in this) | ||
| 19 | + if (!filter.ShouldReflect(type)) | ||
| 20 | + return false; | ||
| 21 | + | ||
| 22 | + return true; | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,18 @@ | |||
| 1 | + using System; | ||
| 2 | + | ||
| 3 | + namespace Python.Runtime | ||
| 4 | + { | ||
| 5 | + /// <summary> | ||
| 6 | + /// Raised when a CLR type would be reflected into Python but an | ||
| 7 | + /// <see cref="IClrTypeFilter"/> registered on | ||
| 8 | + /// <see cref="InteropConfiguration.ClrTypeFilters"/> refused it. | ||
| 9 | + /// </summary> | ||
| 10 | + public class ClrTypeFilteredException : Exception | ||
| 11 | + { | ||
| 12 | + public Type FilteredType { get; } | ||
| 13 | + | ||
| 14 | + public ClrTypeFilteredException(Type type) | ||
| 15 | + : base($"Reflecting {type?.FullName ?? "<null>"} into Python was refused by an IClrTypeFilter.") | ||
| 16 | + => FilteredType = type!; | ||
| 17 | + } | ||
| 18 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -202,6 +202,14 @@ public static bool SetError(Exception e) | |||
| 202 | 202 | return true; | |
| 203 | 203 | } | |
| 204 | 204 | ||
| 205 | + // Raised as a plain TypeError: wrapping the CLR exception would mean | ||
| 206 | + // reflecting its type into Python, which the same filters may refuse | ||
| 207 | + if (e is ClrTypeFilteredException) | ||
| 208 | + { | ||
| 209 | + SetError(TypeError, e.Message); | ||
| 210 | + return true; | ||
| 211 | + } | ||
| 212 | + | ||
| 205 | 213 | using var instance = Converter.ToPython(e); | |
| 206 | 214 | if (instance.IsNull()) return false; | |
| 207 | 215 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,26 @@ | |||
| 1 | + using System; | ||
| 2 | + | ||
| 3 | + namespace Python.Runtime | ||
| 4 | + { | ||
| 5 | + /// <summary> | ||
| 6 | + /// Decides whether a CLR type may be reflected into Python. | ||
| 7 | + /// <para> | ||
| 8 | + /// Filters are consulted when a type would first be presented to Python. | ||
| 9 | + /// Permitted types are cached for the lifetime of the engine and not | ||
| 10 | + /// re-checked; a refused type is checked again on each attempt | ||
| 11 | + /// </para> | ||
| 12 | + /// <para> | ||
| 13 | + /// This is a policy hook for embedders, not a security boundary: Python code | ||
| 14 | + /// holding any reflected object can still reach whatever that object's own | ||
| 15 | + /// API exposes. | ||
| 16 | + /// </para> | ||
| 17 | + /// </summary> | ||
| 18 | + public interface IClrTypeFilter | ||
| 19 | + { | ||
| 20 | + /// <summary> | ||
| 21 | + /// Whether <paramref name="type"/> may be reflected into Python. Returning | ||
| 22 | + /// false causes an attempt to present the type to Python to fail. | ||
| 23 | + /// </summary> | ||
| 24 | + bool ShouldReflect(Type type); | ||
| 25 | + } | ||
| 26 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,15 @@ internal readonly PythonBaseTypeProviderGroup pythonBaseTypeProviders | |||
| 14 | 14 | /// <summary>Enables replacing base types of CLR types as seen from Python</summary> | |
| 15 | 15 | public IList<IPythonBaseTypeProvider> PythonBaseTypeProviders => this.pythonBaseTypeProviders; | |
| 16 | 16 | ||
| 17 | + internal readonly ClrTypeFilterGroup clrTypeFilters = new(); | ||
| 18 | + | ||
| 19 | + /// <summary> | ||
| 20 | + /// Restricts which CLR types are reflected into Python. Empty by default, | ||
| 21 | + /// so every type is reflected; a type is presented to Python only if every | ||
| 22 | + /// registered filter permits it. | ||
| 23 | + /// </summary> | ||
| 24 | + public IList<IClrTypeFilter> ClrTypeFilters => this.clrTypeFilters; | ||
| 25 | + | ||
| 17 | 26 | public static InteropConfiguration MakeDefault() | |
| 18 | 27 | { | |
| 19 | 28 | return new InteropConfiguration | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -106,13 +106,24 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k | |||
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | 108 | string? name = Runtime.GetManagedString(key); | |
| 109 | - if (name == "__implementation__") | ||
| 109 | + | ||
| 110 | + // Both expose the concrete object, whose type an IClrTypeFilter may | ||
| 111 | + // refuse - that must become a Python exception, not unwind through it | ||
| 112 | + try | ||
| 110 | 113 | { | |
| 111 | - return Converter.ToPython(clrObj.inst); | ||
| 114 | + if (name == "__implementation__") | ||
| 115 | + { | ||
| 116 | + return Converter.ToPython(clrObj.inst); | ||
| 117 | + } | ||
| 118 | + else if (name == "__raw_implementation__") | ||
| 119 | + { | ||
| 120 | + return CLRObject.GetReference(clrObj.inst); | ||
| 121 | + } | ||
| 112 | 122 | } | |
| 113 | - else if (name == "__raw_implementation__") | ||
| 123 | + catch (Exception e) | ||
| 114 | 124 | { | |
| 115 | - return CLRObject.GetReference(clrObj.inst); | ||
| 125 | + Exceptions.SetError(e); | ||
| 126 | + return default; | ||
| 116 | 127 | } | |
| 117 | 128 | ||
| 118 | 129 | return Runtime.PyObject_GenericGetAttr(ob, key); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -29,6 +29,12 @@ public static ReflectedClrType GetOrCreate(Type type) | |||
| 29 | 29 | if (ClassManager.cache.TryGetValue(type, out var pyType)) | |
| 30 | 30 | return pyType; | |
| 31 | 31 | ||
| 32 | + // Every CLR type reaching Python passes through here, so consulting the | ||
| 33 | + // filters once, on the cache miss, covers every route a type can take: | ||
| 34 | + // GetType(), an API returning System.Type, a field, a return value | ||
| 35 | + if (!PythonEngine.InteropConfiguration.clrTypeFilters.ShouldReflect(type)) | ||
| 36 | + throw new ClrTypeFilteredException(type); | ||
| 37 | + | ||
| 32 | 38 | // Shared with ClassManager.cache + TypeManager._slotsHolders writes | |
| 33 | 39 | // so the multi-step type build below is atomic. | |
| 34 | 40 | lock (ClassManager._cacheCreateLock) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments