/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/ObservableArrayProxyHandler.h" #include "jsapi.h" #include "js/friend/ErrorMessages.h" #include "js/Conversions.h" #include "js/Object.h" #include "mozilla/dom/JSSlots.h" #include "mozilla/dom/ProxyHandlerUtils.h" #include "mozilla/dom/ToJSValue.h" #include "mozilla/ErrorResult.h" #include "nsDebug.h" #include "nsJSUtils.h" namespace mozilla::dom { const char ObservableArrayProxyHandler::family = 0; bool ObservableArrayProxyHandler::defineProperty( JSContext* aCx, JS::Handle aProxy, JS::Handle aId, JS::Handle aDesc, JS::ObjectOpResult& aResult) const { if (aId.get() == s_length_id) { if (aDesc.isAccessorDescriptor()) { return aResult.failNotDataDescriptor(); } if (aDesc.hasConfigurable() && aDesc.configurable()) { return aResult.failInvalidDescriptor(); } if (aDesc.hasEnumerable() && aDesc.enumerable()) { return aResult.failInvalidDescriptor(); } if (aDesc.hasWritable() && !aDesc.writable()) { return aResult.failInvalidDescriptor(); } if (aDesc.hasValue()) { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } return SetLength(aCx, aProxy, backingListObj, aDesc.value(), aResult); } return aResult.succeed(); } uint32_t index = GetArrayIndexFromId(aId); if (IsArrayIndex(index)) { if (aDesc.isAccessorDescriptor()) { return aResult.failNotDataDescriptor(); } if (aDesc.hasConfigurable() && !aDesc.configurable()) { return aResult.failInvalidDescriptor(); } if (aDesc.hasEnumerable() && !aDesc.enumerable()) { return aResult.failInvalidDescriptor(); } if (aDesc.hasWritable() && !aDesc.writable()) { return aResult.failInvalidDescriptor(); } if (aDesc.hasValue()) { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } return SetIndexedValue(aCx, aProxy, backingListObj, index, aDesc.value(), aResult); } return aResult.succeed(); } return ForwardingProxyHandler::defineProperty(aCx, aProxy, aId, aDesc, aResult); } bool ObservableArrayProxyHandler::delete_(JSContext* aCx, JS::Handle aProxy, JS::Handle aId, JS::ObjectOpResult& aResult) const { if (aId.get() == s_length_id) { return aResult.failCantDelete(); } uint32_t index = GetArrayIndexFromId(aId); if (IsArrayIndex(index)) { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } uint32_t oldLen = 0; if (!JS::GetArrayLength(aCx, backingListObj, &oldLen)) { return false; } // We do not follow the spec (step 3.3 in // https://webidl.spec.whatwg.org/#es-observable-array-deleteProperty) // is because `oldLen - 1` could be `-1` if the backing list is empty, but // `oldLen` is `uint32_t` in practice. See also // https://github.com/whatwg/webidl/issues/1049. if (oldLen != index + 1) { return aResult.failBadIndex(); } JS::Rooted value(aCx); if (!JS_GetElement(aCx, backingListObj, index, &value)) { return false; } if (!OnDeleteItem(aCx, aProxy, value, index)) { return false; } if (!JS::SetArrayLength(aCx, backingListObj, index)) { return false; } return aResult.succeed(); } return ForwardingProxyHandler::delete_(aCx, aProxy, aId, aResult); } bool ObservableArrayProxyHandler::get(JSContext* aCx, JS::Handle aProxy, JS::Handle aReceiver, JS::Handle aId, JS::MutableHandle aVp) const { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } uint32_t length = 0; if (!JS::GetArrayLength(aCx, backingListObj, &length)) { return false; } if (aId.get() == s_length_id) { return ToJSValue(aCx, length, aVp); } uint32_t index = GetArrayIndexFromId(aId); if (IsArrayIndex(index)) { if (index >= length) { aVp.setUndefined(); return true; } return JS_GetElement(aCx, backingListObj, index, aVp); } return ForwardingProxyHandler::get(aCx, aProxy, aReceiver, aId, aVp); } bool ObservableArrayProxyHandler::getOwnPropertyDescriptor( JSContext* aCx, JS::Handle aProxy, JS::Handle aId, JS::MutableHandle> aDesc) const { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } uint32_t length = 0; if (!JS::GetArrayLength(aCx, backingListObj, &length)) { return false; } if (aId.get() == s_length_id) { JS::Rooted value(aCx, JS::NumberValue(length)); aDesc.set(Some(JS::PropertyDescriptor::Data( value, {JS::PropertyAttribute::Writable}))); return true; } uint32_t index = GetArrayIndexFromId(aId); if (IsArrayIndex(index)) { if (index >= length) { return true; } JS::Rooted value(aCx); if (!JS_GetElement(aCx, backingListObj, index, &value)) { return false; } aDesc.set(Some(JS::PropertyDescriptor::Data( value, {JS::PropertyAttribute::Configurable, JS::PropertyAttribute::Writable, JS::PropertyAttribute::Enumerable}))); return true; } return ForwardingProxyHandler::getOwnPropertyDescriptor(aCx, aProxy, aId, aDesc); } bool ObservableArrayProxyHandler::has(JSContext* aCx, JS::Handle aProxy, JS::Handle aId, bool* aBp) const { if (aId.get() == s_length_id) { *aBp = true; return true; } uint32_t index = GetArrayIndexFromId(aId); if (IsArrayIndex(index)) { uint32_t length = 0; if (!GetBackingListLength(aCx, aProxy, &length)) { return false; } *aBp = (index < length); return true; } return ForwardingProxyHandler::has(aCx, aProxy, aId, aBp); } bool ObservableArrayProxyHandler::ownPropertyKeys( JSContext* aCx, JS::Handle aProxy, JS::MutableHandleVector aProps) const { uint32_t length = 0; if (!GetBackingListLength(aCx, aProxy, &length)) { return false; } for (int32_t i = 0; i < int32_t(length); i++) { if (!aProps.append(JS::PropertyKey::Int(i))) { return false; } } return ForwardingProxyHandler::ownPropertyKeys(aCx, aProxy, aProps); } bool ObservableArrayProxyHandler::preventExtensions( JSContext* aCx, JS::Handle aProxy, JS::ObjectOpResult& aResult) const { return aResult.failCantPreventExtensions(); } bool ObservableArrayProxyHandler::set(JSContext* aCx, JS::Handle aProxy, JS::Handle aId, JS::Handle aV, JS::Handle aReceiver, JS::ObjectOpResult& aResult) const { if (aId.get() == s_length_id) { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } return SetLength(aCx, aProxy, backingListObj, aV, aResult); } uint32_t index = GetArrayIndexFromId(aId); if (IsArrayIndex(index)) { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } return SetIndexedValue(aCx, aProxy, backingListObj, index, aV, aResult); } return ForwardingProxyHandler::set(aCx, aProxy, aId, aV, aReceiver, aResult); } bool ObservableArrayProxyHandler::GetBackingListObject( JSContext* aCx, JS::Handle aProxy, JS::MutableHandle aBackingListObject) const { // Retrieve the backing list object from the reserved slot on the proxy // object. If it doesn't exist yet, create it. JS::Rooted slotValue(aCx); slotValue = js::GetProxyReservedSlot( aProxy, OBSERVABLE_ARRAY_BACKING_LIST_OBJECT_SLOT); if (slotValue.isUndefined()) { JS::Rooted newBackingListObj(aCx); newBackingListObj.set(JS::NewArrayObject(aCx, 0)); if (NS_WARN_IF(!newBackingListObj)) { return false; } slotValue = JS::ObjectValue(*newBackingListObj); js::SetProxyReservedSlot(aProxy, OBSERVABLE_ARRAY_BACKING_LIST_OBJECT_SLOT, slotValue); } aBackingListObject.set(&slotValue.toObject()); return true; } bool ObservableArrayProxyHandler::GetBackingListLength( JSContext* aCx, JS::Handle aProxy, uint32_t* aLength) const { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } return JS::GetArrayLength(aCx, backingListObj, aLength); } bool ObservableArrayProxyHandler::SetLength(JSContext* aCx, JS::Handle aProxy, uint32_t aLength) const { JS::Rooted backingListObj(aCx); if (!GetBackingListObject(aCx, aProxy, &backingListObj)) { return false; } JS::ObjectOpResult result; if (!SetLength(aCx, aProxy, backingListObj, aLength, result)) { return false; } return result ? true : result.reportError(aCx, aProxy); } bool ObservableArrayProxyHandler::SetLength(JSContext* aCx, JS::Handle aProxy, JS::Handle aBackingList, uint32_t aLength, JS::ObjectOpResult& aResult) const { uint32_t oldLen; if (!JS::GetArrayLength(aCx, aBackingList, &oldLen)) { return false; } if (aLength > oldLen) { return aResult.failBadArrayLength(); } bool ok = true; uint32_t len = oldLen; for (; len > aLength; len--) { uint32_t indexToDelete = len - 1; JS::Rooted value(aCx); if (!JS_GetElement(aCx, aBackingList, indexToDelete, &value)) { ok = false; break; } if (!OnDeleteItem(aCx, aProxy, value, indexToDelete)) { ok = false; break; } } return JS::SetArrayLength(aCx, aBackingList, len) && ok ? aResult.succeed() : false; } bool ObservableArrayProxyHandler::SetLength(JSContext* aCx, JS::Handle aProxy, JS::Handle aBackingList, JS::Handle aValue, JS::ObjectOpResult& aResult) const { uint32_t uint32Len; if (!ToUint32(aCx, aValue, &uint32Len)) { return false; } double numberLen; if (!ToNumber(aCx, aValue, &numberLen)) { return false; } if (uint32Len != numberLen) { JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, JSMSG_BAD_INDEX); return false; } return SetLength(aCx, aProxy, aBackingList, uint32Len, aResult); } } // namespace mozilla::dom