using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using Jint.Native; using Jint.Runtime.Descriptors; using Jint.Runtime.Descriptors.Specialized; #pragma warning disable IL2067 namespace Jint.Runtime.Interop.Reflection; internal sealed class IndexerAccessor : ReflectionAccessor { private readonly object _key; private readonly MethodInfo? _getter; private readonly MethodInfo? _setter; private readonly MethodInfo? _containsKey; private IndexerAccessor(PropertyInfo indexer, MethodInfo? containsKey, object key) : base(indexer.PropertyType) { Indexer = indexer; FirstIndexParameter = indexer.GetIndexParameters()[0]; _containsKey = containsKey; _key = key; _getter = indexer.GetGetMethod(); _setter = indexer.GetSetMethod(); } internal PropertyInfo Indexer { get; } internal ParameterInfo FirstIndexParameter { get; } internal static bool TryFindIndexer( Engine engine, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType, string propertyName, [NotNullWhen(true)] out IndexerAccessor? indexerAccessor, [NotNullWhen(true)] out PropertyInfo? indexer) { indexerAccessor = null; indexer = null; var paramTypeArray = new Type[1]; // integer keys can be ambiguous as we only know string keys int? integerKey = null; if (int.TryParse(propertyName, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intKeyTemp)) { integerKey = intKeyTemp; } var filter = new Func(m => engine.Options.Interop.TypeResolver.Filter(engine, targetType, m)); // default indexer wins var descriptor = TypeDescriptor.Get(targetType); if (descriptor.IntegerIndexerProperty is not null && !filter(descriptor.IntegerIndexerProperty)) { indexerAccessor = ComposeIndexerFactory(engine, targetType, descriptor.IntegerIndexerProperty, paramType: typeof(int), propertyName, integerKey, paramTypeArray); if (indexerAccessor != null) { indexer = descriptor.IntegerIndexerProperty; return true; } } // try to find first indexer having either public getter or setter with matching argument type PropertyInfo? fallbackIndexer = null; foreach (var candidate in targetType.GetProperties()) { if (!filter(candidate)) { continue; } var indexParameters = candidate.GetIndexParameters(); if (indexParameters.Length != 1) { continue; } if (candidate.GetGetMethod() != null || candidate.GetSetMethod() != null) { var paramType = indexParameters[0].ParameterType; indexerAccessor = ComposeIndexerFactory(engine, targetType, candidate, paramType, propertyName, integerKey, paramTypeArray); if (indexerAccessor != null) { if (paramType != typeof(string) || integerKey is null) { // exact match, we don't need to check for integer key indexer = candidate; return true; } if (fallbackIndexer is null) { // our fallback fallbackIndexer = candidate; } } } } if (fallbackIndexer is not null) { indexer = fallbackIndexer; // just to keep compiler happy, we know we have a value indexerAccessor ??= new IndexerAccessor(indexer, containsKey: null, key: null!); return true; } indexerAccessor = default; indexer = default; return false; } private static IndexerAccessor? ComposeIndexerFactory( Engine engine, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType, PropertyInfo candidate, Type paramType, string propertyName, int? integerKey, Type[] paramTypeArray) { // check for known incompatible types #if NET8_0_OR_GREATER if (typeof(System.Text.Json.Nodes.JsonNode).IsAssignableFrom(targetType) && (targetType != typeof(System.Text.Json.Nodes.JsonArray) || paramType != typeof(int)) && (targetType != typeof(System.Text.Json.Nodes.JsonObject) || paramType != typeof(string))) { // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw // we cannot access this[int] with anything else than JsonArray, otherwise itw will throw return null; } #endif object? key = null; // int key is quite common case if (paramType == typeof(int)) { if (integerKey is not null) { key = integerKey; } } else { engine.TypeConverter.TryConvert(propertyName, paramType, CultureInfo.InvariantCulture, out key); } if (key is not null) { // the key can be converted for this indexer var indexerProperty = candidate; // get contains key method to avoid index exception being thrown in dictionaries paramTypeArray[0] = paramType; var containsKeyMethod = targetType.GetMethod(nameof(IDictionary.ContainsKey), paramTypeArray); if (containsKeyMethod is null && targetType.IsAssignableFrom(typeof(IDictionary))) { paramTypeArray[0] = typeof(object); containsKeyMethod = targetType.GetMethod(nameof(IDictionary.Contains), paramTypeArray); } return new IndexerAccessor(indexerProperty, containsKeyMethod, key); } // the key type doesn't work for this indexer return null; } public override bool Readable => Indexer.CanRead; public override bool Writable => Indexer.CanWrite; protected override object? DoGetValue(object target, string memberName) { if (_getter is null) { ExceptionHelper.ThrowInvalidOperationException("Indexer has no public getter."); return null; } object[] parameters = [_key]; if (_containsKey != null) { if (_containsKey.Invoke(target, parameters) as bool? != true) { return JsValue.Undefined; } } try { return _getter.Invoke(target, parameters); } catch (TargetInvocationException tie) when (tie.InnerException is KeyNotFoundException) { return JsValue.Undefined; } } protected override void DoSetValue(object target, string memberName, object? value) { if (_setter is null) { ExceptionHelper.ThrowInvalidOperationException("Indexer has no public setter."); } object?[] parameters = { _key, value }; _setter.Invoke(target, parameters); } public override PropertyDescriptor CreatePropertyDescriptor(Engine engine, object target, string memberName, bool enumerable = true) { if (_containsKey != null) { if (_containsKey.Invoke(target, [_key]) as bool? != true) { return PropertyDescriptor.Undefined; } } return new ReflectionDescriptor(engine, this, target, memberName, enumerable: true); } }