using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using Jint.Native;
using Jint.Native.Iterator;
using Jint.Native.Object;
using Jint.Native.Symbol;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop.Reflection;
namespace Jint.Runtime.Interop
{
///
/// Wraps a CLR instance
///
public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable
{
private readonly TypeDescriptor _typeDescriptor;
public ObjectWrapper(Engine engine, object obj)
: base(engine)
{
Target = obj;
_typeDescriptor = TypeDescriptor.Get(obj.GetType());
if (_typeDescriptor.LengthProperty is not null)
{
// create a forwarder to produce length from Count or Length if one of them is present
var functionInstance = new ClrFunctionInstance(engine, "length", GetLength);
var descriptor = new GetSetPropertyDescriptor(functionInstance, Undefined, PropertyFlag.Configurable);
SetProperty(KnownKeys.Length, descriptor);
}
}
public object Target { get; }
public override bool IsArrayLike => _typeDescriptor.IsArrayLike;
internal override bool HasOriginalIterator => IsArrayLike;
internal override bool IsIntegerIndexedArray => _typeDescriptor.IsIntegerIndexedArray;
public override bool Set(JsValue property, JsValue value, JsValue receiver)
{
// check if we can take shortcuts for empty object, no need to generate properties
if (property is JsString stringKey)
{
var member = stringKey.ToString();
if (_properties is null || !_properties.ContainsKey(member))
{
// can try utilize fast path
var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member);
// CanPut logic
if (!accessor.Writable || !_engine.Options.Interop.AllowWrite)
{
return false;
}
accessor.SetValue(_engine, Target, value);
return true;
}
}
return SetSlow(property, value);
}
private bool SetSlow(JsValue property, JsValue value)
{
if (!CanPut(property))
{
return false;
}
var ownDesc = GetOwnProperty(property);
if (ownDesc == null)
{
return false;
}
ownDesc.Value = value;
return true;
}
public override JsValue Get(JsValue property, JsValue receiver)
{
if (property.IsInteger() && Target is IList list)
{
var index = (int) ((JsNumber) property)._value;
return (uint) index < list.Count ? FromObject(_engine, list[index]) : Undefined;
}
if (property.IsSymbol() && property != GlobalSymbolRegistry.Iterator)
{
// wrapped objects cannot have symbol properties
return Undefined;
}
if (property is JsString stringKey)
{
var member = stringKey.ToString();
// expando object for instance
if (Target is IDictionary stringKeyedDictionary)
{
if (stringKeyedDictionary.TryGetValue(member, out var value))
{
return FromObject(_engine, value);
}
}
var result = Engine.Options.Interop.MemberAccessor?.Invoke(Engine, Target, member);
if (result is not null)
{
return result;
}
if (_properties is null || !_properties.ContainsKey(member))
{
// can try utilize fast path
var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member);
var value = accessor.GetValue(_engine, Target);
if (value is not null)
{
return FromObject(_engine, value);
}
}
}
return base.Get(property, receiver);
}
public override List GetOwnPropertyKeys(Types types = Types.None | Types.String | Types.Symbol)
{
return new List(EnumerateOwnPropertyKeys(types));
}
public override IEnumerable> GetOwnProperties()
{
foreach (var key in EnumerateOwnPropertyKeys(Types.String | Types.Symbol))
{
yield return new KeyValuePair(key, GetOwnProperty(key));
}
}
private IEnumerable EnumerateOwnPropertyKeys(Types types)
{
var basePropertyKeys = base.GetOwnPropertyKeys(types);
// prefer object order, add possible other properties after
var processed = basePropertyKeys.Count > 0 ? new HashSet() : null;
var includeStrings = (types & Types.String) != 0;
if (includeStrings && Target is IDictionary stringKeyedDictionary) // expando object for instance
{
foreach (var key in stringKeyedDictionary.Keys)
{
var jsString = JsString.Create(key);
processed?.Add(jsString);
yield return jsString;
}
}
else if (includeStrings && Target is IDictionary dictionary)
{
// we take values exposed as dictionary keys only
foreach (var key in dictionary.Keys)
{
if (_engine.ClrTypeConverter.TryConvert(key, typeof(string), CultureInfo.InvariantCulture, out var stringKey))
{
var jsString = JsString.Create((string) stringKey);
processed?.Add(jsString);
yield return jsString;
}
}
}
else if (includeStrings)
{
// we take public properties and fields
var type = Target.GetType();
foreach (var p in type.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
{
var indexParameters = p.GetIndexParameters();
if (indexParameters.Length == 0)
{
var jsString = JsString.Create(p.Name);
processed?.Add(jsString);
yield return jsString;
}
}
foreach (var f in type.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public))
{
var jsString = JsString.Create(f.Name);
processed?.Add(jsString);
yield return jsString;
}
}
if (processed != null)
{
// we have base keys
for (var i = 0; i < basePropertyKeys.Count; i++)
{
var key = basePropertyKeys[i];
if (processed.Add(key))
{
yield return key;
}
}
}
}
public override PropertyDescriptor GetOwnProperty(JsValue property)
{
if (TryGetProperty(property, out var x))
{
return x;
}
// if we have array-like or dictionary or expando, we can provide iterator
if (property.IsSymbol() && property == GlobalSymbolRegistry.Iterator && _typeDescriptor.Iterable)
{
var iteratorFunction = new ClrFunctionInstance(
Engine,
"iterator",
Iterator,
1,
PropertyFlag.Configurable);
var iteratorProperty = new PropertyDescriptor(iteratorFunction, PropertyFlag.Configurable | PropertyFlag.Writable);
SetProperty(GlobalSymbolRegistry.Iterator, iteratorProperty);
return iteratorProperty;
}
var member = property.ToString();
var result = Engine.Options.Interop.MemberAccessor(Engine, Target, member);
if (result is not null)
{
return new PropertyDescriptor(result, PropertyFlag.OnlyEnumerable);
}
var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member);
var descriptor = accessor.CreatePropertyDescriptor(_engine, Target);
SetProperty(member, descriptor);
return descriptor;
}
// need to be public for advanced cases like RavenDB yielding properties from CLR objects
public static PropertyDescriptor GetPropertyDescriptor(Engine engine, object target, MemberInfo member)
{
// fast path which uses slow search if not found for some reason
ReflectionAccessor Factory()
{
return member switch
{
PropertyInfo pi => new PropertyAccessor(pi.Name, pi),
MethodBase mb => new MethodAccessor(MethodDescriptor.Build(new[] {mb})),
FieldInfo fi => new FieldAccessor(fi),
_ => null
};
}
return engine.Options.Interop.TypeResolver.GetAccessor(engine, target.GetType(), member.Name, Factory).CreatePropertyDescriptor(engine, target);
}
private static JsValue Iterator(JsValue thisObj, JsValue[] arguments)
{
var wrapper = (ObjectWrapper) thisObj;
return wrapper._typeDescriptor.IsDictionary
? new DictionaryIterator(wrapper._engine, wrapper)
: new EnumerableIterator(wrapper._engine, (IEnumerable) wrapper.Target);
}
private static JsValue GetLength(JsValue thisObj, JsValue[] arguments)
{
var wrapper = (ObjectWrapper) thisObj;
return JsNumber.Create((int) wrapper._typeDescriptor.LengthProperty.GetValue(wrapper.Target));
}
public override bool Equals(JsValue obj)
{
return Equals(obj as ObjectWrapper);
}
public override bool Equals(object obj)
{
return Equals(obj as ObjectWrapper);
}
public bool Equals(ObjectWrapper other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return Equals(Target, other.Target);
}
public override int GetHashCode()
{
return Target?.GetHashCode() ?? 0;
}
private sealed class DictionaryIterator : IteratorInstance
{
private readonly ObjectWrapper _target;
private readonly IEnumerator _enumerator;
public DictionaryIterator(Engine engine, ObjectWrapper target) : base(engine)
{
_target = target;
_enumerator = target.EnumerateOwnPropertyKeys(Types.String).GetEnumerator();
}
public override bool TryIteratorStep(out ObjectInstance nextItem)
{
if (_enumerator.MoveNext())
{
var key = _enumerator.Current;
var value = _target.Get(key);
nextItem = new KeyValueIteratorPosition(_engine, key, value);
return true;
}
nextItem = KeyValueIteratorPosition.Done(_engine);
return false;
}
}
private sealed class EnumerableIterator : IteratorInstance
{
private readonly IEnumerator _enumerator;
public EnumerableIterator(Engine engine, IEnumerable target) : base(engine)
{
_enumerator = target.GetEnumerator();
}
public override bool TryIteratorStep(out ObjectInstance nextItem)
{
if (_enumerator.MoveNext())
{
var value = _enumerator.Current;
nextItem = new ValueIteratorPosition(_engine, FromObject(_engine, value));
return true;
}
nextItem = KeyValueIteratorPosition.Done(_engine);
return false;
}
}
}
}