using Jint.Native.Function; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Descriptors; namespace Jint.Native; internal sealed class JsProxy : ObjectInstance, IConstructor, ICallable { internal ObjectInstance _target; internal ObjectInstance? _handler; private static readonly JsString TrapApply = new JsString("apply"); private static readonly JsString TrapGet = new JsString("get"); private static readonly JsString TrapSet = new JsString("set"); private static readonly JsString TrapPreventExtensions = new JsString("preventExtensions"); private static readonly JsString TrapIsExtensible = new JsString("isExtensible"); private static readonly JsString TrapDefineProperty = new JsString("defineProperty"); private static readonly JsString TrapDeleteProperty = new JsString("deleteProperty"); private static readonly JsString TrapGetOwnPropertyDescriptor = new JsString("getOwnPropertyDescriptor"); private static readonly JsString TrapHas = new JsString("has"); private static readonly JsString TrapGetProtoTypeOf = new JsString("getPrototypeOf"); private static readonly JsString TrapSetProtoTypeOf = new JsString("setPrototypeOf"); private static readonly JsString TrapOwnKeys = new JsString("ownKeys"); private static readonly JsString TrapConstruct = new JsString("construct"); private static readonly JsString KeyFunctionRevoke = new JsString("revoke"); private static readonly JsString KeyIsArray = new JsString("isArray"); public JsProxy( Engine engine, ObjectInstance target, ObjectInstance handler) : base(engine, target.Class) { _target = target; _handler = handler; IsCallable = target.IsCallable; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-call-thisargument-argumentslist /// JsValue ICallable.Call(JsValue thisObject, params JsCallArguments arguments) { if (_target is not ICallable) { Throw.TypeError(_engine.Realm, "(intermediate value) is not a function"); } var jsValues = new[] { _target, thisObject, _engine.Realm.Intrinsics.Array.ConstructFast(arguments) }; if (TryCallHandler(TrapApply, jsValues, out var result)) { return result; } var callable = _target as ICallable; if (callable is null) { Throw.TypeError(_engine.Realm, _target + " is not a function"); } return callable.Call(thisObject, arguments); } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-construct-argumentslist-newtarget /// ObjectInstance IConstructor.Construct(JsCallArguments arguments, JsValue newTarget) { if (_target is not ICallable) { Throw.TypeError(_engine.Realm, "(intermediate value) is not a constructor"); } var argArray = _engine.Realm.Intrinsics.Array.Construct(arguments, _engine.Realm.Intrinsics.Array); if (!TryCallHandler(TrapConstruct, [_target, argArray, newTarget], out var result)) { var constructor = _target as IConstructor; if (constructor is null) { Throw.TypeError(_engine.Realm); } return constructor.Construct(arguments, newTarget); } var oi = result as ObjectInstance; if (oi is null) { Throw.TypeError(_engine.Realm); } return oi; } internal override bool IsArray() { AssertNotRevoked(KeyIsArray); return _target.IsArray(); } public override object ToObject() => _target.ToObject(); internal override bool IsConstructor { get { if (_target is not null && _target.IsConstructor) { return true; } if (_handler is not null && _handler.TryGetValue(TrapConstruct, out var handlerFunction) && handlerFunction is IConstructor) { return true; } return false; } } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver /// public override JsValue Get(JsValue property, JsValue receiver) { AssertTargetNotRevoked(property); var target = _target; if (KeyFunctionRevoke.Equals(property) || !TryCallHandler(TrapGet, [target, TypeConverter.ToPropertyKey(property), receiver], out var result)) { return target.Get(property, receiver); } var targetDesc = target.GetOwnProperty(property); if (targetDesc != PropertyDescriptor.Undefined) { if (targetDesc.IsDataDescriptor()) { var targetValue = targetDesc.Value; if (!targetDesc.Configurable && !targetDesc.Writable && !SameValue(result, targetValue)) { Throw.TypeError(_engine.Realm, $"'get' on proxy: property '{property}' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '{targetValue}' but got '{result}')"); } } if (targetDesc.IsAccessorDescriptor()) { if (!targetDesc.Configurable && (targetDesc.Get ?? Undefined).IsUndefined() && !result.IsUndefined()) { Throw.TypeError(_engine.Realm, $"'get' on proxy: property '{property}' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '{result}')"); } } } return result; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-ownpropertykeys /// public override List GetOwnPropertyKeys(Types types = Types.Empty | Types.String | Types.Symbol) { if (!TryCallHandler(TrapOwnKeys, [_target], out var result)) { return _target.GetOwnPropertyKeys(types); } var trapResult = new List(FunctionPrototype.CreateListFromArrayLike(_engine.Realm, result, Types.String | Types.Symbol)); if (trapResult.Count != new HashSet(trapResult).Count) { Throw.TypeError(_engine.Realm); } var extensibleTarget = _target.Extensible; var targetKeys = _target.GetOwnPropertyKeys(); var targetConfigurableKeys = new List(); var targetNonconfigurableKeys = new List(); foreach (var property in targetKeys) { var desc = _target.GetOwnProperty(property); if (desc != PropertyDescriptor.Undefined && !desc.Configurable) { targetNonconfigurableKeys.Add(property); } else { targetConfigurableKeys.Add(property); } } var uncheckedResultKeys = new HashSet(trapResult); for (var i = 0; i < targetNonconfigurableKeys.Count; i++) { var key = targetNonconfigurableKeys[i]; if (!uncheckedResultKeys.Remove(key)) { Throw.TypeError(_engine.Realm); } } if (extensibleTarget) { return trapResult; } for (var i = 0; i < targetConfigurableKeys.Count; i++) { var key = targetConfigurableKeys[i]; if (!uncheckedResultKeys.Remove(key)) { Throw.TypeError(_engine.Realm); } } if (uncheckedResultKeys.Count > 0) { Throw.TypeError(_engine.Realm); } return trapResult; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getownproperty-p /// public override PropertyDescriptor GetOwnProperty(JsValue property) { if (!TryCallHandler(TrapGetOwnPropertyDescriptor, [_target, TypeConverter.ToPropertyKey(property)], out var trapResultObj)) { return _target.GetOwnProperty(property); } if (!trapResultObj.IsObject() && !trapResultObj.IsUndefined()) { Throw.TypeError(_engine.Realm); } var targetDesc = _target.GetOwnProperty(property); if (trapResultObj.IsUndefined()) { if (targetDesc == PropertyDescriptor.Undefined) { return targetDesc; } if (!targetDesc.Configurable || !_target.Extensible) { Throw.TypeError(_engine.Realm); } return PropertyDescriptor.Undefined; } var extensibleTarget = _target.Extensible; var resultDesc = PropertyDescriptor.ToPropertyDescriptor(_engine.Realm, trapResultObj); CompletePropertyDescriptor(resultDesc); var valid = IsCompatiblePropertyDescriptor(extensibleTarget, resultDesc, targetDesc); if (!valid) { Throw.TypeError(_engine.Realm); } if (!resultDesc.Configurable) { if (targetDesc == PropertyDescriptor.Undefined || targetDesc.Configurable) { Throw.TypeError(_engine.Realm); } if (resultDesc.WritableSet && !resultDesc.Writable) { if (targetDesc.Writable) { Throw.TypeError(_engine.Realm); } } } return resultDesc; } /// /// https://tc39.es/ecma262/#sec-completepropertydescriptor /// private static void CompletePropertyDescriptor(PropertyDescriptor desc) { if (desc.IsGenericDescriptor() || desc.IsDataDescriptor()) { desc.Value ??= Undefined; if (!desc.WritableSet) { desc.Writable = false; } } else { var getSet = (GetSetPropertyDescriptor) desc; getSet.SetGet(getSet.Get ?? Undefined); getSet.SetSet(getSet.Set ?? Undefined); } if (!desc.EnumerableSet) { desc.Enumerable = false; } if (!desc.ConfigurableSet) { desc.Configurable = false; } } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver /// public override bool Set(JsValue property, JsValue value, JsValue receiver) { if (!TryCallHandler(TrapSet, [_target, TypeConverter.ToPropertyKey(property), value, receiver], out var trapResult)) { return _target.Set(property, value, receiver); } var result = TypeConverter.ToBoolean(trapResult); if (!result) { return false; } var targetDesc = _target.GetOwnProperty(property); if (targetDesc != PropertyDescriptor.Undefined) { if (targetDesc.IsDataDescriptor() && !targetDesc.Configurable && !targetDesc.Writable) { var targetValue = targetDesc.Value; if (!SameValue(targetValue, value)) { Throw.TypeError(_engine.Realm, $"'set' on proxy: trap returned truish for property '{property}' which exists in the proxy target as a non-configurable and non-writable data property with a different value"); } } if (targetDesc.IsAccessorDescriptor() && !targetDesc.Configurable) { if ((targetDesc.Set ?? Undefined).IsUndefined()) { Throw.TypeError(_engine.Realm, $"'set' on proxy: trap returned truish for property '{property}' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter"); } } } return true; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc /// public override bool DefineOwnProperty(JsValue property, PropertyDescriptor desc) { var arguments = new[] { _target, TypeConverter.ToPropertyKey(property), PropertyDescriptor.FromPropertyDescriptor(_engine, desc, strictUndefined: true) }; if (!TryCallHandler(TrapDefineProperty, arguments, out var result)) { return _target.DefineOwnProperty(property, desc); } var success = TypeConverter.ToBoolean(result); if (!success) { return false; } var targetDesc = _target.GetOwnProperty(property); var extensibleTarget = _target.Extensible; var settingConfigFalse = desc.ConfigurableSet && !desc.Configurable; if (targetDesc == PropertyDescriptor.Undefined) { if (!extensibleTarget || settingConfigFalse) { Throw.TypeError(_engine.Realm); } } else { if (!IsCompatiblePropertyDescriptor(extensibleTarget, desc, targetDesc)) { Throw.TypeError(_engine.Realm); } if (targetDesc.Configurable && settingConfigFalse) { Throw.TypeError(_engine.Realm); } if (targetDesc.IsDataDescriptor() && !targetDesc.Configurable && targetDesc.Writable) { if (desc.WritableSet && !desc.Writable) { Throw.TypeError(_engine.Realm); } } } return true; } private static bool IsCompatiblePropertyDescriptor(bool extensible, PropertyDescriptor desc, PropertyDescriptor current) { return ValidateAndApplyPropertyDescriptor(null, JsString.Empty, extensible, desc, current); } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-hasproperty-p /// public override bool HasProperty(JsValue property) { if (!TryCallHandler(TrapHas, [_target, TypeConverter.ToPropertyKey(property)], out var jsValue)) { return _target.HasProperty(property); } var trapResult = TypeConverter.ToBoolean(jsValue); if (!trapResult) { var targetDesc = _target.GetOwnProperty(property); if (targetDesc != PropertyDescriptor.Undefined) { if (!targetDesc.Configurable) { Throw.TypeError(_engine.Realm); } if (!_target.Extensible) { Throw.TypeError(_engine.Realm); } } } return trapResult; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-delete-p /// public override bool Delete(JsValue property) { if (!TryCallHandler(TrapDeleteProperty, [_target, TypeConverter.ToPropertyKey(property)], out var result)) { return _target.Delete(property); } var booleanTrapResult = TypeConverter.ToBoolean(result); if (!booleanTrapResult) { return false; } var targetDesc = _target.GetOwnProperty(property); if (targetDesc == PropertyDescriptor.Undefined) { return true; } if (!targetDesc.Configurable) { Throw.TypeError(_engine.Realm, $"'deleteProperty' on proxy: trap returned truish for property '{property}' which is non-configurable in the proxy target"); } if (!_target.Extensible) { Throw.TypeError(_engine.Realm); } return true; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-preventextensions /// public override bool PreventExtensions() { if (!TryCallHandler(TrapPreventExtensions, [_target], out var result)) { return _target.PreventExtensions(); } var success = TypeConverter.ToBoolean(result); if (success && _target.Extensible) { Throw.TypeError(_engine.Realm); } return success; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-isextensible /// public override bool Extensible { get { if (!TryCallHandler(TrapIsExtensible, [_target], out var result)) { return _target.Extensible; } var booleanTrapResult = TypeConverter.ToBoolean(result); var targetResult = _target.Extensible; if (booleanTrapResult != targetResult) { Throw.TypeError(_engine.Realm); } return booleanTrapResult; } } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof /// protected internal override ObjectInstance? GetPrototypeOf() { if (!TryCallHandler(TrapGetProtoTypeOf, [_target], out var handlerProto)) { return _target.Prototype; } if (!handlerProto.IsObject() && !handlerProto.IsNull()) { Throw.TypeError(_engine.Realm, "'getPrototypeOf' on proxy: trap returned neither object nor null"); } if (_target.Extensible) { return (ObjectInstance) handlerProto; } if (!ReferenceEquals(handlerProto, _target.Prototype)) { Throw.TypeError(_engine.Realm); } return (ObjectInstance) handlerProto; } /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-setprototypeof-v /// internal override bool SetPrototypeOf(JsValue value) { if (!TryCallHandler(TrapSetProtoTypeOf, [_target, value], out var result)) { return _target.SetPrototypeOf(value); } var success = TypeConverter.ToBoolean(result); if (!success) { return false; } if (_target.Extensible) { return true; } if (!ReferenceEquals(value, _target.Prototype)) { Throw.TypeError(_engine.Realm); } return true; } internal override bool IsCallable { get; } private bool TryCallHandler(JsValue propertyName, JsCallArguments arguments, out JsValue result) { AssertNotRevoked(propertyName); result = Undefined; var handlerFunction = _handler!.Get(propertyName); if (!handlerFunction.IsNullOrUndefined()) { var callable = handlerFunction as ICallable; if (callable is null) { Throw.TypeError(_engine.Realm, $"{_handler} returned for property '{propertyName}' of object '{_target}' is not a function"); } result = callable.Call(_handler, arguments); return true; } return false; } private void AssertNotRevoked(JsValue key) { if (_handler is null) { Throw.TypeError(_engine.Realm, $"Cannot perform '{key}' on a proxy that has been revoked"); } } private void AssertTargetNotRevoked(JsValue key) { if (_target is null) { Throw.TypeError(_engine.Realm, $"Cannot perform '{key}' on a proxy that has been revoked"); } } public override string ToString() => IsCallable ? "function () { [native code] }" : base.ToString(); }