using System.Collections; using Jint.Native.Number; using Jint.Native.Object; using Jint.Native.String; using Jint.Runtime; using Jint.Runtime.Interop; namespace Jint.Native.Array; internal abstract class ArrayOperations : IEnumerable { protected internal const ulong MaxArrayLength = 4294967295; protected internal const ulong MaxArrayLikeLength = NumberConstructor.MaxSafeInteger; public static ArrayOperations For(Realm realm, JsValue value, bool forWrite) { if (!forWrite) { if (value.IsString()) { return new JsStringOperations(realm, (JsString) value); } if (value is StringInstance stringInstance) { return new JsStringOperations(realm, stringInstance); } if (value is JsArray { CanUseFastAccess: true } array && array.Length <= (array._dense?.Length ?? -1)) { return new ArrayReadOperations(array); } } return For(TypeConverter.ToObject(realm, value), forWrite); } public static ArrayOperations For(ObjectInstance instance, bool forWrite) { if (instance is JsArray { CanUseFastAccess: true } arrayInstance) { return new JsArrayOperations(arrayInstance); } if (instance is JsTypedArray typedArrayInstance) { return new JsTypedArrayOperations(typedArrayInstance); } if (instance is ArrayLikeWrapper arrayWrapper) { return new ArrayLikeOperations(arrayWrapper); } if (instance is ObjectWrapper wrapper) { var descriptor = wrapper._typeDescriptor; if (descriptor.IsArrayLike && wrapper.Target is ICollection) { return new IndexWrappedOperations(wrapper); } } return new ObjectOperations(instance); } public abstract ObjectInstance Target { get; } public abstract ulong GetSmallestIndex(ulong length); public abstract uint GetLength(); public abstract ulong GetLongLength(); public abstract void SetLength(ulong length); public abstract void EnsureCapacity(ulong capacity); public abstract JsValue Get(ulong index); public virtual JsValue[] GetAll( Types elementTypes = Types.Undefined | Types.Null | Types.Boolean | Types.String | Types.Symbol | Types.Number | Types.BigInt | Types.Object, bool skipHoles = false) { uint writeIndex = 0; var n = (int) GetLength(); var jsValues = new JsValue[n]; for (uint i = 0; i < (uint) jsValues.Length; i++) { var jsValue = skipHoles && !HasProperty(i) ? JsValue.Undefined : Get(i); if ((jsValue.Type & elementTypes) == Types.Empty) { Throw.TypeErrorNoEngine("invalid type"); } jsValues[writeIndex++] = jsValue; } return jsValues; } public abstract bool TryGetValue(ulong index, out JsValue value); public abstract bool HasProperty(ulong index); public abstract void CreateDataPropertyOrThrow(ulong index, JsValue value); public abstract void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true); public abstract void DeletePropertyOrThrow(ulong index); public ArrayLikeIterator GetEnumerator() => new ArrayLikeIterator(this); IEnumerator IEnumerable.GetEnumerator() => new ArrayLikeIterator(this); IEnumerator IEnumerable.GetEnumerator() => new ArrayLikeIterator(this); internal sealed class ArrayLikeIterator : IEnumerator { private readonly ArrayOperations _obj; private ulong _current; private bool _initialized; private readonly uint _length; public ArrayLikeIterator(ArrayOperations obj) { _obj = obj; _length = obj.GetLength(); Reset(); } public JsValue Current { get { return !_initialized ? JsValue.Undefined : _obj.TryGetValue(_current, out var temp) ? temp : JsValue.Undefined; } } object? IEnumerator.Current => Current; public void Dispose() { } public bool MoveNext() { if (!_initialized) { _initialized = true; } else { _current++; } return _current < _length; } public void Reset() { _initialized = false; _current = 0; } } private sealed class ObjectOperations : ArrayOperations { public ObjectOperations(ObjectInstance target) : base(target) { } private double GetIntegerLength() { var descValue = _target.Get(CommonProperties.Length); if (descValue is not null) { return TypeConverter.ToInteger(descValue); } return 0; } public override ulong GetSmallestIndex(ulong length) { return _target.GetSmallestIndex(length); } public override uint GetLength() { var integerLength = GetIntegerLength(); return (uint) (integerLength >= 0 ? integerLength : 0); } public override ulong GetLongLength() { var integerLength = GetIntegerLength(); if (integerLength <= 0) { return 0; } return (ulong) System.Math.Min(integerLength, MaxArrayLikeLength); } public override void SetLength(ulong length) => _target.Set(CommonProperties.Length, length, true); public override void EnsureCapacity(ulong capacity) { } public override JsValue Get(ulong index) => _target.Get(JsString.Create(index)); public override bool TryGetValue(ulong index, out JsValue value) { var propertyName = JsString.Create(index); var kPresent = _target.HasProperty(propertyName); value = kPresent ? _target.Get(propertyName) : JsValue.Undefined; return kPresent; } public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => _target.CreateDataPropertyOrThrow(JsString.Create(index), value); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) => _target.Set(JsString.Create(index), value, throwOnError); public override void DeletePropertyOrThrow(ulong index) => _target.DeletePropertyOrThrow(JsString.Create(index)); public override bool HasProperty(ulong index) => Target.HasProperty(index); } private sealed class JsArrayOperations : ArrayOperations { public JsArrayOperations(JsArray target) : base(target) { } public override ulong GetSmallestIndex(ulong length) => _target.GetSmallestIndex(); public override uint GetLength() => (uint) ((JsNumber) _target._length!._value!)._value; public override ulong GetLongLength() => (ulong) ((JsNumber) _target._length!._value!)._value; public override void SetLength(ulong length) => _target.SetLength(length); public override void EnsureCapacity(ulong capacity) => _target.EnsureCapacity((uint) capacity); public override bool TryGetValue(ulong index, out JsValue value) // array max size is uint => _target.TryGetValue((uint) index, out value); public override JsValue Get(ulong index) => _target.Get((uint) index); public override JsValue[] GetAll( Types elementTypes = Types.Empty | Types.Undefined | Types.Null | Types.Boolean | Types.String | Types.Number | Types.Symbol | Types.BigInt | Types.Object, bool skipHoles = false) { var n = _target.GetLength(); if (_target._dense == null || _target._dense.Length < n) { return base.GetAll(elementTypes, skipHoles); } // optimized uint writeIndex = 0; var jsValues = new JsValue[n]; for (uint i = 0; i < (uint) jsValues.Length; i++) { var value = _target._dense[i]; if (value is null) { value = _target.Prototype?.Get(i) ?? JsValue.Undefined; } if ((value.Type & elementTypes) == Types.Empty) { Throw.TypeErrorNoEngine("invalid type"); } jsValues[writeIndex++] = (JsValue?) value ?? JsValue.Undefined; } return jsValues; } public override void DeletePropertyOrThrow(ulong index) => _target.DeletePropertyOrThrow((uint) index); public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => _target.SetIndexValue((uint) index, value, updateLength: false); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) => _target.SetIndexValue((uint) index, value, updateLength); public override bool HasProperty(ulong index) => _target.HasProperty(index); } private sealed class JsTypedArrayOperations : ArrayOperations { private readonly JsTypedArray _target; public JsTypedArrayOperations(JsTypedArray target) { _target = target; } public override ObjectInstance Target => _target; public override ulong GetSmallestIndex(ulong length) => 0; public override uint GetLength() { if (!_target.IsConcatSpreadable) { return _target.GetLength(); } var descValue = _target.Get(CommonProperties.Length); if (descValue is not null) { return (uint) TypeConverter.ToInteger(descValue); } return 0; } public override ulong GetLongLength() => GetLength(); public override void SetLength(ulong length) { } public override void EnsureCapacity(ulong capacity) { } public override JsValue Get(ulong index) => _target[(int) index]; public override bool TryGetValue(ulong index, out JsValue value) { if (_target.IsValidIntegerIndex(index)) { value = _target[(int) index]; return true; } value = JsValue.Undefined; return false; } public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => _target.CreateDataPropertyOrThrow(index, value); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) => _target[(int) index] = value; public override void DeletePropertyOrThrow(ulong index) => _target.DeletePropertyOrThrow(index); public override bool HasProperty(ulong index) => _target.HasProperty(index); } private sealed class JsStringOperations : ArrayOperations { private readonly Realm _realm; private readonly JsString _target; private ObjectInstance? _wrappedTarget; public JsStringOperations(Realm realm, JsString target) { _realm = realm; _target = target; } public JsStringOperations(Realm realm, StringInstance stringInstance) : this(realm, stringInstance.StringData) { _wrappedTarget = stringInstance; } public override ObjectInstance Target => _wrappedTarget ??= _realm.Intrinsics.String.Construct(_target); public override ulong GetSmallestIndex(ulong length) => 0; public override uint GetLength() => (uint) _target.Length; public override ulong GetLongLength() => GetLength(); public override void SetLength(ulong length) => throw new NotSupportedException(); public override void EnsureCapacity(ulong capacity) { } public override JsValue Get(ulong index) => index < (ulong) _target.Length ? _target[(int) index] : JsValue.Undefined; public override bool TryGetValue(ulong index, out JsValue value) { if (index < (ulong) _target.Length) { value = _target[(int) index]; return true; } value = JsValue.Undefined; return false; } public override bool HasProperty(ulong index) => index < (ulong) _target.Length; public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => throw new NotSupportedException(); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) => throw new NotSupportedException(); public override void DeletePropertyOrThrow(ulong index) => throw new NotSupportedException(); } private sealed class ArrayReadOperations : ArrayOperations { private readonly JsArray _target; private readonly JsValue?[] _data; private readonly uint _length; public ArrayReadOperations(JsArray target) { _target = target; _data = target._dense ?? []; _length = target.Length; } public override ObjectInstance Target => _target; public override ulong GetSmallestIndex(ulong length) => 0; public override uint GetLength() => _length; public override ulong GetLongLength() => _length; public override void SetLength(ulong length) => throw new NotSupportedException(); public override void EnsureCapacity(ulong capacity) { } public override JsValue Get(ulong index) => (index < (ulong) _data.Length ? _data[(int) index] : JsValue.Undefined) ?? JsValue.Undefined; public override bool TryGetValue(ulong index, out JsValue value) { if (index < _length) { value = _data[(int) index]!; return value is not null; } value = JsValue.Undefined; return false; } public override bool HasProperty(ulong index) => index < _length && _data[index] is not null; public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => throw new NotSupportedException(); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) => throw new NotSupportedException(); public override void DeletePropertyOrThrow(ulong index) => throw new NotSupportedException(); } private sealed class IndexWrappedOperations : ArrayOperations { private readonly ObjectWrapper _target; private readonly ICollection _collection; private readonly IList _list; public IndexWrappedOperations(ObjectWrapper wrapper) { _target = wrapper; _collection = (ICollection) wrapper.Target; _list = (IList) wrapper.Target; } public override ObjectInstance Target => _target; public override ulong GetSmallestIndex(ulong length) => 0; public override uint GetLength() => (uint) _collection.Count; public override ulong GetLongLength() => GetLength(); public override void SetLength(ulong length) { if (_list == null) { throw new NotSupportedException(); } while (_list.Count > (int) length) { // shrink list to fit _list.RemoveAt(_list.Count - 1); } while (_list.Count < (int) length) { // expand list to fit _list.Add(null); } } public override void EnsureCapacity(ulong capacity) { } public override JsValue Get(ulong index) => index < (ulong) _collection.Count ? ReadValue((int) index) : JsValue.Undefined; public override bool TryGetValue(ulong index, out JsValue value) { if (index < (ulong) _collection.Count) { value = ReadValue((int) index); return true; } value = JsValue.Undefined; return false; } private JsValue ReadValue(int index) { if (_list is not null) { return (uint) index < _list.Count ? JsValue.FromObject(_target.Engine, _list[index]) : JsValue.Undefined; } // via reflection is slow, but better than nothing return JsValue.FromObject(_target.Engine, _target._typeDescriptor.IntegerIndexerProperty!.GetValue(Target, [index])); } public override bool HasProperty(ulong index) => index < (ulong) _collection.Count; public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => _target.CreateDataPropertyOrThrow(index, value); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) { if (updateLength && _list != null && index >= (ulong) _list.Count) { SetLength(index + 1); } _target[(int) index] = value; } public override void DeletePropertyOrThrow(ulong index) => _target.DeletePropertyOrThrow(index); } private sealed class ArrayLikeOperations : ArrayOperations { private readonly ArrayLikeWrapper _target; public ArrayLikeOperations(ArrayLikeWrapper wrapper) { _target = wrapper; } public override ObjectInstance Target => _target; public override ulong GetSmallestIndex(ulong length) => 0; public override uint GetLength() => (uint) _target.Length; public override ulong GetLongLength() => GetLength(); public override void SetLength(ulong length) { while (_target.Length > (int) length) { // shrink list to fit _target.RemoveAt(_target.Length - 1); } while (_target.Length < (int) length) { // expand list to fit _target.AddDefault(); } } public override void EnsureCapacity(ulong capacity) { _target.EnsureCapacity((int) capacity); } public override JsValue Get(ulong index) => index < (ulong) _target.Length ? ReadValue((int) index) : JsValue.Undefined; public override bool TryGetValue(ulong index, out JsValue value) { if (index < (ulong) _target.Length) { value = ReadValue((int) index); return true; } value = JsValue.Undefined; return false; } private JsValue ReadValue(int index) { return (uint) index < _target.Length ? JsValue.FromObject(_target.Engine, _target.GetAt(index)) : JsValue.Undefined; } public override bool HasProperty(ulong index) => index < (ulong) _target.Length; public override void CreateDataPropertyOrThrow(ulong index, JsValue value) => _target.CreateDataPropertyOrThrow(index, value); public override void Set(ulong index, JsValue value, bool updateLength = false, bool throwOnError = true) { _target.SetAt((int) index, value); } public override void DeletePropertyOrThrow(ulong index) => _target.DeletePropertyOrThrow(index); } } /// /// Adapter to use optimized array operations when possible. /// Gaps the difference between ArgumentsInstance and ArrayInstance. /// internal abstract class ArrayOperations : ArrayOperations where T : ObjectInstance { protected readonly T _target; protected ArrayOperations(T target) { _target = target; } public override ObjectInstance Target => _target; }