/* -*- 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/. */ /** * A struct for tracking exceptions that need to be thrown to JS. * * Conceptually, an ErrorResult represents either success or an exception in the * process of being thrown. This means that a failing ErrorResult _must_ be * handled in one of the following ways before coming off the stack: * * 1) Suppressed via SuppressException(). * 2) Converted to a pure nsresult return value via StealNSResult(). * 3) Converted to an actual pending exception on a JSContext via * MaybeSetPendingException. * 4) Converted to an exception JS::Value (probably to then reject a Promise * with) via dom::ToJSValue. */ #ifndef mozilla_ErrorResult_h #define mozilla_ErrorResult_h #include #include "js/Value.h" #include "nscore.h" #include "nsStringGlue.h" #include "mozilla/Assertions.h" #include "mozilla/Move.h" #include "nsTArray.h" namespace IPC { class Message; template struct ParamTraits; } // namespace IPC namespace mozilla { namespace dom { enum ErrNum { #define MSG_DEF(_name, _argc, _exn, _str) \ _name, #include "mozilla/dom/Errors.msg" #undef MSG_DEF Err_Limit }; // Debug-only compile-time table of the number of arguments of each error, for use in static_assert. #if defined(DEBUG) && (defined(__clang__) || defined(__GNUC__)) uint16_t constexpr ErrorFormatNumArgs[] = { #define MSG_DEF(_name, _argc, _exn, _str) \ _argc, #include "mozilla/dom/Errors.msg" #undef MSG_DEF }; #endif uint16_t GetErrorArgCount(const ErrNum aErrorNumber); bool ThrowErrorMessage(JSContext* aCx, const ErrNum aErrorNumber, ...); struct StringArrayAppender { static void Append(nsTArray& aArgs, uint16_t aCount) { MOZ_RELEASE_ASSERT(aCount == 0, "Must give at least as many string arguments as are required by the ErrNum."); } template static void Append(nsTArray& aArgs, uint16_t aCount, const nsAString& aFirst, Ts&&... aOtherArgs) { if (aCount == 0) { MOZ_ASSERT(false, "There should not be more string arguments provided than are required by the ErrNum."); return; } aArgs.AppendElement(aFirst); Append(aArgs, aCount - 1, Forward(aOtherArgs)...); } }; } // namespace dom class ErrorResult { public: ErrorResult() : mResult(NS_OK) #ifdef DEBUG , mMightHaveUnreportedJSException(false) , mUnionState(HasNothing) #endif { } #ifdef DEBUG ~ErrorResult() { // Consumers should have called one of MaybeSetPendingException // (possibly via ToJSValue), StealNSResult, and SuppressException MOZ_ASSERT(!Failed()); MOZ_ASSERT(!mMightHaveUnreportedJSException); MOZ_ASSERT(mUnionState == HasNothing); } #endif // DEBUG ErrorResult(ErrorResult&& aRHS) // Initialize mResult and whatever else we need to default-initialize, so // the ClearUnionData call in our operator= will do the right thing // (nothing). : ErrorResult() { *this = Move(aRHS); } ErrorResult& operator=(ErrorResult&& aRHS); explicit ErrorResult(nsresult aRv) : ErrorResult() { AssignErrorCode(aRv); } void Throw(nsresult rv) { MOZ_ASSERT(NS_FAILED(rv), "Please don't try throwing success"); AssignErrorCode(rv); } // Duplicate our current state on the given ErrorResult object. Any existing // errors or messages on the target will be suppressed before cloning. Our // own error state remains unchanged. void CloneTo(ErrorResult& aRv) const; // Use SuppressException when you want to suppress any exception that might be // on the ErrorResult. After this call, the ErrorResult will be back a "no // exception thrown" state. void SuppressException(); // Use StealNSResult() when you want to safely convert the ErrorResult to an // nsresult that you will then return to a caller. This will // SuppressException(), since there will no longer be a way to report it. nsresult StealNSResult() { nsresult rv = ErrorCode(); SuppressException(); // Don't propagate out our internal error codes that have special meaning. if (rv == NS_ERROR_TYPE_ERR || rv == NS_ERROR_RANGE_ERR || rv == NS_ERROR_DOM_JS_EXCEPTION || rv == NS_ERROR_DOM_DOMEXCEPTION) { // What about NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT? I guess that can be // legitimately passed on through.... // What to pick here? return NS_ERROR_DOM_INVALID_STATE_ERR; } return rv; } // Use MaybeSetPendingException to convert an ErrorResult to a pending // exception on the given JSContext. This is the normal "throw an exception" // codepath. // // The return value is false if the ErrorResult represents success, true // otherwise. This does mean that in JSAPI method implementations you can't // just use this as |return rv.MaybeSetPendingException(cx)| (though you could // |return !rv.MaybeSetPendingException(cx)|), but in practice pretty much any // consumer would want to do some more work on the success codepath. So // instead the way you use this is: // // if (rv.MaybeSetPendingException(cx)) { // bail out here // } // go on to do something useful // // The success path is inline, since it should be the common case and we don't // want to pay the price of a function call in some of the consumers of this // method in the common case. // // Note that a true return value does NOT mean there is now a pending // exception on aCx, due to uncatchable exceptions. It should still be // considered equivalent to a JSAPI failure in terms of what callers should do // after true is returned. // // After this call, the ErrorResult will no longer return true from Failed(), // since the exception will have moved to the JSContext. bool MaybeSetPendingException(JSContext* cx) { WouldReportJSException(); if (!Failed()) { return false; } SetPendingException(cx); return true; } template void ThrowTypeError(Ts&&... messageArgs) { ThrowErrorWithMessage(NS_ERROR_TYPE_ERR, Forward(messageArgs)...); } template void ThrowRangeError(Ts&&... messageArgs) { ThrowErrorWithMessage(NS_ERROR_RANGE_ERR, Forward(messageArgs)...); } bool IsErrorWithMessage() const { return ErrorCode() == NS_ERROR_TYPE_ERR || ErrorCode() == NS_ERROR_RANGE_ERR; } // Facilities for throwing a preexisting JS exception value via this // ErrorResult. The contract is that any code which might end up calling // ThrowJSException() must call MightThrowJSException() even if no exception // is being thrown. Code that conditionally calls ToJSValue on this // ErrorResult only if Failed() must first call WouldReportJSException even if // this ErrorResult has not failed. // // The exn argument to ThrowJSException can be in any compartment. It does // not have to be in the compartment of cx. If someone later uses it, they // will wrap it into whatever compartment they're working in, as needed. void ThrowJSException(JSContext* cx, JS::Handle exn); bool IsJSException() const { return ErrorCode() == NS_ERROR_DOM_JS_EXCEPTION; } // Facilities for throwing a DOMException. If an empty message string is // passed to ThrowDOMException, the default message string for the given // nsresult will be used. The passed-in string must be UTF-8. The nsresult // passed in must be one we create DOMExceptions for; otherwise you may get an // XPConnect Exception. void ThrowDOMException(nsresult rv, const nsACString& message = EmptyCString()); bool IsDOMException() const { return ErrorCode() == NS_ERROR_DOM_DOMEXCEPTION; } // Flag on the ErrorResult that whatever needs throwing has been // thrown on the JSContext already and we should not mess with it. void NoteJSContextException() { mResult = NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT; } // Check whether the ErrorResult says to just throw whatever is on // the JSContext already. bool IsJSContextException() { return ErrorCode() == NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT; } // Support for uncatchable exceptions. void ThrowUncatchableException() { Throw(NS_ERROR_UNCATCHABLE_EXCEPTION); } bool IsUncatchableException() const { return ErrorCode() == NS_ERROR_UNCATCHABLE_EXCEPTION; } void MOZ_ALWAYS_INLINE MightThrowJSException() { #ifdef DEBUG mMightHaveUnreportedJSException = true; #endif } void MOZ_ALWAYS_INLINE WouldReportJSException() { #ifdef DEBUG mMightHaveUnreportedJSException = false; #endif } // In the future, we can add overloads of Throw that take more // interesting things, like strings or DOM exception types or // something if desired. // Backwards-compat to make conversion simpler. We don't call // Throw() here because people can easily pass success codes to // this. void operator=(nsresult rv) { AssignErrorCode(rv); } bool Failed() const { return NS_FAILED(mResult); } bool ErrorCodeIs(nsresult rv) const { return mResult == rv; } // For use in logging ONLY. uint32_t ErrorCodeAsInt() const { return static_cast(ErrorCode()); } protected: nsresult ErrorCode() const { return mResult; } private: #ifdef DEBUG enum UnionState { HasMessage, HasDOMExceptionInfo, HasJSException, HasNothing }; #endif // DEBUG friend struct IPC::ParamTraits; void SerializeMessage(IPC::Message* aMsg) const; bool DeserializeMessage(const IPC::Message* aMsg, void** aIter); void SerializeDOMExceptionInfo(IPC::Message* aMsg) const; bool DeserializeDOMExceptionInfo(const IPC::Message* aMsg, void** aIter); // Helper method that creates a new Message for this ErrorResult, // and returns the arguments array from that Message. nsTArray& CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType); template void ThrowErrorWithMessage(nsresult errorType, Ts&&... messageArgs) { #if defined(DEBUG) && (defined(__clang__) || defined(__GNUC__)) static_assert(dom::ErrorFormatNumArgs[errorNumber] == sizeof...(messageArgs), "Pass in the right number of arguments"); #endif ClearUnionData(); nsTArray& messageArgsArray = CreateErrorMessageHelper(errorNumber, errorType); uint16_t argCount = dom::GetErrorArgCount(errorNumber); dom::StringArrayAppender::Append(messageArgsArray, argCount, Forward(messageArgs)...); #ifdef DEBUG mUnionState = HasMessage; #endif // DEBUG } void AssignErrorCode(nsresult aRv) { MOZ_ASSERT(aRv != NS_ERROR_TYPE_ERR, "Use ThrowTypeError()"); MOZ_ASSERT(aRv != NS_ERROR_RANGE_ERR, "Use ThrowRangeError()"); MOZ_ASSERT(!IsErrorWithMessage(), "Don't overwrite errors with message"); MOZ_ASSERT(aRv != NS_ERROR_DOM_JS_EXCEPTION, "Use ThrowJSException()"); MOZ_ASSERT(!IsJSException(), "Don't overwrite JS exceptions"); MOZ_ASSERT(aRv != NS_ERROR_DOM_DOMEXCEPTION, "Use ThrowDOMException()"); MOZ_ASSERT(!IsDOMException(), "Don't overwrite DOM exceptions"); MOZ_ASSERT(aRv != NS_ERROR_XPC_NOT_ENOUGH_ARGS, "May need to bring back ThrowNotEnoughArgsError"); MOZ_ASSERT(aRv != NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT, "Use NoteJSContextException"); // Don't trust people anyway, though. if (aRv == NS_ERROR_TYPE_ERR || aRv == NS_ERROR_RANGE_ERR || aRv == NS_ERROR_DOM_JS_EXCEPTION || aRv == NS_ERROR_DOM_DOMEXCEPTION) { mResult = NS_ERROR_UNEXPECTED; } else { mResult = aRv; } } void ClearMessage(); void ClearDOMExceptionInfo(); // ClearUnionData will try to clear the data in our // mMessage/mJSException/mDOMExceptionInfo union. After this the union may be // in an uninitialized state (e.g. mMessage or mDOMExceptionInfo may be // pointing to deleted memory) and the caller must either reinitialize it or // change mResult to something that will not involve us touching the union // anymore. void ClearUnionData(); // Implementation of MaybeSetPendingException for the case when we're a // failure result. void SetPendingException(JSContext* cx); // Methods for setting various specific kinds of pending exceptions. void SetPendingExceptionWithMessage(JSContext* cx); void SetPendingJSException(JSContext* cx); void SetPendingDOMException(JSContext* cx); void SetPendingGenericErrorException(JSContext* cx); // Special values of mResult: // NS_ERROR_TYPE_ERR -- ThrowTypeError() called on us. // NS_ERROR_RANGE_ERR -- ThrowRangeError() called on us. // NS_ERROR_DOM_JS_EXCEPTION -- ThrowJSException() called on us. // NS_ERROR_UNCATCHABLE_EXCEPTION -- ThrowUncatchableException called on us. // NS_ERROR_DOM_DOMEXCEPTION -- ThrowDOMException() called on us. nsresult mResult; struct Message; struct DOMExceptionInfo; // mMessage is set by ThrowErrorWithMessage and reported (and deallocated) by // SetPendingExceptionWithMessage. // mJSException is set (and rooted) by ThrowJSException and reported // (and unrooted) by SetPendingJSException. // mDOMExceptionInfo is set by ThrowDOMException and reported // (and deallocated) by SetPendingDOMException. union { Message* mMessage; // valid when IsErrorWithMessage() JS::Value mJSException; // valid when IsJSException() DOMExceptionInfo* mDOMExceptionInfo; // valid when IsDOMException() }; #ifdef DEBUG // Used to keep track of codepaths that might throw JS exceptions, // for assertion purposes. bool mMightHaveUnreportedJSException; // Used to keep track of what's stored in our union right now. Note // that this may be set to HasNothing even if our mResult suggests // we should have something, if we have already cleaned up the // something. UnionState mUnionState; #endif // Not to be implemented, to make sure people always pass this by // reference, not by value. ErrorResult(const ErrorResult&) = delete; void operator=(const ErrorResult&) = delete; }; // A class for use when an ErrorResult should just automatically be ignored. class IgnoredErrorResult : public ErrorResult { public: ~IgnoredErrorResult() { SuppressException(); } }; /****************************************************************************** ** Macros for checking results ******************************************************************************/ #define ENSURE_SUCCESS(res, ret) \ do { \ if (res.Failed()) { \ nsCString msg; \ msg.AppendPrintf("ENSURE_SUCCESS(%s, %s) failed with " \ "result 0x%X", #res, #ret, res.ErrorCodeAsInt()); \ NS_WARNING(msg.get()); \ return ret; \ } \ } while(0) #define ENSURE_SUCCESS_VOID(res) \ do { \ if (res.Failed()) { \ nsCString msg; \ msg.AppendPrintf("ENSURE_SUCCESS_VOID(%s) failed with " \ "result 0x%X", #res, res.ErrorCodeAsInt()); \ NS_WARNING(msg.get()); \ return; \ } \ } while(0) } // namespace mozilla #endif /* mozilla_ErrorResult_h */