mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-24 16:17:29 +09:00
Merge remote-tracking branch 'origin/tracking' into custom
This commit is contained in:
commit
7f3ccb5dc3
12 changed files with 310 additions and 188 deletions
|
|
@ -373,7 +373,9 @@ void PannerNode::DestroyMediaStream()
|
||||||
float
|
float
|
||||||
PannerNodeEngine::LinearGainFunction(double aDistance)
|
PannerNodeEngine::LinearGainFunction(double aDistance)
|
||||||
{
|
{
|
||||||
return 1 - mRolloffFactor * (std::max(std::min(aDistance, mMaxDistance), mRefDistance) - mRefDistance) / (mMaxDistance - mRefDistance);
|
double clampedRollof = std::clamp(mRolloffFactor, 0.0, 1.0);
|
||||||
|
return AssertedCast<float>(
|
||||||
|
1.0 - clampedRollof * (std::max(std::min(aDistance, mMaxDistance), mRefDistance) - mRefDistance) / (mMaxDistance - mRefDistance));
|
||||||
}
|
}
|
||||||
|
|
||||||
float
|
float
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ public:
|
||||||
{
|
{
|
||||||
mPt.mX = aPt.x;
|
mPt.mX = aPt.x;
|
||||||
mPt.mY = aPt.y;
|
mPt.mY = aPt.y;
|
||||||
NS_ASSERTION(IsFinite(mPt.mX) && IsFinite(mPt.mX),
|
NS_ASSERTION(IsFinite(mPt.mX) && IsFinite(mPt.mY),
|
||||||
"DOMSVGPoint coords are not finite");
|
"DOMSVGPoint coords are not finite");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1246,9 +1246,8 @@ private:
|
||||||
|
|
||||||
// We did inherit CSP in bug 1223647. If we do not already have a CSP, we
|
// We did inherit CSP in bug 1223647. If we do not already have a CSP, we
|
||||||
// should get it from the HTTP headers on the worker script.
|
// should get it from the HTTP headers on the worker script.
|
||||||
if (mWorkerPrivate->CSPEnabled() &&
|
if (CSPService::sCSPEnabled &&
|
||||||
!mWorkerPrivate->GetCSP() &&
|
!mWorkerPrivate->GetCSP()) {
|
||||||
CSPService::sCSPEnabled) {
|
|
||||||
rv = mWorkerPrivate->SetCSPFromHeaderValues(tCspHeaderValue,
|
rv = mWorkerPrivate->SetCSPFromHeaderValues(tCspHeaderValue,
|
||||||
tCspROHeaderValue);
|
tCspROHeaderValue);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ using namespace mozilla;
|
||||||
"/System/Library/Frameworks/ApplicationServices.framework/Frameworks/" \
|
"/System/Library/Frameworks/ApplicationServices.framework/Frameworks/" \
|
||||||
"CoreGraphics.framework/CoreGraphics"
|
"CoreGraphics.framework/CoreGraphics"
|
||||||
#define COREVIDEO_FRAMEWORK_PATH \
|
#define COREVIDEO_FRAMEWORK_PATH \
|
||||||
"/System/Library/Frameworks/ApplicationServices.framework/Frameworks/" \
|
"/System/Library/Frameworks/" \
|
||||||
"CoreVideo.framework/CoreVideo"
|
"CoreVideo.framework/CoreVideo"
|
||||||
|
|
||||||
#define GET_CONST(const_name) \
|
#define GET_CONST(const_name) \
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ enum RejectFunctionSlots {
|
||||||
|
|
||||||
enum PromiseCombinatorElementFunctionSlots {
|
enum PromiseCombinatorElementFunctionSlots {
|
||||||
PromiseCombinatorElementFunctionSlot_Data = 0,
|
PromiseCombinatorElementFunctionSlot_Data = 0,
|
||||||
PromiseCombinatorElementFunctionSlot_ElementIndex,
|
PromiseCombinatorElementFunctionSlot_ElementIndexOrResolveFunc,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ReactionJobSlots {
|
enum ReactionJobSlots {
|
||||||
|
|
@ -2351,7 +2351,8 @@ PerformPromiseThenWithoutSettleHandlers(JSContext* cx, Handle<PromiseObject*> pr
|
||||||
|
|
||||||
static JSFunction* NewPromiseCombinatorElementFunction(
|
static JSFunction* NewPromiseCombinatorElementFunction(
|
||||||
JSContext* cx, Native native,
|
JSContext* cx, Native native,
|
||||||
Handle<PromiseCombinatorDataHolder*> dataHolder, uint32_t index);
|
Handle<PromiseCombinatorDataHolder*> dataHolder, uint32_t index,
|
||||||
|
Handle<Value> maybeResolveFunc);
|
||||||
|
|
||||||
static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, Value* vp);
|
static bool PromiseAllResolveElementFunction(JSContext* cx, unsigned argc, Value* vp);
|
||||||
|
|
||||||
|
|
@ -2430,7 +2431,7 @@ js::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises)
|
||||||
|
|
||||||
// Steps j-o.
|
// Steps j-o.
|
||||||
JSFunction* resolveFunc = NewPromiseCombinatorElementFunction(
|
JSFunction* resolveFunc = NewPromiseCombinatorElementFunction(
|
||||||
cx, PromiseAllResolveElementFunction, dataHolder, index);
|
cx, PromiseAllResolveElementFunction, dataHolder, index, UndefinedHandleValue);
|
||||||
if (!resolveFunc)
|
if (!resolveFunc)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
|
|
@ -2888,7 +2889,7 @@ GetPromiseCombinatorElements(JSContext* cx, Handle<PromiseCombinatorDataHolder*>
|
||||||
static JSFunction*
|
static JSFunction*
|
||||||
NewPromiseCombinatorElementFunction(JSContext* cx, Native native,
|
NewPromiseCombinatorElementFunction(JSContext* cx, Native native,
|
||||||
Handle<PromiseCombinatorDataHolder*> dataHolder,
|
Handle<PromiseCombinatorDataHolder*> dataHolder,
|
||||||
uint32_t index)
|
uint32_t index, Handle<Value> maybeResolveFunc)
|
||||||
{
|
{
|
||||||
JSFunction* fn = NewNativeFunction(cx, native, 1, nullptr,
|
JSFunction* fn = NewNativeFunction(cx, native, 1, nullptr,
|
||||||
gc::AllocKind::FUNCTION_EXTENDED, GenericObject);
|
gc::AllocKind::FUNCTION_EXTENDED, GenericObject);
|
||||||
|
|
@ -2898,8 +2899,13 @@ NewPromiseCombinatorElementFunction(JSContext* cx, Native native,
|
||||||
|
|
||||||
fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_Data,
|
fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_Data,
|
||||||
ObjectValue(*dataHolder));
|
ObjectValue(*dataHolder));
|
||||||
fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_ElementIndex,
|
if (maybeResolveFunc.isObject()) {
|
||||||
|
fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_ElementIndexOrResolveFunc,
|
||||||
|
maybeResolveFunc);
|
||||||
|
} else {
|
||||||
|
fn->setExtendedSlot(PromiseCombinatorElementFunctionSlot_ElementIndexOrResolveFunc,
|
||||||
Int32Value(index));
|
Int32Value(index));
|
||||||
|
}
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2917,6 +2923,13 @@ PromiseCombinatorElementFunctionAlreadyCalled(const CallArgs& args,
|
||||||
// Step 1.
|
// Step 1.
|
||||||
JSFunction* fn = &args.callee().as<JSFunction>();
|
JSFunction* fn = &args.callee().as<JSFunction>();
|
||||||
|
|
||||||
|
size_t indexOrResolveFuncSlot = PromiseCombinatorElementFunctionSlot_ElementIndexOrResolveFunc;
|
||||||
|
if (fn->getExtendedSlot(indexOrResolveFuncSlot).isObject()) {
|
||||||
|
Value slotVal = fn->getExtendedSlot(indexOrResolveFuncSlot);
|
||||||
|
fn = &slotVal.toObject().as<JSFunction>();
|
||||||
|
}
|
||||||
|
MOZ_RELEASE_ASSERT(fn->getExtendedSlot(indexOrResolveFuncSlot).isInt32());
|
||||||
|
|
||||||
// Step 2.
|
// Step 2.
|
||||||
const Value& dataVal =
|
const Value& dataVal =
|
||||||
fn->getExtendedSlot(PromiseCombinatorElementFunctionSlot_Data);
|
fn->getExtendedSlot(PromiseCombinatorElementFunctionSlot_Data);
|
||||||
|
|
@ -2936,9 +2949,7 @@ PromiseCombinatorElementFunctionAlreadyCalled(const CallArgs& args,
|
||||||
UndefinedValue());
|
UndefinedValue());
|
||||||
|
|
||||||
// Step 5.
|
// Step 5.
|
||||||
int32_t idx =
|
int32_t idx = fn->getExtendedSlot(indexOrResolveFuncSlot).toInt32();
|
||||||
fn->getExtendedSlot(PromiseCombinatorElementFunctionSlot_ElementIndex)
|
|
||||||
.toInt32();
|
|
||||||
MOZ_ASSERT(idx >= 0);
|
MOZ_ASSERT(idx >= 0);
|
||||||
*index = uint32_t(idx);
|
*index = uint32_t(idx);
|
||||||
|
|
||||||
|
|
@ -2988,7 +2999,7 @@ PerformPromiseAll(JSContext *cx, PromiseForOfIterator& iterator, HandleObject C,
|
||||||
|
|
||||||
// Steps 8.j-p.
|
// Steps 8.j-p.
|
||||||
JSFunction* resolveFunc = NewPromiseCombinatorElementFunction(cx,
|
JSFunction* resolveFunc = NewPromiseCombinatorElementFunction(cx,
|
||||||
PromiseAllResolveElementFunction, dataHolder, index);
|
PromiseAllResolveElementFunction, dataHolder, index, UndefinedHandleValue);
|
||||||
if (!resolveFunc)
|
if (!resolveFunc)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -3187,7 +3198,7 @@ static MOZ_MUST_USE bool PerformPromiseAllSettled(
|
||||||
|
|
||||||
// Steps 8.j-q.
|
// Steps 8.j-q.
|
||||||
JSFunction* resolveFunc = NewPromiseCombinatorElementFunction(
|
JSFunction* resolveFunc = NewPromiseCombinatorElementFunction(
|
||||||
cx, PromiseAllSettledResolveElementFunction, dataHolder, index);
|
cx, PromiseAllSettledResolveElementFunction, dataHolder, index, UndefinedHandleValue);
|
||||||
if (!resolveFunc) {
|
if (!resolveFunc) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -3195,7 +3206,7 @@ static MOZ_MUST_USE bool PerformPromiseAllSettled(
|
||||||
|
|
||||||
// Steps 8.r-x.
|
// Steps 8.r-x.
|
||||||
JSFunction* rejectFunc = NewPromiseCombinatorElementFunction(
|
JSFunction* rejectFunc = NewPromiseCombinatorElementFunction(
|
||||||
cx, PromiseAllSettledRejectElementFunction, dataHolder, index);
|
cx, PromiseAllSettledRejectElementFunction, dataHolder, index, resolveFunVal);
|
||||||
if (!rejectFunc) {
|
if (!rejectFunc) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -3391,7 +3402,7 @@ PerformPromiseAny(JSContext* cx, PromiseForOfIterator& iterator, HandleObject C,
|
||||||
|
|
||||||
// Steps 8.j-p.
|
// Steps 8.j-p.
|
||||||
JSFunction* rejectFunc = NewPromiseCombinatorElementFunction(
|
JSFunction* rejectFunc = NewPromiseCombinatorElementFunction(
|
||||||
cx, PromiseAnyRejectElementFunction, dataHolder, index);
|
cx, PromiseAnyRejectElementFunction, dataHolder, index, UndefinedHandleValue);
|
||||||
if (!rejectFunc) {
|
if (!rejectFunc) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2892,6 +2892,12 @@ jit::ExtractLinearSum(MDefinition* ins, MathSpace space)
|
||||||
return SimpleLinearSum(ins, 0);
|
return SimpleLinearSum(ins, 0);
|
||||||
MOZ_ASSERT(space == MathSpace::Modulo || space == MathSpace::Infinite);
|
MOZ_ASSERT(space == MathSpace::Modulo || space == MathSpace::Infinite);
|
||||||
|
|
||||||
|
// We don't support modulo math space in ExtractLinearSum, because it
|
||||||
|
// doesn't work well in some compilation environments.
|
||||||
|
if (space == MathSpace::Modulo) {
|
||||||
|
return SimpleLinearSum(ins, 0);
|
||||||
|
}
|
||||||
|
|
||||||
MDefinition* lhs = ins->getOperand(0);
|
MDefinition* lhs = ins->getOperand(0);
|
||||||
MDefinition* rhs = ins->getOperand(1);
|
MDefinition* rhs = ins->getOperand(1);
|
||||||
if (lhs->type() != MIRType::Int32 || rhs->type() != MIRType::Int32)
|
if (lhs->type() != MIRType::Int32 || rhs->type() != MIRType::Int32)
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ if sys.platform == 'darwin':
|
||||||
|
|
||||||
KNOWN_SDK_VERSIONS = ["10.7", "10.8", "10.9", "10.10", "10.11"
|
KNOWN_SDK_VERSIONS = ["10.7", "10.8", "10.9", "10.10", "10.11"
|
||||||
"10.12", "10.13", "10.14", "10.15", "10.16",
|
"10.12", "10.13", "10.14", "10.15", "10.16",
|
||||||
"11.0", "11.1", "11.2", "11.3",
|
"11.0", "11.1", "11.2", "11.3", "12.0", "12.1", "12.3",
|
||||||
"12.0"]
|
"13.0", "13.1", "13.2", "14.0", "14.2", "14.4", "14.5",
|
||||||
|
"15.0", "15.1", "15.2", "15.4"]
|
||||||
|
|
||||||
REGEX = "^MacOSX(\d+\.\d+)\.sdk$"
|
REGEX = "^MacOSX(\d+\.\d+)\.sdk$"
|
||||||
SDK_VERSION = re.findall(REGEX, os.path.basename(SDK_PATH))
|
SDK_VERSION = re.findall(REGEX, os.path.basename(SDK_PATH))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
#include <cstdlib> // These need to be included before overriding
|
||||||
|
#include <string.h> // with clang on 32-bit ARM Linux -dbsoft
|
||||||
#include <stddef.h> // for size_t
|
#include <stddef.h> // for size_t
|
||||||
|
|
||||||
// Building with USE_STATIC_LIBS = True sets -MT instead of -MD. -MT sets _MT,
|
// Building with USE_STATIC_LIBS = True sets -MT instead of -MD. -MT sets _MT,
|
||||||
|
|
|
||||||
266
mfbt/Casting.h
266
mfbt/Casting.h
|
|
@ -11,7 +11,8 @@
|
||||||
#include "mozilla/Assertions.h"
|
#include "mozilla/Assertions.h"
|
||||||
#include "mozilla/TypeTraits.h"
|
#include "mozilla/TypeTraits.h"
|
||||||
|
|
||||||
#include <limits.h>
|
#include <limits>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace mozilla {
|
namespace mozilla {
|
||||||
|
|
||||||
|
|
@ -62,190 +63,141 @@ BitwiseCast(const From aFrom)
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
|
|
||||||
enum ToSignedness { ToIsSigned, ToIsUnsigned };
|
template <typename T>
|
||||||
enum FromSignedness { FromIsSigned, FromIsUnsigned };
|
constexpr int64_t safe_integer() {
|
||||||
|
static_assert(std::is_floating_point_v<T>);
|
||||||
|
return std::pow(2, std::numeric_limits<T>::digits);
|
||||||
|
}
|
||||||
|
|
||||||
template<typename From,
|
template <typename T>
|
||||||
typename To,
|
constexpr uint64_t safe_integer_unsigned() {
|
||||||
FromSignedness = IsSigned<From>::value ? FromIsSigned : FromIsUnsigned,
|
static_assert(std::is_floating_point_v<T>);
|
||||||
ToSignedness = IsSigned<To>::value ? ToIsSigned : ToIsUnsigned>
|
return std::pow(2, std::numeric_limits<T>::digits);
|
||||||
struct BoundsCheckImpl;
|
}
|
||||||
|
|
||||||
// Implicit conversions on operands to binary operations make this all a bit
|
#ifdef __GNUC__
|
||||||
// hard to verify. Attempt to ease the pain below by *only* comparing values
|
// This is working around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81676,
|
||||||
// that are obviously the same type (and will undergo no further conversions),
|
// fixed in gcc-10
|
||||||
// even when it's not strictly necessary, for explicitness.
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
|
||||||
|
#endif
|
||||||
|
|
||||||
enum UUComparison { FromIsBigger, FromIsNotBigger };
|
template <typename In, typename Out>
|
||||||
|
bool IsInBounds(In aIn) {
|
||||||
|
constexpr bool inSigned = std::is_signed_v<In>;
|
||||||
|
constexpr bool outSigned = std::is_signed_v<Out>;
|
||||||
|
constexpr bool bothSigned = inSigned && outSigned;
|
||||||
|
constexpr bool bothUnsigned = !inSigned && !outSigned;
|
||||||
|
constexpr bool inFloat = std::is_floating_point_v<In>;
|
||||||
|
constexpr bool outFloat = std::is_floating_point_v<Out>;
|
||||||
|
constexpr bool bothFloat = inFloat && outFloat;
|
||||||
|
constexpr bool noneFloat = !inFloat && !outFloat;
|
||||||
|
constexpr Out outMax = std::numeric_limits<Out>::max();
|
||||||
|
constexpr Out outMin = std::numeric_limits<Out>::lowest();
|
||||||
|
|
||||||
// Unsigned-to-unsigned range check
|
// This selects the widest of two types, and is used to cast throughout.
|
||||||
|
using select_widest = std::conditional_t<(sizeof(In) > sizeof(Out)), In, Out>;
|
||||||
|
|
||||||
template<typename From, typename To,
|
if constexpr (bothFloat) {
|
||||||
UUComparison = (sizeof(From) > sizeof(To))
|
if (aIn > select_widest(outMax) || aIn < select_widest(outMin)) {
|
||||||
? FromIsBigger
|
|
||||||
: FromIsNotBigger>
|
|
||||||
struct UnsignedUnsignedCheck;
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct UnsignedUnsignedCheck<From, To, FromIsBigger>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return aFrom <= From(To(-1));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct UnsignedUnsignedCheck<From, To, FromIsNotBigger>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct BoundsCheckImpl<From, To, FromIsUnsigned, ToIsUnsigned>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return UnsignedUnsignedCheck<From, To>::checkBounds(aFrom);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Signed-to-unsigned range check
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct BoundsCheckImpl<From, To, FromIsSigned, ToIsUnsigned>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
if (aFrom < 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (sizeof(To) >= sizeof(From)) {
|
}
|
||||||
|
// Normal casting applies, the floating point number is floored.
|
||||||
|
if constexpr (inFloat && !outFloat) {
|
||||||
|
static_assert(sizeof(aIn) <= sizeof(int64_t));
|
||||||
|
// Check if the input floating point is larger than the output bounds. This
|
||||||
|
// catches situations where the input is a float larger than the max of the
|
||||||
|
// output type.
|
||||||
|
if (aIn < static_cast<double>(outMin) ||
|
||||||
|
aIn > static_cast<double>(outMax)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// At this point we know that the input can be converted to an integer.
|
||||||
|
// Check if it's larger than the bounds of the target integer.
|
||||||
|
if (outSigned) {
|
||||||
|
int64_t asInteger = static_cast<int64_t>(aIn);
|
||||||
|
if (asInteger < outMin || asInteger > outMax) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uint64_t asInteger = static_cast<uint64_t>(aIn);
|
||||||
|
if (asInteger > outMax) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Checks if the integer is representable exactly as a floating point value of
|
||||||
|
// a specific width.
|
||||||
|
if constexpr (!inFloat && outFloat) {
|
||||||
|
if constexpr (inSigned) {
|
||||||
|
if (aIn < -safe_integer<Out>() || aIn > safe_integer<Out>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (aIn >= safe_integer_unsigned<Out>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if constexpr (noneFloat) {
|
||||||
|
if constexpr (bothUnsigned) {
|
||||||
|
if (aIn > select_widest(outMax)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if constexpr (bothSigned) {
|
||||||
|
if (aIn > select_widest(outMax) || aIn < select_widest(outMin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if constexpr (inSigned && !outSigned) {
|
||||||
|
if (aIn < 0 || std::make_unsigned_t<In>(aIn) > outMax) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if constexpr (!inSigned && outSigned) {
|
||||||
|
if (aIn > select_widest(outMax)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
return aFrom <= From(To(-1));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Unsigned-to-signed range check
|
|
||||||
|
|
||||||
enum USComparison { FromIsSmaller, FromIsNotSmaller };
|
|
||||||
|
|
||||||
template<typename From, typename To,
|
|
||||||
USComparison = (sizeof(From) < sizeof(To))
|
|
||||||
? FromIsSmaller
|
|
||||||
: FromIsNotSmaller>
|
|
||||||
struct UnsignedSignedCheck;
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct UnsignedSignedCheck<From, To, FromIsSmaller>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct UnsignedSignedCheck<From, To, FromIsNotSmaller>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
const To MaxValue = To((1ULL << (CHAR_BIT * sizeof(To) - 1)) - 1);
|
|
||||||
return aFrom <= From(MaxValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct BoundsCheckImpl<From, To, FromIsUnsigned, ToIsSigned>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return UnsignedSignedCheck<From, To>::checkBounds(aFrom);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Signed-to-signed range check
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
struct BoundsCheckImpl<From, To, FromIsSigned, ToIsSigned>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
if (sizeof(From) <= sizeof(To)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const To MaxValue = To((1ULL << (CHAR_BIT * sizeof(To) - 1)) - 1);
|
|
||||||
const To MinValue = -MaxValue - To(1);
|
|
||||||
return From(MinValue) <= aFrom &&
|
|
||||||
From(aFrom) <= From(MaxValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To,
|
|
||||||
bool TypesAreIntegral = IsIntegral<From>::value &&
|
|
||||||
IsIntegral<To>::value>
|
|
||||||
class BoundsChecker;
|
|
||||||
|
|
||||||
template<typename From>
|
|
||||||
class BoundsChecker<From, From, true>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom) { return true; }
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
class BoundsChecker<From, To, true>
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
static bool checkBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return BoundsCheckImpl<From, To>::checkBounds(aFrom);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename From, typename To>
|
|
||||||
inline bool
|
|
||||||
IsInBounds(const From aFrom)
|
|
||||||
{
|
|
||||||
return BoundsChecker<From, To>::checkBounds(aFrom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cast a value of integral type |From| to a value of integral type |To|,
|
* Cast a value of type |From| to a value of type |To|, asserting that the cast
|
||||||
* asserting that the cast will be a safe cast per C++ (that is, that |to| is in
|
* will be a safe cast per C++ (that is, that |to| is in the range of values
|
||||||
* the range of values permitted for the type |From|).
|
* permitted for the type |From|).
|
||||||
|
* In particular, this will fail if a integer cann
|
||||||
*/
|
*/
|
||||||
template<typename To, typename From>
|
template<typename To, typename From>
|
||||||
inline To
|
inline To
|
||||||
AssertedCast(const From aFrom)
|
AssertedCast(const From aFrom)
|
||||||
{
|
{
|
||||||
|
static_assert(std::is_arithmetic_v<To> && std::is_arithmetic_v<From>);
|
||||||
MOZ_ASSERT((detail::IsInBounds<From, To>(aFrom)));
|
MOZ_ASSERT((detail::IsInBounds<From, To>(aFrom)));
|
||||||
return static_cast<To>(aFrom);
|
return static_cast<To>(aFrom);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cast a value of integral type |From| to a value of integral type |To|,
|
* Cast a value of numeric type |From| to a value of numeric type |To|, release
|
||||||
* release asserting that the cast will be a safe cast per C++ (that is, that
|
* asserting that the cast will be a safe cast per C++ (that is, that |to| is in
|
||||||
* |to| is in the range of values permitted for the type |From|).
|
* the range of values permitted for the type |From|).
|
||||||
|
* In particular, this will fail if a integer cannot be represented exactly as a
|
||||||
|
* floating point value, because it's too large.
|
||||||
*/
|
*/
|
||||||
template<typename To, typename From>
|
template<typename To, typename From>
|
||||||
inline To
|
inline To
|
||||||
ReleaseAssertedCast(const From aFrom)
|
ReleaseAssertedCast(const From aFrom)
|
||||||
{
|
{
|
||||||
|
static_assert(std::is_arithmetic_v<To> && std::is_arithmetic_v<From>);
|
||||||
MOZ_RELEASE_ASSERT((detail::IsInBounds<From, To>(aFrom)));
|
MOZ_RELEASE_ASSERT((detail::IsInBounds<From, To>(aFrom)));
|
||||||
return static_cast<To>(aFrom);
|
return static_cast<To>(aFrom);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@
|
||||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
#include "mozilla/Casting.h"
|
#include "mozilla/Casting.h"
|
||||||
|
#include "mozilla/ThreadSafety.h"
|
||||||
|
#include "mozilla/TypeTraits.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
using mozilla::AssertedCast;
|
||||||
using mozilla::BitwiseCast;
|
using mozilla::BitwiseCast;
|
||||||
using mozilla::detail::IsInBounds;
|
using mozilla::detail::IsInBounds;
|
||||||
|
|
||||||
|
static const uint8_t floatMantissaBitsPlusOne = 24;
|
||||||
|
static const uint8_t doubleMantissaBitsPlusOne = 53;
|
||||||
|
|
||||||
template<typename Uint, typename Ulong, bool = (sizeof(Uint) == sizeof(Ulong))>
|
template<typename Uint, typename Ulong, bool = (sizeof(Uint) == sizeof(Ulong))>
|
||||||
struct UintUlongBitwiseCast;
|
struct UintUlongBitwiseCast;
|
||||||
|
|
||||||
|
|
@ -98,6 +106,153 @@ TestToSmallerSize()
|
||||||
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, uint32_t>(int64_t(UINT32_MAX) + 1)));
|
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, uint32_t>(int64_t(UINT32_MAX) + 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename In, typename Out>
|
||||||
|
void checkBoundariesFloating(In aEpsilon = {}, Out aIntegerOffset = {}) {
|
||||||
|
// Check the max value of the input float can't be represented as an integer.
|
||||||
|
// This is true for all floating point and integer width.
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(std::numeric_limits<In>::max())));
|
||||||
|
// Check that the max value of the integer, as a float, minus an offset that
|
||||||
|
// depends on the magnitude, can be represented as an integer.
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<In, Out>(
|
||||||
|
static_cast<In>(std::numeric_limits<Out>::max() - aIntegerOffset))));
|
||||||
|
// Check that the max value of the integer, plus a number that depends on the
|
||||||
|
// magnitude of the number, can't be represented as this integer (because it
|
||||||
|
// becomes too big).
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(
|
||||||
|
aEpsilon + static_cast<In>(std::numeric_limits<Out>::max()))));
|
||||||
|
if constexpr (std::is_signed_v<In>) {
|
||||||
|
// Same for negative numbers.
|
||||||
|
MOZ_RELEASE_ASSERT(
|
||||||
|
(!IsInBounds<In, Out>(std::numeric_limits<In>::lowest())));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<In, Out>(
|
||||||
|
static_cast<In>(std::numeric_limits<Out>::lowest()))));
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(
|
||||||
|
static_cast<In>(std::numeric_limits<Out>::lowest()) - aEpsilon)));
|
||||||
|
} else {
|
||||||
|
// Check for negative floats and unsigned integer types.
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(static_cast<In>(-1))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestFloatConversion() {
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<uint64_t, float>(UINT64_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<uint32_t, float>(UINT32_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<uint16_t, float>(UINT16_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<uint8_t, float>(UINT8_MAX)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, float>(INT64_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, float>(INT64_MIN)));
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<int32_t, float>(INT32_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<int32_t, float>(INT32_MIN)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, float>(INT16_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, float>(INT16_MIN)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, float>(INT8_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, float>(INT8_MIN)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<uint64_t, double>(UINT64_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<uint32_t, double>(UINT32_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<uint16_t, double>(UINT16_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<uint8_t, double>(UINT8_MAX)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, double>(INT64_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, double>(INT64_MIN)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int32_t, double>(INT32_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int32_t, double>(INT32_MIN)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, double>(INT16_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, double>(INT16_MIN)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, double>(INT8_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, double>(INT8_MIN)));
|
||||||
|
|
||||||
|
// Floor check
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, uint64_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<uint64_t>(4.3f) == 4u));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, uint32_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<uint32_t>(4.3f) == 4u));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, uint16_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<uint16_t>(4.3f) == 4u));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, uint8_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<uint8_t>(4.3f) == 4u));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int64_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int64_t>(4.3f) == 4u));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int32_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int32_t>(4.3f) == 4u));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int16_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int16_t>(4.3f) == 4u));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int8_t>(4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int8_t>(4.3f) == 4u));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int64_t>(-4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int64_t>(-4.3f) == -4));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int32_t>(-4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int32_t>(-4.3f) == -4));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int16_t>(-4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int16_t>(-4.3f) == -4));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<float, int8_t>(-4.3)));
|
||||||
|
MOZ_RELEASE_ASSERT((AssertedCast<int8_t>(-4.3f) == -4));
|
||||||
|
|
||||||
|
// Bound check for float to unsigned integer conversion. The parameters are
|
||||||
|
// espilons and offsets allowing to check boundaries, that depend on the
|
||||||
|
// magnitude of the numbers.
|
||||||
|
checkBoundariesFloating<double, uint64_t>(2049.);
|
||||||
|
checkBoundariesFloating<double, uint32_t>(1.);
|
||||||
|
checkBoundariesFloating<double, uint16_t>(1.);
|
||||||
|
checkBoundariesFloating<double, uint8_t>(1.);
|
||||||
|
// Large number because of the lack of precision of floats at this magnitude
|
||||||
|
checkBoundariesFloating<float, uint64_t>(1.1e12f);
|
||||||
|
checkBoundariesFloating<float, uint32_t>(1.f, 128u);
|
||||||
|
checkBoundariesFloating<float, uint16_t>(1.f);
|
||||||
|
checkBoundariesFloating<float, uint8_t>(1.f);
|
||||||
|
|
||||||
|
checkBoundariesFloating<double, int64_t>(1025.);
|
||||||
|
checkBoundariesFloating<double, int32_t>(1.);
|
||||||
|
checkBoundariesFloating<double, int16_t>(1.);
|
||||||
|
checkBoundariesFloating<double, int8_t>(1.);
|
||||||
|
// Large number because of the lack of precision of floats at this magnitude
|
||||||
|
checkBoundariesFloating<float, int64_t>(1.1e12f);
|
||||||
|
checkBoundariesFloating<float, int32_t>(256.f, 64u);
|
||||||
|
checkBoundariesFloating<float, int16_t>(1.f);
|
||||||
|
checkBoundariesFloating<float, int8_t>(1.f);
|
||||||
|
|
||||||
|
// Integer to floating point, boundary cases
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, float>(
|
||||||
|
int64_t(std::pow(2, floatMantissaBitsPlusOne)) + 1)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
|
||||||
|
int64_t(std::pow(2, floatMantissaBitsPlusOne)))));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
|
||||||
|
int64_t(std::pow(2, floatMantissaBitsPlusOne)) - 1)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, float>(
|
||||||
|
int64_t(-std::pow(2, floatMantissaBitsPlusOne)) - 1)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
|
||||||
|
int64_t(-std::pow(2, floatMantissaBitsPlusOne)))));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
|
||||||
|
int64_t(-std::pow(2, floatMantissaBitsPlusOne)) + 1)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(
|
||||||
|
uint64_t(std::pow(2, doubleMantissaBitsPlusOne)) + 1)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
|
||||||
|
uint64_t(std::pow(2, doubleMantissaBitsPlusOne)))));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
|
||||||
|
uint64_t(std::pow(2, doubleMantissaBitsPlusOne)) - 1)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(
|
||||||
|
int64_t(-std::pow(2, doubleMantissaBitsPlusOne)) - 1)));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
|
||||||
|
int64_t(-std::pow(2, doubleMantissaBitsPlusOne)))));
|
||||||
|
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
|
||||||
|
int64_t(-std::pow(2, doubleMantissaBitsPlusOne)) + 1)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<uint64_t, double>(UINT64_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(INT64_MAX)));
|
||||||
|
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(INT64_MIN)));
|
||||||
|
|
||||||
|
MOZ_RELEASE_ASSERT(
|
||||||
|
!(IsInBounds<double, float>(std::numeric_limits<double>::max())));
|
||||||
|
MOZ_RELEASE_ASSERT(
|
||||||
|
!(IsInBounds<double, float>(-std::numeric_limits<double>::max())));
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
main()
|
main()
|
||||||
{
|
{
|
||||||
|
|
@ -106,6 +261,7 @@ main()
|
||||||
TestSameSize();
|
TestSameSize();
|
||||||
TestToBiggerSize();
|
TestToBiggerSize();
|
||||||
TestToSmallerSize();
|
TestToSmallerSize();
|
||||||
|
TestFloatConversion();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2737,18 +2737,6 @@ pref("layout.frame_rate", -1);
|
||||||
pref("layout.display-list.dump", false);
|
pref("layout.display-list.dump", false);
|
||||||
pref("layout.display-list.dump-content", false);
|
pref("layout.display-list.dump-content", false);
|
||||||
|
|
||||||
// pref to control precision of the frame rate timer. When true,
|
|
||||||
// we use a "precise" timer, which means each notification fires
|
|
||||||
// Nms after the start of the last notification. That means if the
|
|
||||||
// processing of the notification is slow, the timer can fire immediately
|
|
||||||
// after we've just finished processing the last notification, which might
|
|
||||||
// lead to starvation problems.
|
|
||||||
// When false, we use a "slack" timer which fires Nms after the *end*
|
|
||||||
// of the last notification. This can give less tight frame rates
|
|
||||||
// but provides more time for other operations when the browser is
|
|
||||||
// heavily loaded.
|
|
||||||
pref("layout.frame_rate.precise", false);
|
|
||||||
|
|
||||||
// pref to control whether layout warnings that are hit quite often are enabled
|
// pref to control whether layout warnings that are hit quite often are enabled
|
||||||
pref("layout.spammy_warnings.enabled", false);
|
pref("layout.spammy_warnings.enabled", false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,11 @@ elif CONFIG['_MSC_VER']:
|
||||||
'-wd4099', # mismatched class/struct tags
|
'-wd4099', # mismatched class/struct tags
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if CONFIG['_MSC_VER']:
|
||||||
|
CXXFLAGS += [
|
||||||
|
'-GR', # protobuf uses dynamic_cast on polymorphic types
|
||||||
|
]
|
||||||
|
|
||||||
if CONFIG['MOZ_USE_PTHREADS']:
|
if CONFIG['MOZ_USE_PTHREADS']:
|
||||||
DEFINES['HAVE_PTHREAD'] = True
|
DEFINES['HAVE_PTHREAD'] = True
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue