mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-29 15:52:07 +00:00
Merge m-c to m-i, a=merge
This commit is contained in:
commit
c383ca71b8
@ -173,6 +173,9 @@ nsNullPrincipal::Read(nsIObjectInputStream* aStream)
|
||||
NS_IMETHODIMP
|
||||
nsNullPrincipal::Write(nsIObjectOutputStream* aStream)
|
||||
{
|
||||
NS_ENSURE_TRUE(mOriginAttributes.mAppId != nsIScriptSecurityManager::UNKNOWN_APP_ID,
|
||||
NS_ERROR_INVALID_ARG);
|
||||
|
||||
nsAutoCString suffix;
|
||||
OriginAttributesRef().CreateSuffix(suffix);
|
||||
|
||||
|
@ -427,6 +427,8 @@ NS_IMETHODIMP
|
||||
nsPrincipal::Write(nsIObjectOutputStream* aStream)
|
||||
{
|
||||
NS_ENSURE_STATE(mCodebase);
|
||||
NS_ENSURE_TRUE(mOriginAttributes.mAppId != nsIScriptSecurityManager::UNKNOWN_APP_ID,
|
||||
NS_ERROR_INVALID_ARG);
|
||||
|
||||
nsresult rv = NS_WriteOptionalCompoundObject(aStream, mCodebase, NS_GET_IID(nsIURI),
|
||||
true);
|
||||
|
@ -103,6 +103,20 @@ function run_test() {
|
||||
var simplePrin = ssm.getSimpleCodebasePrincipal(makeURI('http://example.com'));
|
||||
try { simplePrin.origin; do_check_true(false); } catch (e) { do_check_true(true); }
|
||||
|
||||
// Make sure we don't crash when serializing them either.
|
||||
try {
|
||||
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
|
||||
createInstance(Ci.nsIObjectOutputStream);
|
||||
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
||||
pipe.init(false, false, 0, 0xffffffff, null);
|
||||
binaryStream.setOutputStream(pipe.outputStream);
|
||||
binaryStream.writeCompoundObject(simplePrin, Ci.nsISupports, true);
|
||||
binaryStream.close();
|
||||
} catch (e) {
|
||||
do_check_true(true);
|
||||
}
|
||||
|
||||
|
||||
// Just userContext.
|
||||
var exampleOrg_userContext = ssm.createCodebasePrincipal(makeURI('http://example.org'), {userContextId: 42});
|
||||
checkOriginAttributes(exampleOrg_userContext, { userContextId: 42 }, '^userContextId=42');
|
||||
|
@ -168,7 +168,9 @@ const mozilla::Module::ContractIDEntry kDocShellContracts[] = {
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "neterror", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "networking", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "newaddon", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
#ifdef NIGHTLY_BUILD
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "performance", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
#endif
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "plugins", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "serviceworkers", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "srcdoc", &kNS_ABOUT_REDIRECTOR_MODULE_CID },
|
||||
|
@ -249,6 +249,7 @@ public:
|
||||
bool CanBeFormatted();
|
||||
bool CanBeShared();
|
||||
bool IsRemovable();
|
||||
bool LowDiskSpace();
|
||||
bool Default();
|
||||
void GetStorageName(nsAString& aStorageName);
|
||||
|
||||
|
@ -72,6 +72,7 @@ DeviceStorageStatics::InitializeDirs()
|
||||
DeviceStorageStatics::DeviceStorageStatics()
|
||||
: mInitialized(false)
|
||||
, mPromptTesting(false)
|
||||
, mLowDiskSpace(false)
|
||||
{
|
||||
DS_LOG_INFO("");
|
||||
}
|
||||
@ -358,6 +359,16 @@ DeviceStorageStatics::IsPromptTesting()
|
||||
return sInstance->mPromptTesting;
|
||||
}
|
||||
|
||||
/* static */ bool
|
||||
DeviceStorageStatics::LowDiskSpace()
|
||||
{
|
||||
StaticMutexAutoLock lock(sMutex);
|
||||
if (NS_WARN_IF(!sInstance)) {
|
||||
return false;
|
||||
}
|
||||
return sInstance->mLowDiskSpace;
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
DeviceStorageStatics::GetWritableName(nsString& aName)
|
||||
{
|
||||
@ -605,27 +616,29 @@ DeviceStorageStatics::Observe(nsISupports* aSubject,
|
||||
}
|
||||
|
||||
if (!strcmp(aTopic, kDiskSpaceWatcher)) {
|
||||
// 'disk-space-watcher' notifications are sent when there is a modification
|
||||
// of a file in a specific location while a low device storage situation
|
||||
// exists or after recovery of a low storage situation. For Firefox OS,
|
||||
// these notifications are specific for apps storage.
|
||||
bool lowDiskSpace = false;
|
||||
if (!NS_strcmp(aData, MOZ_UTF16("full"))) {
|
||||
lowDiskSpace = true;
|
||||
} else if (NS_strcmp(aData, MOZ_UTF16("free"))) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
StaticMutexAutoLock lock(sMutex);
|
||||
if (NS_WARN_IF(!sInstance)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// 'disk-space-watcher' notifications are sent when there is a modification
|
||||
// of a file in a specific location while a low device storage situation
|
||||
// exists or after recovery of a low storage situation. For Firefox OS,
|
||||
// these notifications are specific for apps storage.
|
||||
if (!NS_strcmp(aData, MOZ_UTF16("full"))) {
|
||||
sInstance->mLowDiskSpace = true;
|
||||
} else if (!NS_strcmp(aData, MOZ_UTF16("free"))) {
|
||||
sInstance->mLowDiskSpace = false;
|
||||
} else {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
||||
uint32_t i = mListeners.Length();
|
||||
DS_LOG_INFO("disk space %d (%u)", lowDiskSpace, i);
|
||||
DS_LOG_INFO("disk space %d (%u)", sInstance->mLowDiskSpace, i);
|
||||
while (i > 0) {
|
||||
--i;
|
||||
mListeners[i]->OnDiskSpaceWatcher(lowDiskSpace);
|
||||
mListeners[i]->OnDiskSpaceWatcher(sInstance->mLowDiskSpace);
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ public:
|
||||
static void AddListener(nsDOMDeviceStorage* aListener);
|
||||
static void RemoveListener(nsDOMDeviceStorage* aListener);
|
||||
|
||||
static bool LowDiskSpace();
|
||||
static bool IsPromptTesting();
|
||||
static void GetWritableName(nsString& aName);
|
||||
static void SetWritableName(const nsAString& aName);
|
||||
@ -92,6 +93,7 @@ private:
|
||||
|
||||
bool mInitialized;
|
||||
bool mPromptTesting;
|
||||
bool mLowDiskSpace;
|
||||
nsString mWritableName;
|
||||
|
||||
static StaticRefPtr<DeviceStorageStatics> sInstance;
|
||||
|
@ -3374,6 +3374,12 @@ nsDOMDeviceStorage::IsRemovable()
|
||||
return mIsRemovable;
|
||||
}
|
||||
|
||||
bool
|
||||
nsDOMDeviceStorage::LowDiskSpace()
|
||||
{
|
||||
return DeviceStorageStatics::LowDiskSpace();
|
||||
}
|
||||
|
||||
already_AddRefed<Promise>
|
||||
nsDOMDeviceStorage::GetRoot(ErrorResult& aRv)
|
||||
{
|
||||
|
@ -42,6 +42,7 @@
|
||||
#include "WorkerRunnable.h"
|
||||
#include "WorkerScope.h"
|
||||
#include "Workers.h"
|
||||
#include "FetchUtil.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
@ -909,53 +910,6 @@ ExtractByteStreamFromBody(const ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUS
|
||||
}
|
||||
|
||||
namespace {
|
||||
class StreamDecoder final
|
||||
{
|
||||
nsCOMPtr<nsIUnicodeDecoder> mDecoder;
|
||||
nsString mDecoded;
|
||||
|
||||
public:
|
||||
StreamDecoder()
|
||||
: mDecoder(EncodingUtils::DecoderForEncoding("UTF-8"))
|
||||
{
|
||||
MOZ_ASSERT(mDecoder);
|
||||
}
|
||||
|
||||
nsresult
|
||||
AppendText(const char* aSrcBuffer, uint32_t aSrcBufferLen)
|
||||
{
|
||||
int32_t destBufferLen;
|
||||
nsresult rv =
|
||||
mDecoder->GetMaxLength(aSrcBuffer, aSrcBufferLen, &destBufferLen);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (!mDecoded.SetCapacity(mDecoded.Length() + destBufferLen, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
char16_t* destBuffer = mDecoded.BeginWriting() + mDecoded.Length();
|
||||
int32_t totalChars = mDecoded.Length();
|
||||
|
||||
int32_t srcLen = (int32_t) aSrcBufferLen;
|
||||
int32_t outLen = destBufferLen;
|
||||
rv = mDecoder->Convert(aSrcBuffer, &srcLen, destBuffer, &outLen);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
|
||||
totalChars += outLen;
|
||||
mDecoded.SetLength(totalChars);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsString&
|
||||
GetText()
|
||||
{
|
||||
return mDecoded;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Called on successfully reading the complete stream.
|
||||
*/
|
||||
@ -1436,128 +1390,81 @@ FetchBody<Derived>::ContinueConsumeBody(nsresult aStatus, uint32_t aResultLength
|
||||
jsapi.Init(DerivedClass()->GetParentObject());
|
||||
JSContext* cx = jsapi.cx();
|
||||
|
||||
ErrorResult error;
|
||||
|
||||
switch (mConsumeType) {
|
||||
case CONSUME_ARRAYBUFFER: {
|
||||
JS::Rooted<JSObject*> arrayBuffer(cx);
|
||||
arrayBuffer = JS_NewArrayBufferWithContents(cx, aResultLength, reinterpret_cast<void *>(aResult));
|
||||
if (!arrayBuffer) {
|
||||
JS_ClearPendingException(cx);
|
||||
localPromise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
|
||||
NS_WARNING("OUT OF MEMORY");
|
||||
return;
|
||||
}
|
||||
FetchUtil::ConsumeArrayBuffer(cx, &arrayBuffer, aResultLength, aResult,
|
||||
error);
|
||||
|
||||
JS::Rooted<JS::Value> val(cx);
|
||||
val.setObjectOrNull(arrayBuffer);
|
||||
localPromise->MaybeResolve(cx, val);
|
||||
// ArrayBuffer takes over ownership.
|
||||
autoFree.Reset();
|
||||
return;
|
||||
error.WouldReportJSException();
|
||||
if (!error.Failed()) {
|
||||
JS::Rooted<JS::Value> val(cx);
|
||||
val.setObjectOrNull(arrayBuffer);
|
||||
|
||||
localPromise->MaybeResolve(cx, val);
|
||||
// ArrayBuffer takes over ownership.
|
||||
autoFree.Reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONSUME_BLOB: {
|
||||
nsRefPtr<dom::Blob> blob =
|
||||
Blob::CreateMemoryBlob(DerivedClass()->GetParentObject(),
|
||||
reinterpret_cast<void *>(aResult), aResultLength,
|
||||
NS_ConvertUTF8toUTF16(mMimeType));
|
||||
|
||||
if (!blob) {
|
||||
localPromise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
|
||||
return;
|
||||
nsRefPtr<dom::Blob> blob = FetchUtil::ConsumeBlob(
|
||||
DerivedClass()->GetParentObject(), NS_ConvertUTF8toUTF16(mMimeType),
|
||||
aResultLength, aResult, error);
|
||||
error.WouldReportJSException();
|
||||
if (!error.Failed()) {
|
||||
localPromise->MaybeResolve(blob);
|
||||
// File takes over ownership.
|
||||
autoFree.Reset();
|
||||
}
|
||||
|
||||
localPromise->MaybeResolve(blob);
|
||||
// File takes over ownership.
|
||||
autoFree.Reset();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
case CONSUME_FORMDATA: {
|
||||
nsCString data;
|
||||
data.Adopt(reinterpret_cast<char*>(aResult), aResultLength);
|
||||
autoFree.Reset();
|
||||
|
||||
NS_NAMED_LITERAL_CSTRING(formDataMimeType, "multipart/form-data");
|
||||
|
||||
// Allow semicolon separated boundary/encoding suffix like multipart/form-data; boundary=
|
||||
// but disallow multipart/form-datafoobar.
|
||||
bool isValidFormDataMimeType = StringBeginsWith(mMimeType, formDataMimeType);
|
||||
|
||||
if (isValidFormDataMimeType && mMimeType.Length() > formDataMimeType.Length()) {
|
||||
isValidFormDataMimeType = mMimeType[formDataMimeType.Length()] == ';';
|
||||
}
|
||||
|
||||
if (isValidFormDataMimeType) {
|
||||
FormDataParser parser(mMimeType, data, DerivedClass()->GetParentObject());
|
||||
if (!parser.Parse()) {
|
||||
ErrorResult result;
|
||||
result.ThrowTypeError(MSG_BAD_FORMDATA);
|
||||
localPromise->MaybeReject(result);
|
||||
return;
|
||||
}
|
||||
|
||||
nsRefPtr<nsFormData> fd = parser.FormData();
|
||||
MOZ_ASSERT(fd);
|
||||
nsRefPtr<nsFormData> fd = FetchUtil::ConsumeFormData(
|
||||
DerivedClass()->GetParentObject(),
|
||||
mMimeType, data, error);
|
||||
if (!error.Failed()) {
|
||||
localPromise->MaybeResolve(fd);
|
||||
} else {
|
||||
NS_NAMED_LITERAL_CSTRING(urlDataMimeType, "application/x-www-form-urlencoded");
|
||||
bool isValidUrlEncodedMimeType = StringBeginsWith(mMimeType, urlDataMimeType);
|
||||
|
||||
if (isValidUrlEncodedMimeType && mMimeType.Length() > urlDataMimeType.Length()) {
|
||||
isValidUrlEncodedMimeType = mMimeType[urlDataMimeType.Length()] == ';';
|
||||
}
|
||||
|
||||
if (isValidUrlEncodedMimeType) {
|
||||
URLParams params;
|
||||
params.ParseInput(data);
|
||||
|
||||
nsRefPtr<nsFormData> fd = new nsFormData(DerivedClass()->GetParentObject());
|
||||
FillFormIterator iterator(fd);
|
||||
DebugOnly<bool> status = params.ForEach(iterator);
|
||||
MOZ_ASSERT(status);
|
||||
|
||||
localPromise->MaybeResolve(fd);
|
||||
} else {
|
||||
ErrorResult result;
|
||||
result.ThrowTypeError(MSG_BAD_FORMDATA);
|
||||
localPromise->MaybeReject(result);
|
||||
}
|
||||
}
|
||||
return;
|
||||
break;
|
||||
}
|
||||
case CONSUME_TEXT:
|
||||
// fall through handles early exit.
|
||||
case CONSUME_JSON: {
|
||||
StreamDecoder decoder;
|
||||
decoder.AppendText(reinterpret_cast<char*>(aResult), aResultLength);
|
||||
|
||||
nsString& decoded = decoder.GetText();
|
||||
if (mConsumeType == CONSUME_TEXT) {
|
||||
localPromise->MaybeResolve(decoded);
|
||||
return;
|
||||
}
|
||||
|
||||
AutoForceSetExceptionOnContext forceExn(cx);
|
||||
JS::Rooted<JS::Value> json(cx);
|
||||
if (!JS_ParseJSON(cx, decoded.get(), decoded.Length(), &json)) {
|
||||
if (!JS_IsExceptionPending(cx)) {
|
||||
localPromise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
|
||||
return;
|
||||
nsString decoded;
|
||||
if (NS_SUCCEEDED(FetchUtil::ConsumeText(aResultLength, aResult, decoded))) {
|
||||
if (mConsumeType == CONSUME_TEXT) {
|
||||
localPromise->MaybeResolve(decoded);
|
||||
} else {
|
||||
JS::Rooted<JS::Value> json(cx);
|
||||
FetchUtil::ConsumeJson(cx, &json, decoded, error);
|
||||
if (!error.Failed()) {
|
||||
localPromise->MaybeResolve(cx, json);
|
||||
}
|
||||
}
|
||||
|
||||
JS::Rooted<JS::Value> exn(cx);
|
||||
DebugOnly<bool> gotException = JS_GetPendingException(cx, &exn);
|
||||
MOZ_ASSERT(gotException);
|
||||
|
||||
JS_ClearPendingException(cx);
|
||||
localPromise->MaybeReject(cx, exn);
|
||||
return;
|
||||
}
|
||||
|
||||
localPromise->MaybeResolve(cx, json);
|
||||
return;
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
NS_NOTREACHED("Unexpected consume body type");
|
||||
}
|
||||
|
||||
NS_NOTREACHED("Unexpected consume body type");
|
||||
error.WouldReportJSException();
|
||||
if (error.Failed()) {
|
||||
if (error.IsJSException()) {
|
||||
JS::Rooted<JS::Value> exn(cx);
|
||||
error.StealJSException(cx, &exn);
|
||||
localPromise->MaybeReject(cx, exn);
|
||||
} else {
|
||||
localPromise->MaybeReject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <class Derived>
|
||||
@ -1622,5 +1529,6 @@ FetchBody<Request>::SetMimeType();
|
||||
template
|
||||
void
|
||||
FetchBody<Response>::SetMimeType();
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
@ -1,10 +1,63 @@
|
||||
#include "FetchUtil.h"
|
||||
|
||||
#include "nsError.h"
|
||||
#include "nsIUnicodeDecoder.h"
|
||||
#include "nsString.h"
|
||||
|
||||
#include "mozilla/dom/EncodingUtils.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
namespace {
|
||||
class StreamDecoder final
|
||||
{
|
||||
nsCOMPtr<nsIUnicodeDecoder> mDecoder;
|
||||
nsString mDecoded;
|
||||
|
||||
public:
|
||||
StreamDecoder()
|
||||
: mDecoder(EncodingUtils::DecoderForEncoding("UTF-8"))
|
||||
{
|
||||
MOZ_ASSERT(mDecoder);
|
||||
}
|
||||
|
||||
nsresult
|
||||
AppendText(const char* aSrcBuffer, uint32_t aSrcBufferLen)
|
||||
{
|
||||
int32_t destBufferLen;
|
||||
nsresult rv =
|
||||
mDecoder->GetMaxLength(aSrcBuffer, aSrcBufferLen, &destBufferLen);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (!mDecoded.SetCapacity(mDecoded.Length() + destBufferLen, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
char16_t* destBuffer = mDecoded.BeginWriting() + mDecoded.Length();
|
||||
int32_t totalChars = mDecoded.Length();
|
||||
|
||||
int32_t srcLen = (int32_t) aSrcBufferLen;
|
||||
int32_t outLen = destBufferLen;
|
||||
rv = mDecoder->Convert(aSrcBuffer, &srcLen, destBuffer, &outLen);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
|
||||
totalChars += outLen;
|
||||
mDecoded.SetLength(totalChars);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsString&
|
||||
GetText()
|
||||
{
|
||||
return mDecoded;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// static
|
||||
nsresult
|
||||
FetchUtil::GetValidRequestMethod(const nsACString& aMethod, nsCString& outMethod)
|
||||
@ -33,5 +86,133 @@ FetchUtil::GetValidRequestMethod(const nsACString& aMethod, nsCString& outMethod
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// static
|
||||
void
|
||||
FetchUtil::ConsumeArrayBuffer(JSContext* aCx,
|
||||
JS::MutableHandle<JSObject*> aValue,
|
||||
uint32_t aInputLength, uint8_t* aInput,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
JS::Rooted<JSObject*> arrayBuffer(aCx);
|
||||
arrayBuffer = JS_NewArrayBufferWithContents(aCx, aInputLength,
|
||||
reinterpret_cast<void *>(aInput));
|
||||
if (!arrayBuffer) {
|
||||
JS_ClearPendingException(aCx);
|
||||
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
||||
return;
|
||||
}
|
||||
aValue.set(arrayBuffer);
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<Blob>
|
||||
FetchUtil::ConsumeBlob(nsISupports* aParent, const nsString& aMimeType,
|
||||
uint32_t aInputLength, uint8_t* aInput,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
nsRefPtr<Blob> blob =
|
||||
Blob::CreateMemoryBlob(aParent,
|
||||
reinterpret_cast<void *>(aInput), aInputLength,
|
||||
aMimeType);
|
||||
|
||||
if (!blob) {
|
||||
aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR);
|
||||
return nullptr;
|
||||
}
|
||||
return blob.forget();
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<nsFormData>
|
||||
FetchUtil::ConsumeFormData(nsIGlobalObject* aParent, const nsCString& aMimeType,
|
||||
const nsCString& aStr, ErrorResult& aRv)
|
||||
{
|
||||
NS_NAMED_LITERAL_CSTRING(formDataMimeType, "multipart/form-data");
|
||||
|
||||
// Allow semicolon separated boundary/encoding suffix like multipart/form-data; boundary=
|
||||
// but disallow multipart/form-datafoobar.
|
||||
bool isValidFormDataMimeType = StringBeginsWith(aMimeType, formDataMimeType);
|
||||
|
||||
if (isValidFormDataMimeType && aMimeType.Length() > formDataMimeType.Length()) {
|
||||
isValidFormDataMimeType = aMimeType[formDataMimeType.Length()] == ';';
|
||||
}
|
||||
|
||||
if (isValidFormDataMimeType) {
|
||||
FormDataParser parser(aMimeType, aStr, aParent);
|
||||
if (!parser.Parse()) {
|
||||
aRv.ThrowTypeError(MSG_BAD_FORMDATA);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsRefPtr<nsFormData> fd = parser.FormData();
|
||||
MOZ_ASSERT(fd);
|
||||
return fd.forget();
|
||||
}
|
||||
|
||||
NS_NAMED_LITERAL_CSTRING(urlDataMimeType, "application/x-www-form-urlencoded");
|
||||
bool isValidUrlEncodedMimeType = StringBeginsWith(aMimeType, urlDataMimeType);
|
||||
|
||||
if (isValidUrlEncodedMimeType && aMimeType.Length() > urlDataMimeType.Length()) {
|
||||
isValidUrlEncodedMimeType = aMimeType[urlDataMimeType.Length()] == ';';
|
||||
}
|
||||
|
||||
if (isValidUrlEncodedMimeType) {
|
||||
URLParams params;
|
||||
params.ParseInput(aStr);
|
||||
|
||||
nsRefPtr<nsFormData> fd = new nsFormData(aParent);
|
||||
FillFormIterator iterator(fd);
|
||||
DebugOnly<bool> status = params.ForEach(iterator);
|
||||
MOZ_ASSERT(status);
|
||||
|
||||
return fd.forget();
|
||||
}
|
||||
|
||||
aRv.ThrowTypeError(MSG_BAD_FORMDATA);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// static
|
||||
nsresult
|
||||
FetchUtil::ConsumeText(uint32_t aInputLength, uint8_t* aInput,
|
||||
nsString& aText)
|
||||
{
|
||||
StreamDecoder decoder;
|
||||
nsresult rv = decoder.AppendText(reinterpret_cast<char*>(aInput),
|
||||
aInputLength);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
aText = decoder.GetText();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// static
|
||||
void
|
||||
FetchUtil::ConsumeJson(JSContext* aCx, JS::MutableHandle<JS::Value> aValue,
|
||||
const nsString& aStr, ErrorResult& aRv)
|
||||
{
|
||||
aRv.MightThrowJSException();
|
||||
|
||||
AutoForceSetExceptionOnContext forceExn(aCx);
|
||||
JS::Rooted<JS::Value> json(aCx);
|
||||
if (!JS_ParseJSON(aCx, aStr.get(), aStr.Length(), &json)) {
|
||||
if (!JS_IsExceptionPending(aCx)) {
|
||||
aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
JS::Rooted<JS::Value> exn(aCx);
|
||||
DebugOnly<bool> gotException = JS_GetPendingException(aCx, &exn);
|
||||
MOZ_ASSERT(gotException);
|
||||
|
||||
JS_ClearPendingException(aCx);
|
||||
aRv.ThrowJSException(aCx, exn);
|
||||
return;
|
||||
}
|
||||
|
||||
aValue.set(json);
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
@ -3,6 +3,10 @@
|
||||
|
||||
#include "nsString.h"
|
||||
#include "nsError.h"
|
||||
#include "nsFormData.h"
|
||||
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/dom/File.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
@ -21,6 +25,46 @@ public:
|
||||
*/
|
||||
static nsresult
|
||||
GetValidRequestMethod(const nsACString& aMethod, nsCString& outMethod);
|
||||
|
||||
/**
|
||||
* Creates an array buffer from an array, assigning the result to |aValue|.
|
||||
* The array buffer takes ownership of |aInput|, which must be allocated
|
||||
* by |malloc|.
|
||||
*/
|
||||
static void
|
||||
ConsumeArrayBuffer(JSContext* aCx, JS::MutableHandle<JSObject*> aValue,
|
||||
uint32_t aInputLength, uint8_t* aInput, ErrorResult& aRv);
|
||||
|
||||
/**
|
||||
* Creates an in-memory blob from an array. The blob takes ownership of
|
||||
* |aInput|, which must be allocated by |malloc|.
|
||||
*/
|
||||
static already_AddRefed<Blob>
|
||||
ConsumeBlob(nsISupports* aParent, const nsString& aMimeType,
|
||||
uint32_t aInputLength, uint8_t* aInput, ErrorResult& aRv);
|
||||
|
||||
/**
|
||||
* Creates a form data object from a UTF-8 encoded |aStr|. Returns |nullptr|
|
||||
* and sets |aRv| to MSG_BAD_FORMDATA if |aStr| contains invalid data.
|
||||
*/
|
||||
static already_AddRefed<nsFormData>
|
||||
ConsumeFormData(nsIGlobalObject* aParent, const nsCString& aMimeType,
|
||||
const nsCString& aStr, ErrorResult& aRv);
|
||||
|
||||
/**
|
||||
* UTF-8 decodes |aInput| into |aText|. The caller may free |aInput|
|
||||
* once this method returns.
|
||||
*/
|
||||
static nsresult
|
||||
ConsumeText(uint32_t aInputLength, uint8_t* aInput, nsString& aText);
|
||||
|
||||
/**
|
||||
* Parses a UTF-8 encoded |aStr| as JSON, assigning the result to |aValue|.
|
||||
* Sets |aRv| to a syntax error if |aStr| contains invalid data.
|
||||
*/
|
||||
static void
|
||||
ConsumeJson(JSContext* aCx, JS::MutableHandle<JS::Value> aValue,
|
||||
const nsString& aStr, ErrorResult& aRv);
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
@ -34,7 +34,7 @@ interface nsIServiceWorkerInfo : nsISupports
|
||||
readonly attribute DOMString waitingCacheName;
|
||||
};
|
||||
|
||||
[scriptable, builtinclass, uuid(8d80dd18-597b-4378-b41e-768bfe48dd4f)]
|
||||
[scriptable, builtinclass, uuid(471b2d5d-64c3-4dea-bde1-219853dcaac8)]
|
||||
interface nsIServiceWorkerManager : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -141,9 +141,10 @@ interface nsIServiceWorkerManager : nsISupports
|
||||
in AString aIcon,
|
||||
in AString aData,
|
||||
in AString aBehavior);
|
||||
void sendPushEvent(in ACString aOriginAttributes,
|
||||
in ACString aScope,
|
||||
in DOMString aData);
|
||||
[optional_argc] void sendPushEvent(in ACString aOriginAttributes,
|
||||
in ACString aScope,
|
||||
[optional] in uint32_t aDataLength,
|
||||
[optional, array, size_is(aDataLength)] in uint8_t aDataBytes);
|
||||
void sendPushSubscriptionChangeEvent(in ACString aOriginAttributes,
|
||||
in ACString scope);
|
||||
|
||||
|
@ -33,6 +33,7 @@
|
||||
29 = Unbalanced curly brace.
|
||||
30 = Creating an element with an invalid QName.
|
||||
31 = Variable binding shadows variable binding within the same template.
|
||||
32 = Call to the key function not allowed.
|
||||
|
||||
LoadingError = Error loading stylesheet: %S
|
||||
TransformError = Error during XSLT transformation: %S
|
||||
|
@ -9,11 +9,43 @@ const Cu = Components.utils;
|
||||
|
||||
Cu.importGlobalProperties(['crypto']);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ['PushServiceHttp2Crypto', 'concatArray'];
|
||||
this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
|
||||
'getEncryptionKeyParams', 'getEncryptionParams',
|
||||
'base64UrlDecode'];
|
||||
|
||||
var ENCRYPT_INFO = new TextEncoder('utf-8').encode('Content-Encoding: aesgcm128');
|
||||
var NONCE_INFO = new TextEncoder('utf-8').encode('Content-Encoding: nonce');
|
||||
|
||||
this.getEncryptionKeyParams = function(encryptKeyField) {
|
||||
var params = encryptKeyField.split(',');
|
||||
return params.reduce((m, p) => {
|
||||
var pmap = p.split(';').reduce(parseHeaderFieldParams, {});
|
||||
if (pmap.keyid && pmap.dh) {
|
||||
m[pmap.keyid] = pmap.dh;
|
||||
}
|
||||
return m;
|
||||
}, {});
|
||||
};
|
||||
|
||||
this.getEncryptionParams = function(encryptField) {
|
||||
var p = encryptField.split(',', 1)[0];
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
return p.split(';').reduce(parseHeaderFieldParams, {});
|
||||
};
|
||||
|
||||
var parseHeaderFieldParams = (m, v) => {
|
||||
var i = v.indexOf('=');
|
||||
if (i >= 0) {
|
||||
// A quoted string with internal quotes is invalid for all the possible
|
||||
// values of this header field.
|
||||
m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
|
||||
.replace(/^"(.*)"$/, '$1');
|
||||
}
|
||||
return m;
|
||||
};
|
||||
|
||||
function chunkArray(array, size) {
|
||||
var start = array.byteOffset || 0;
|
||||
array = array.buffer || array;
|
||||
@ -29,7 +61,7 @@ function chunkArray(array, size) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function base64UrlDecode(s) {
|
||||
this.base64UrlDecode = function(s) {
|
||||
s = s.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
// Replace padding if it was stripped by the sender.
|
||||
@ -55,7 +87,7 @@ function base64UrlDecode(s) {
|
||||
array[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
};
|
||||
|
||||
this.concatArray = function(arrays) {
|
||||
var size = arrays.reduce((total, a) => total + a.byteLength, 0);
|
||||
@ -108,7 +140,7 @@ function generateNonce(base, index) {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
this.PushServiceHttp2Crypto = {
|
||||
this.PushCrypto = {
|
||||
|
||||
generateKeys: function() {
|
||||
return crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256'},
|
@ -44,6 +44,8 @@ function PushRecord(props) {
|
||||
this.originAttributes = props.originAttributes;
|
||||
this.pushCount = props.pushCount || 0;
|
||||
this.lastPush = props.lastPush || 0;
|
||||
this.p256dhPublicKey = props.p256dhPublicKey;
|
||||
this.p256dhPrivateKey = props.p256dhPrivateKey;
|
||||
this.setQuota(props.quota);
|
||||
this.ctime = (typeof props.ctime === "number") ? props.ctime : 0;
|
||||
}
|
||||
@ -191,12 +193,14 @@ PushRecord.prototype = {
|
||||
pushEndpoint: this.pushEndpoint,
|
||||
lastPush: this.lastPush,
|
||||
pushCount: this.pushCount,
|
||||
p256dhKey: this.p256dhPublicKey,
|
||||
};
|
||||
},
|
||||
|
||||
toRegister() {
|
||||
return {
|
||||
pushEndpoint: this.pushEndpoint,
|
||||
p256dhKey: this.p256dhPublicKey,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
@ -28,6 +28,7 @@ Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
const {PushServiceWebSocket} = Cu.import("resource://gre/modules/PushServiceWebSocket.jsm");
|
||||
const {PushServiceHttp2} = Cu.import("resource://gre/modules/PushServiceHttp2.jsm");
|
||||
const {PushCrypto} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
|
||||
// Currently supported protocols: WebSocket.
|
||||
const CONNECTION_PROTOCOLS = [PushServiceWebSocket, PushServiceHttp2];
|
||||
@ -710,11 +711,11 @@ this.PushService = {
|
||||
_notifyAllAppsRegister: function() {
|
||||
debug("notifyAllAppsRegister()");
|
||||
// records are objects describing the registration as stored in IndexedDB.
|
||||
return this._db.getAllUnexpired().then(records =>
|
||||
records.forEach(record =>
|
||||
this._notifySubscriptionChangeObservers(record)
|
||||
)
|
||||
);
|
||||
return this._db.getAllUnexpired().then(records => {
|
||||
records.forEach(record => {
|
||||
this._notifySubscriptionChangeObservers(record);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
dropRegistrationAndNotifyApp: function(aKeyId) {
|
||||
@ -730,9 +731,31 @@ this.PushService = {
|
||||
.then(record => this._notifySubscriptionChangeObservers(record));
|
||||
},
|
||||
|
||||
ensureP256dhKey: function(record) {
|
||||
if (record.p256dhPublicKey && record.p256dhPrivateKey) {
|
||||
return Promise.resolve(record);
|
||||
}
|
||||
// We do not have a encryption key. so we need to generate it. This
|
||||
// is only going to happen on db upgrade from version 4 to higher.
|
||||
return PushCrypto.generateKeys()
|
||||
.then(exportedKeys => {
|
||||
return this.updateRecordAndNotifyApp(record.keyID, record => {
|
||||
record.p256dhPublicKey = exportedKeys[0];
|
||||
record.p256dhPrivateKey = exportedKeys[1];
|
||||
return record;
|
||||
});
|
||||
}, error => {
|
||||
return this.dropRegistrationAndNotifyApp(record.keyID).then(
|
||||
() => Promise.reject(error));
|
||||
});
|
||||
},
|
||||
|
||||
updateRecordAndNotifyApp: function(aKeyID, aUpdateFunc) {
|
||||
return this._db.update(aKeyID, aUpdateFunc)
|
||||
.then(record => this._notifySubscriptionChangeObservers(record));
|
||||
.then(record => {
|
||||
this._notifySubscriptionChangeObservers(record);
|
||||
return record;
|
||||
});
|
||||
},
|
||||
|
||||
_recordDidNotNotify: function(reason) {
|
||||
@ -749,13 +772,14 @@ this.PushService = {
|
||||
*
|
||||
* @param {String} keyID The push registration ID.
|
||||
* @param {String} message The message contents.
|
||||
* @param {Object} cryptoParams The message encryption settings.
|
||||
* @param {Function} updateFunc A function that receives the existing
|
||||
* registration record as its argument, and returns a new record. If the
|
||||
* function returns `null` or `undefined`, the record will not be updated.
|
||||
* `PushServiceWebSocket` uses this to drop incoming updates with older
|
||||
* versions.
|
||||
*/
|
||||
receivedPushMessage: function(keyID, message, updateFunc) {
|
||||
receivedPushMessage: function(keyID, message, cryptoParams, updateFunc) {
|
||||
debug("receivedPushMessage()");
|
||||
Services.telemetry.getHistogramById("PUSH_API_NOTIFICATION_RECEIVED").add();
|
||||
|
||||
@ -779,10 +803,11 @@ this.PushService = {
|
||||
this._recordDidNotNotify(kDROP_NOTIFICATION_REASON_NO_VERSION_INCREMENT);
|
||||
return null;
|
||||
}
|
||||
// FIXME(nsm): WHY IS expired checked here but then also checked in the next case?
|
||||
// Because `unregister` is advisory only, we can still receive messages
|
||||
// for stale Simple Push registrations from the server. To work around
|
||||
// this, we check if the record has expired before *and* after updating
|
||||
// the quota.
|
||||
if (newRecord.isExpired()) {
|
||||
// Because `unregister` is advisory only, we can still receive messages
|
||||
// for stale registrations from the server.
|
||||
debug("receivedPushMessage: Ignoring update for expired key ID " + keyID);
|
||||
return null;
|
||||
}
|
||||
@ -794,20 +819,33 @@ this.PushService = {
|
||||
if (!record) {
|
||||
return notified;
|
||||
}
|
||||
|
||||
if (shouldNotify) {
|
||||
notified = this._notifyApp(record, message);
|
||||
let decodedPromise;
|
||||
if (cryptoParams) {
|
||||
decodedPromise = PushCrypto.decodeMsg(
|
||||
message,
|
||||
record.p256dhPrivateKey,
|
||||
cryptoParams.dh,
|
||||
cryptoParams.salt,
|
||||
cryptoParams.rs
|
||||
);
|
||||
} else {
|
||||
decodedPromise = Promise.resolve(null);
|
||||
}
|
||||
if (record.isExpired()) {
|
||||
this._recordDidNotNotify(kDROP_NOTIFICATION_REASON_EXPIRED);
|
||||
// Drop the registration in the background. If the user returns to the
|
||||
// site, the service worker will be notified on the next `idle-daily`
|
||||
// event.
|
||||
this._sendUnregister(record).catch(error => {
|
||||
debug("receivedPushMessage: Unregister error: " + error);
|
||||
});
|
||||
}
|
||||
return notified;
|
||||
return decodedPromise.then(message => {
|
||||
if (shouldNotify) {
|
||||
notified = this._notifyApp(record, message);
|
||||
}
|
||||
if (record.isExpired()) {
|
||||
this._recordDidNotNotify(kDROP_NOTIFICATION_REASON_EXPIRED);
|
||||
// Drop the registration in the background. If the user returns to the
|
||||
// site, the service worker will be notified on the next `idle-daily`
|
||||
// event.
|
||||
this._sendUnregister(record).catch(error => {
|
||||
debug("receivedPushMessage: Unregister error: " + error);
|
||||
});
|
||||
}
|
||||
return notified;
|
||||
});
|
||||
}).catch(error => {
|
||||
debug("receivedPushMessage: Error notifying app: " + error);
|
||||
});
|
||||
@ -827,7 +865,16 @@ this.PushService = {
|
||||
.createInstance(Ci.nsIPushObserverNotification);
|
||||
notification.pushEndpoint = aPushRecord.pushEndpoint;
|
||||
notification.version = aPushRecord.version;
|
||||
notification.data = message;
|
||||
|
||||
let payload = ArrayBuffer.isView(message) ?
|
||||
new Uint8Array(message.buffer) : message;
|
||||
if (payload) {
|
||||
notification.data = "";
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
notification.data += String.fromCharCode(payload[i]);
|
||||
}
|
||||
}
|
||||
|
||||
notification.lastPush = aPushRecord.lastPush;
|
||||
notification.pushCount = aPushRecord.pushCount;
|
||||
|
||||
@ -843,9 +890,8 @@ this.PushService = {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO data.
|
||||
let data = {
|
||||
payload: message,
|
||||
payload: payload,
|
||||
originAttributes: aPushRecord.originAttributes,
|
||||
scope: aPushRecord.scope
|
||||
};
|
||||
|
@ -23,8 +23,12 @@ var processType = Cc["@mozilla.org/xre/app-info;1"]
|
||||
var isParent = processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
||||
|
||||
Services.cpmm.addMessageListener("push", function (aMessage) {
|
||||
swm.sendPushEvent(aMessage.data.originAttributes,
|
||||
aMessage.data.scope, aMessage.data.payload);
|
||||
let {originAttributes, scope, payload} = aMessage.data;
|
||||
if (payload) {
|
||||
swm.sendPushEvent(originAttributes, scope, payload.length, payload);
|
||||
} else {
|
||||
swm.sendPushEvent(originAttributes, scope);
|
||||
}
|
||||
});
|
||||
|
||||
Services.cpmm.addMessageListener("pushsubscriptionchange", function (aMessage) {
|
||||
|
@ -19,8 +19,12 @@ Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
const {PushServiceHttp2Crypto, concatArray} =
|
||||
Cu.import("resource://gre/modules/PushServiceHttp2Crypto.jsm");
|
||||
const {
|
||||
PushCrypto,
|
||||
concatArray,
|
||||
getEncryptionKeyParams,
|
||||
getEncryptionParams,
|
||||
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushServiceHttp2"];
|
||||
|
||||
@ -176,30 +180,10 @@ PushChannelListener.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
var parseHeaderFieldParams = (m, v) => {
|
||||
var i = v.indexOf('=');
|
||||
if (i >= 0) {
|
||||
// A quoted string with internal quotes is invalid for all the possible
|
||||
// values of this header field.
|
||||
m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
|
||||
.replace(/^"(.*)"$/, '$1');
|
||||
}
|
||||
return m;
|
||||
};
|
||||
|
||||
function encryptKeyFieldParser(aRequest) {
|
||||
try {
|
||||
var encryptKeyField = aRequest.getRequestHeader("Encryption-Key");
|
||||
|
||||
var params = encryptKeyField.split(',');
|
||||
return params.reduce((m, p) => {
|
||||
var pmap = p.split(';').reduce(parseHeaderFieldParams, {});
|
||||
if (pmap.keyid && pmap.dh) {
|
||||
m[pmap.keyid] = pmap.dh;
|
||||
}
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
return getEncryptionKeyParams(encryptKeyField);
|
||||
} catch(e) {
|
||||
// getRequestHeader can throw.
|
||||
return null;
|
||||
@ -208,10 +192,8 @@ function encryptKeyFieldParser(aRequest) {
|
||||
|
||||
function encryptFieldParser(aRequest) {
|
||||
try {
|
||||
return aRequest.getRequestHeader("Encryption")
|
||||
.split(',', 1)[0]
|
||||
.split(';')
|
||||
.reduce(parseHeaderFieldParams, {});
|
||||
var encryptField = aRequest.getRequestHeader("Encryption");
|
||||
return getEncryptionParams(encryptField);
|
||||
} catch(e) {
|
||||
// getRequestHeader can throw.
|
||||
return null;
|
||||
@ -533,7 +515,7 @@ this.PushServiceHttp2 = {
|
||||
retries: 0
|
||||
})
|
||||
.then(result =>
|
||||
PushServiceHttp2Crypto.generateKeys()
|
||||
PushCrypto.generateKeys()
|
||||
.then(exportedKeys => {
|
||||
result.p256dhPublicKey = exportedKeys[0];
|
||||
result.p256dhPrivateKey = exportedKeys[1];
|
||||
@ -724,34 +706,11 @@ this.PushServiceHttp2 = {
|
||||
|
||||
for (let i = 0; i < aSubscriptions.length; i++) {
|
||||
let record = aSubscriptions[i];
|
||||
if (record.p256dhPublicKey && record.p256dhPrivateKey) {
|
||||
this._mainPushService.ensureP256dhKey(record).then(record => {
|
||||
this._startSingleConnection(record);
|
||||
} else {
|
||||
// We do not have a encryption key. so we need to generate it. This
|
||||
// is only going to happen on db upgrade from version 4 to higher.
|
||||
PushServiceHttp2Crypto.generateKeys()
|
||||
.then(exportedKeys => {
|
||||
if (this._mainPushService) {
|
||||
return this._mainPushService
|
||||
.updateRecordAndNotifyApp(record.subscriptionUri, record => {
|
||||
record.p256dhPublicKey = exportedKeys[0];
|
||||
record.p256dhPrivateKey = exportedKeys[1];
|
||||
return record;
|
||||
});
|
||||
}
|
||||
}, error => {
|
||||
record = null;
|
||||
if (this._mainPushService) {
|
||||
this._mainPushService
|
||||
.dropRegistrationAndNotifyApp(record.subscriptionUri);
|
||||
}
|
||||
})
|
||||
.then(_ => {
|
||||
if (record) {
|
||||
this._startSingleConnection(record);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, error => {
|
||||
debug("startConnections: Error updating record " + record.keyID);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -875,22 +834,16 @@ this.PushServiceHttp2 = {
|
||||
_pushChannelOnStop: function(aUri, aAckUri, aMessage, dh, salt, rs) {
|
||||
debug("pushChannelOnStop() ");
|
||||
|
||||
this._mainPushService.getByKeyID(aUri)
|
||||
.then(aPushRecord =>
|
||||
PushServiceHttp2Crypto.decodeMsg(aMessage, aPushRecord.p256dhPrivateKey,
|
||||
dh, salt, rs)
|
||||
.then(msg => {
|
||||
var msgString = '';
|
||||
for (var i=0; i<msg.length; i++) {
|
||||
msgString += String.fromCharCode(msg[i]);
|
||||
}
|
||||
return this._mainPushService.receivedPushMessage(aUri,
|
||||
msgString,
|
||||
record => {
|
||||
// Always update the stored record.
|
||||
return record;
|
||||
});
|
||||
})
|
||||
let cryptoParams = {
|
||||
dh: dh,
|
||||
salt: salt,
|
||||
rs: rs,
|
||||
};
|
||||
this._mainPushService.receivedPushMessage(
|
||||
aUri, aMessage, cryptoParams, record => {
|
||||
// Always update the stored record.
|
||||
return record;
|
||||
}
|
||||
)
|
||||
.then(_ => this._ackMsgRecv(aAckUri))
|
||||
.catch(err => {
|
||||
@ -907,8 +860,6 @@ function PushRecordHttp2(record) {
|
||||
PushRecord.call(this, record);
|
||||
this.subscriptionUri = record.subscriptionUri;
|
||||
this.pushReceiptEndpoint = record.pushReceiptEndpoint;
|
||||
this.p256dhPublicKey = record.p256dhPublicKey;
|
||||
this.p256dhPrivateKey = record.p256dhPrivateKey;
|
||||
}
|
||||
|
||||
PushRecordHttp2.prototype = Object.create(PushRecord.prototype, {
|
||||
@ -922,13 +873,11 @@ PushRecordHttp2.prototype = Object.create(PushRecord.prototype, {
|
||||
PushRecordHttp2.prototype.toRegistration = function() {
|
||||
let registration = PushRecord.prototype.toRegistration.call(this);
|
||||
registration.pushReceiptEndpoint = this.pushReceiptEndpoint;
|
||||
registration.p256dhKey = this.p256dhPublicKey;
|
||||
return registration;
|
||||
};
|
||||
|
||||
PushRecordHttp2.prototype.toRegister = function() {
|
||||
let register = PushRecord.prototype.toRegister.call(this);
|
||||
register.pushReceiptEndpoint = this.pushReceiptEndpoint;
|
||||
register.p256dhKey = this.p256dhPublicKey;
|
||||
return register;
|
||||
};
|
||||
|
@ -12,6 +12,12 @@ const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
||||
const {
|
||||
PushCrypto,
|
||||
base64UrlDecode,
|
||||
getEncryptionKeyParams,
|
||||
getEncryptionParams,
|
||||
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
@ -33,7 +39,7 @@ var threadManager = Cc["@mozilla.org/thread-manager;1"]
|
||||
.getService(Ci.nsIThreadManager);
|
||||
|
||||
const kPUSHWSDB_DB_NAME = "pushapi";
|
||||
const kPUSHWSDB_DB_VERSION = 4; // Change this if the IndexedDB format changes
|
||||
const kPUSHWSDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
|
||||
const kPUSHWSDB_STORE_NAME = "pushapi";
|
||||
|
||||
const kUDP_WAKEUP_WS_STATUS_CODE = 4774; // WebSocket Close status code sent
|
||||
@ -47,6 +53,27 @@ this.EXPORTED_SYMBOLS = ["PushServiceWebSocket"];
|
||||
// Don't modify this, instead set dom.push.debug.
|
||||
var gDebuggingEnabled = true;
|
||||
|
||||
function getCryptoParams(headers) {
|
||||
if (!headers) {
|
||||
return null;
|
||||
}
|
||||
var keymap = getEncryptionKeyParams(headers.encryption_key);
|
||||
if (!keymap) {
|
||||
return null;
|
||||
}
|
||||
var enc = getEncryptionParams(headers.encryption);
|
||||
if (!enc || !enc.keyid) {
|
||||
return null;
|
||||
}
|
||||
var dh = keymap[enc.keyid];
|
||||
var salt = enc.salt;
|
||||
var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
|
||||
if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
|
||||
return null;
|
||||
}
|
||||
return {dh, salt, rs};
|
||||
}
|
||||
|
||||
function debug(s) {
|
||||
if (gDebuggingEnabled) {
|
||||
dump("-*- PushServiceWebSocket.jsm: " + s + "\n");
|
||||
@ -136,11 +163,11 @@ this.PushServiceWebSocket = {
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
|
||||
switch (aTopic) {
|
||||
case "nsPref:changed":
|
||||
if (aData == "dom.push.debug") {
|
||||
gDebuggingEnabled = prefs.get("debug");
|
||||
}
|
||||
break;
|
||||
case "nsPref:changed":
|
||||
if (aData == "dom.push.debug") {
|
||||
gDebuggingEnabled = prefs.get("debug");
|
||||
}
|
||||
break;
|
||||
case "timer-callback":
|
||||
if (aSubject == this._requestTimeoutTimer) {
|
||||
if (Object.keys(this._pendingRequests).length === 0) {
|
||||
@ -265,6 +292,9 @@ this.PushServiceWebSocket = {
|
||||
*/
|
||||
_upperLimit: 0,
|
||||
|
||||
/** Indicates whether the server supports Web Push-style message delivery. */
|
||||
_dataEnabled: false,
|
||||
|
||||
/**
|
||||
* Sends a message to the Push Server through an open websocket.
|
||||
* typeof(msg) shall be an object
|
||||
@ -356,6 +386,8 @@ this.PushServiceWebSocket = {
|
||||
}
|
||||
|
||||
this._mainPushService = null;
|
||||
|
||||
this._dataEnabled = false;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -749,13 +781,28 @@ this.PushServiceWebSocket = {
|
||||
return;
|
||||
}
|
||||
|
||||
function finishHandshake() {
|
||||
this._UAID = reply.uaid;
|
||||
this._currentState = STATE_READY;
|
||||
let notifyRequestQueue = () => {
|
||||
if (this._notifyRequestQueue) {
|
||||
this._notifyRequestQueue();
|
||||
this._notifyRequestQueue = null;
|
||||
}
|
||||
};
|
||||
|
||||
function finishHandshake() {
|
||||
this._UAID = reply.uaid;
|
||||
this._currentState = STATE_READY;
|
||||
this._dataEnabled = !!reply.use_webpush;
|
||||
if (this._dataEnabled) {
|
||||
this._mainPushService.getAllUnexpired().then(records =>
|
||||
Promise.all(records.map(record =>
|
||||
this._mainPushService.ensureP256dhKey(record).catch(error => {
|
||||
debug("finishHandshake: Error updating record " + record.keyID);
|
||||
})
|
||||
))
|
||||
).then(notifyRequestQueue);
|
||||
} else {
|
||||
notifyRequestQueue();
|
||||
}
|
||||
}
|
||||
|
||||
// By this point we've got a UAID from the server that we are ready to
|
||||
@ -821,11 +868,48 @@ this.PushServiceWebSocket = {
|
||||
}
|
||||
},
|
||||
|
||||
_handleDataUpdate: function(update) {
|
||||
let promise;
|
||||
if (typeof update.channelID != "string") {
|
||||
debug("handleDataUpdate: Discarding message without channel ID");
|
||||
return;
|
||||
}
|
||||
if (typeof update.data != "string") {
|
||||
promise = this._mainPushService.receivedPushMessage(
|
||||
update.channelID,
|
||||
null,
|
||||
null,
|
||||
record => record
|
||||
);
|
||||
} else {
|
||||
let params = getCryptoParams(update.headers);
|
||||
if (!params) {
|
||||
debug("handleDataUpdate: Discarding invalid encrypted message");
|
||||
return;
|
||||
}
|
||||
let message = base64UrlDecode(update.data);
|
||||
promise = this._mainPushService.receivedPushMessage(
|
||||
update.channelID,
|
||||
message,
|
||||
params,
|
||||
record => record
|
||||
);
|
||||
}
|
||||
promise.then(() => this._sendAck(update.channelID)).catch(err => {
|
||||
debug("handleDataUpdate: Error delivering message: " + err);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Protocol handler invoked by server message.
|
||||
*/
|
||||
_handleNotificationReply: function(reply) {
|
||||
debug("handleNotificationReply()");
|
||||
if (this._dataEnabled) {
|
||||
this._handleDataUpdate(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof reply.updates !== 'object') {
|
||||
debug("No 'updates' field in response. Type = " + typeof reply.updates);
|
||||
return;
|
||||
@ -902,6 +986,16 @@ this.PushServiceWebSocket = {
|
||||
ctime: Date.now()
|
||||
};
|
||||
this._queueRequest(data);
|
||||
}).then(record => {
|
||||
if (!this._dataEnabled) {
|
||||
return record;
|
||||
}
|
||||
return PushCrypto.generateKeys()
|
||||
.then(([publicKey, privateKey]) => {
|
||||
record.p256dhPublicKey = publicKey;
|
||||
record.p256dhPrivateKey = privateKey;
|
||||
return record;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -961,7 +1055,7 @@ this.PushServiceWebSocket = {
|
||||
_receivedUpdate: function(aChannelID, aLatestVersion) {
|
||||
debug("Updating: " + aChannelID + " -> " + aLatestVersion);
|
||||
|
||||
this._mainPushService.receivedPushMessage(aChannelID, "", record => {
|
||||
this._mainPushService.receivedPushMessage(aChannelID, null, null, record => {
|
||||
if (record.version === null ||
|
||||
record.version < aLatestVersion) {
|
||||
debug("Version changed for " + aChannelID + ": " + aLatestVersion);
|
||||
@ -990,6 +1084,7 @@ this.PushServiceWebSocket = {
|
||||
|
||||
let data = {
|
||||
messageType: "hello",
|
||||
use_webpush: true,
|
||||
};
|
||||
|
||||
if (this._UAID) {
|
||||
|
@ -15,12 +15,12 @@ EXTRA_PP_JS_MODULES += [
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushCrypto.jsm',
|
||||
'PushDB.jsm',
|
||||
'PushRecord.jsm',
|
||||
'PushService.jsm',
|
||||
'PushServiceChildPreload.jsm',
|
||||
'PushServiceHttp2.jsm',
|
||||
'PushServiceHttp2Crypto.jsm',
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
|
@ -10,8 +10,7 @@
|
||||
var p = new Promise(function(res, rej) {
|
||||
navigator.serviceWorker.onmessage = function(e) {
|
||||
if (e.data.type == "finished") {
|
||||
parent.ok(e.data.okay == "yes", "Got a push message.");
|
||||
res(pushSubscription);
|
||||
(e.data.okay == "yes" ? res : rej)(e.data);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ support-files =
|
||||
worker.js
|
||||
push-server.sjs
|
||||
frame.html
|
||||
webpush.js
|
||||
|
||||
[test_has_permissions.html]
|
||||
skip-if = os == "android" || toolkit == "gonk"
|
||||
@ -19,6 +20,8 @@ skip-if = os == "android" || toolkit == "gonk"
|
||||
skip-if = os == "android" || toolkit == "gonk"
|
||||
[test_multiple_register_different_scope.html]
|
||||
skip-if = os == "android" || toolkit == "gonk"
|
||||
[test_data.html]
|
||||
skip-if = os == "android" || toolkit == "gonk"
|
||||
# Disabled for too many intermittent failures (bug 1164432)
|
||||
# [test_try_registering_offline_disabled.html]
|
||||
# skip-if = os == "android" || toolkit == "gonk"
|
||||
|
@ -2,6 +2,15 @@ function debug(str) {
|
||||
// dump("@@@ push-server " + str + "\n");
|
||||
}
|
||||
|
||||
function concatUint8Arrays(arrays, size) {
|
||||
let index = 0;
|
||||
return arrays.reduce((result, a) => {
|
||||
result.set(new Uint8Array(a), index);
|
||||
index += a.byteLength;
|
||||
return result;
|
||||
}, new Uint8Array(size));
|
||||
}
|
||||
|
||||
function handleRequest(request, response)
|
||||
{
|
||||
debug("handling request!");
|
||||
@ -13,8 +22,25 @@ function handleRequest(request, response)
|
||||
debug("params = " + params);
|
||||
|
||||
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
|
||||
xhr.open("PUT", params);
|
||||
xhr.send();
|
||||
xhr.open(request.getHeader("X-Push-Method"), params);
|
||||
|
||||
for (let headers = request.headers; headers.hasMoreElements();) {
|
||||
let header = headers.getNext().QueryInterface(Ci.nsISupportsString).data;
|
||||
if (header.toLowerCase() != "x-push-server") {
|
||||
xhr.setRequestHeader(header, request.getHeader(header));
|
||||
}
|
||||
}
|
||||
|
||||
let bodyStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
|
||||
bodyStream.setInputStream(request.bodyInputStream);
|
||||
let size = 0;
|
||||
let data = [];
|
||||
for (let available; available = bodyStream.available();) {
|
||||
data.push(bodyStream.readByteArray(available));
|
||||
size += available;
|
||||
}
|
||||
xhr.send(concatUint8Arrays(data, size));
|
||||
|
||||
xhr.onload = function(e) {
|
||||
debug("xhr : " + this.status);
|
||||
}
|
||||
|
186
dom/push/test/test_data.html
Normal file
186
dom/push/test/test_data.html
Normal file
@ -0,0 +1,186 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1185544: Add data delivery to the WebSocket backend.
|
||||
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
-->
|
||||
<head>
|
||||
<title>Test for Bug 1185544</title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="/tests/dom/push/test/webpush.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1185544">Mozilla Bug 1185544</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
var registration;
|
||||
|
||||
function start() {
|
||||
return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
|
||||
.then(swr => { registration = swr; return swr; });
|
||||
}
|
||||
|
||||
var controlledFrame;
|
||||
function createControlledIFrame(swr) {
|
||||
var p = new Promise(function(res, rej) {
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = "controlledFrame";
|
||||
iframe.src = "http://mochi.test:8888/tests/dom/push/test/frame.html";
|
||||
|
||||
iframe.onload = function() {
|
||||
res(swr)
|
||||
}
|
||||
controlledFrame = iframe;
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
function subscribe(swr) {
|
||||
return swr.pushManager.subscribe();
|
||||
}
|
||||
|
||||
function sendRequestToWorker(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var channel = new MessageChannel();
|
||||
channel.port1.onmessage = e => {
|
||||
(e.data.error ? reject : resolve)(e.data);
|
||||
};
|
||||
registration.active.postMessage(request, [channel.port2]);
|
||||
});
|
||||
}
|
||||
|
||||
function comparePublicKey(pushSubscription) {
|
||||
// FIXME(kitcambridge): Enable when `ServiceWorkerMessageEvent` is
|
||||
// implemented (bug 1143717).
|
||||
return Promise.resolve(pushSubscription);
|
||||
/*
|
||||
return sendRequestToWorker({ type: "publicKey" }).then(data => {
|
||||
return registration.pushManager.getSubscription().then(
|
||||
pushSubscription => {
|
||||
isDeeply(pushSubscription.getKey("p256dh"), data,
|
||||
"Mismatched key share");
|
||||
return pushSubscription;
|
||||
});
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
function waitForMessage(pushSubscription, message) {
|
||||
return Promise.all([
|
||||
controlledFrame.contentWindow.waitOnPushMessage(pushSubscription),
|
||||
webpush(pushSubscription, message),
|
||||
]).then(([message]) => message);
|
||||
}
|
||||
|
||||
function sendPushMessageFromPage(pushSubscription) {
|
||||
var typedArray = new Uint8Array([226, 130, 40, 240, 40, 140, 188]);
|
||||
var json = { hello: "world" };
|
||||
return waitForMessage(pushSubscription, "Text message from page")
|
||||
.then(message => {
|
||||
is(message.data.text, "Text message from page", "Wrong text message data");
|
||||
return waitForMessage(
|
||||
pushSubscription,
|
||||
typedArray
|
||||
);
|
||||
}).then(message => {
|
||||
isDeeply(new Uint8Array(message.data.arrayBuffer), typedArray,
|
||||
"Wrong array buffer message data");
|
||||
return waitForMessage(
|
||||
pushSubscription,
|
||||
JSON.stringify(json)
|
||||
);
|
||||
}).then(message => {
|
||||
ok(message.data.json.ok, "Unexpected error parsing JSON");
|
||||
isDeeply(message.data.json.value, json, "Wrong JSON message data");
|
||||
return waitForMessage(
|
||||
pushSubscription,
|
||||
""
|
||||
);
|
||||
}).then(message => {
|
||||
ok(message, "Should include data for empty messages");
|
||||
is(message.data.text, "", "Wrong text for empty message");
|
||||
is(message.data.arrayBuffer.byteLength, 0, "Wrong buffer length for empty message");
|
||||
ok(!message.data.json.ok, "Expected JSON parse error for empty message");
|
||||
return waitForMessage(
|
||||
pushSubscription,
|
||||
new Uint8Array([0x48, 0x69, 0x21, 0x20, 0xf0, 0x9f, 0x91, 0x80])
|
||||
);
|
||||
}).then(message => {
|
||||
is(message.data.text, "Hi! \ud83d\udc40", "Wrong text for message with emoji");
|
||||
return new Promise((resolve, reject) => {
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = event => {
|
||||
if (reader.error) {
|
||||
reject(reader.error);
|
||||
} else {
|
||||
resolve(reader.result);
|
||||
}
|
||||
};
|
||||
reader.readAsText(message.data.blob);
|
||||
});
|
||||
}).then(text => {
|
||||
is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
|
||||
is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
|
||||
// Send a blank message.
|
||||
return Promise.all([
|
||||
controlledFrame.contentWindow.waitOnPushMessage(pushSubscription),
|
||||
fetch("http://mochi.test:8888/tests/dom/push/test/push-server.sjs", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Push-Method": "POST",
|
||||
"X-Push-Server": pushSubscription.endpoint,
|
||||
},
|
||||
}),
|
||||
]).then(([message]) => message);
|
||||
}).then(message => {
|
||||
ok(!message.data, "Should exclude data for blank messages");
|
||||
return pushSubscription;
|
||||
});
|
||||
}
|
||||
|
||||
function unsubscribe(pushSubscription) {
|
||||
controlledFrame.parentNode.removeChild(controlledFrame);
|
||||
controlledFrame = null;
|
||||
return pushSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
function unregister() {
|
||||
return registration.unregister();
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
start()
|
||||
.then(createControlledIFrame)
|
||||
.then(subscribe)
|
||||
.then(comparePublicKey)
|
||||
.then(sendPushMessageFromPage)
|
||||
.then(unsubscribe)
|
||||
.then(unregister)
|
||||
.catch(function(e) {
|
||||
ok(false, "Some test failed with error " + e);
|
||||
}).then(SimpleTest.finish);
|
||||
}
|
||||
|
||||
SpecialPowers.pushPrefEnv({"set": [
|
||||
["dom.push.enabled", true],
|
||||
["dom.serviceWorkers.exemptFromPerDomainMax", true],
|
||||
["dom.serviceWorkers.enabled", true],
|
||||
["dom.serviceWorkers.testing.enabled", true]
|
||||
]}, runTest);
|
||||
SpecialPowers.addPermission('push', true, document);
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -57,6 +57,7 @@ http://creativecommons.org/licenses/publicdomain/
|
||||
// Work around CORS for now.
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', "http://mochi.test:8888/tests/dom/push/test/push-server.sjs", true);
|
||||
xhr.setRequestHeader("X-Push-Method", "PUT");
|
||||
xhr.setRequestHeader("X-Push-Server", pushEndpoint);
|
||||
xhr.onload = function(e) {
|
||||
debug("xhr : " + this.status);
|
||||
|
206
dom/push/test/webpush.js
Normal file
206
dom/push/test/webpush.js
Normal file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Browser-based Web Push client for the application server piece.
|
||||
*
|
||||
* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/licenses/publicdomain/
|
||||
*
|
||||
* Uses the WebCrypto API.
|
||||
* Uses the fetch API. Polyfill: https://github.com/github/fetch
|
||||
*/
|
||||
|
||||
(function (g) {
|
||||
'use strict';
|
||||
|
||||
var P256DH = {
|
||||
name: 'ECDH',
|
||||
namedCurve: 'P-256'
|
||||
};
|
||||
var webCrypto = g.crypto.subtle;
|
||||
var ENCRYPT_INFO = new TextEncoder('utf-8').encode("Content-Encoding: aesgcm128");
|
||||
var NONCE_INFO = new TextEncoder('utf-8').encode("Content-Encoding: nonce");
|
||||
|
||||
function chunkArray(array, size) {
|
||||
var start = array.byteOffset || 0;
|
||||
array = array.buffer || array;
|
||||
var index = 0;
|
||||
var result = [];
|
||||
while(index + size <= array.byteLength) {
|
||||
result.push(new Uint8Array(array, start + index, size));
|
||||
index += size;
|
||||
}
|
||||
if (index < array.byteLength) {
|
||||
result.push(new Uint8Array(array, start + index));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* I can't believe that this is needed here, in this day and age ...
|
||||
* Note: these are not efficient, merely expedient.
|
||||
*/
|
||||
var base64url = {
|
||||
_strmap: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
|
||||
encode: function(data) {
|
||||
data = new Uint8Array(data);
|
||||
var len = Math.ceil(data.length * 4 / 3);
|
||||
return chunkArray(data, 3).map(chunk => [
|
||||
chunk[0] >>> 2,
|
||||
((chunk[0] & 0x3) << 4) | (chunk[1] >>> 4),
|
||||
((chunk[1] & 0xf) << 2) | (chunk[2] >>> 6),
|
||||
chunk[2] & 0x3f
|
||||
].map(v => base64url._strmap[v]).join('')).join('').slice(0, len);
|
||||
},
|
||||
_lookup: function(s, i) {
|
||||
return base64url._strmap.indexOf(s.charAt(i));
|
||||
},
|
||||
decode: function(str) {
|
||||
var v = new Uint8Array(Math.floor(str.length * 3 / 4));
|
||||
var vi = 0;
|
||||
for (var si = 0; si < str.length;) {
|
||||
var w = base64url._lookup(str, si++);
|
||||
var x = base64url._lookup(str, si++);
|
||||
var y = base64url._lookup(str, si++);
|
||||
var z = base64url._lookup(str, si++);
|
||||
v[vi++] = w << 2 | x >>> 4;
|
||||
v[vi++] = x << 4 | y >>> 2;
|
||||
v[vi++] = y << 6 | z;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
g.base64url = base64url;
|
||||
|
||||
/* Coerces data into a Uint8Array */
|
||||
function ensureView(data) {
|
||||
if (typeof data === 'string') {
|
||||
return new TextEncoder('utf-8').encode(data);
|
||||
}
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return new Uint8Array(data);
|
||||
}
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
return new Uint8Array(data.buffer);
|
||||
}
|
||||
throw new Error('webpush() needs a string or BufferSource');
|
||||
}
|
||||
|
||||
function bsConcat(arrays) {
|
||||
var size = arrays.reduce((total, a) => total + a.byteLength, 0);
|
||||
var index = 0;
|
||||
return arrays.reduce((result, a) => {
|
||||
result.set(new Uint8Array(a), index);
|
||||
index += a.byteLength;
|
||||
return result;
|
||||
}, new Uint8Array(size));
|
||||
}
|
||||
|
||||
function hmac(key) {
|
||||
this.keyPromise = webCrypto.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' },
|
||||
false, ['sign']);
|
||||
}
|
||||
hmac.prototype.hash = function(input) {
|
||||
return this.keyPromise.then(k => webCrypto.sign('HMAC', k, input));
|
||||
};
|
||||
|
||||
function hkdf(salt, ikm) {
|
||||
this.prkhPromise = new hmac(salt).hash(ikm)
|
||||
.then(prk => new hmac(prk));
|
||||
}
|
||||
|
||||
hkdf.prototype.generate = function(info, len) {
|
||||
var input = bsConcat([info, new Uint8Array([1])]);
|
||||
return this.prkhPromise
|
||||
.then(prkh => prkh.hash(input))
|
||||
.then(h => {
|
||||
if (h.byteLength < len) {
|
||||
throw new Error('Length is too long');
|
||||
}
|
||||
return h.slice(0, len);
|
||||
});
|
||||
};
|
||||
|
||||
/* generate a 96-bit IV for use in GCM, 48-bits of which are populated */
|
||||
function generateNonce(base, index) {
|
||||
var nonce = base.slice(0, 12);
|
||||
for (var i = 0; i < 6; ++i) {
|
||||
nonce[nonce.length - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
|
||||
}
|
||||
return nonce;
|
||||
}
|
||||
|
||||
function encrypt(localKey, remoteShare, salt, data) {
|
||||
return webCrypto.importKey('raw', remoteShare, P256DH, false, ['deriveBits'])
|
||||
.then(remoteKey =>
|
||||
webCrypto.deriveBits({ name: P256DH.name, public: remoteKey },
|
||||
localKey, 256))
|
||||
.then(rawKey => {
|
||||
var kdf = new hkdf(salt, rawKey);
|
||||
return Promise.all([
|
||||
kdf.generate(ENCRYPT_INFO, 16)
|
||||
.then(gcmBits =>
|
||||
webCrypto.importKey('raw', gcmBits, 'AES-GCM', false, ['encrypt'])),
|
||||
kdf.generate(NONCE_INFO, 12)
|
||||
]);
|
||||
})
|
||||
.then(([key, nonce]) => {
|
||||
if (data.byteLength === 0) {
|
||||
// Send an authentication tag for empty messages.
|
||||
return webCrypto.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv: generateNonce(nonce, 0)
|
||||
}, key, new Uint8Array([0])).then(value => [value]);
|
||||
}
|
||||
// 4096 is the default size, though we burn 1 for padding
|
||||
return Promise.all(chunkArray(data, 4095).map((slice, index) => {
|
||||
var padded = bsConcat([new Uint8Array([0]), slice]);
|
||||
return webCrypto.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv: generateNonce(nonce, index)
|
||||
}, key, padded);
|
||||
}));
|
||||
}).then(bsConcat);
|
||||
}
|
||||
|
||||
/*
|
||||
* Request push for a message. This returns a promise that resolves when the
|
||||
* push has been delivered to the push service.
|
||||
*
|
||||
* @param subscription A PushSubscription that contains endpoint and p256dh
|
||||
* parameters.
|
||||
* @param data The message to send.
|
||||
*/
|
||||
function webpush(subscription, data) {
|
||||
data = ensureView(data);
|
||||
|
||||
var salt = g.crypto.getRandomValues(new Uint8Array(16));
|
||||
return webCrypto.generateKey(P256DH, false, ['deriveBits'])
|
||||
.then(localKey => {
|
||||
return Promise.all([
|
||||
encrypt(localKey.privateKey, subscription.getKey("p256dh"), salt, data),
|
||||
// 1337 p-256 specific haxx to get the raw value out of the spki value
|
||||
webCrypto.exportKey('raw', localKey.publicKey),
|
||||
]);
|
||||
}).then(([payload, pubkey]) => {
|
||||
var options = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-Push-Server': subscription.endpoint,
|
||||
// Web Push requires POST requests.
|
||||
'X-Push-Method': 'POST',
|
||||
'Encryption-Key': 'keyid=p256dh;dh=' + base64url.encode(pubkey),
|
||||
Encryption: 'keyid=p256dh;salt=' + base64url.encode(salt),
|
||||
'Content-Encoding': 'aesgcm128'
|
||||
},
|
||||
body: payload,
|
||||
};
|
||||
return fetch('http://mochi.test:8888/tests/dom/push/test/push-server.sjs', options);
|
||||
}).then(response => {
|
||||
if (response.status / 100 !== 2) {
|
||||
throw new Error('Unable to deliver message');
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
g.webpush = webpush;
|
||||
}(this));
|
@ -2,15 +2,56 @@
|
||||
// http://creativecommons.org/licenses/publicdomain/
|
||||
|
||||
this.onpush = handlePush;
|
||||
this.onmessage = handleMessage;
|
||||
|
||||
function getJSON(data) {
|
||||
var result = {
|
||||
ok: false,
|
||||
};
|
||||
try {
|
||||
result.value = data.json();
|
||||
result.ok = true;
|
||||
} catch (e) {
|
||||
// Ignore syntax errors for invalid JSON.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function handlePush(event) {
|
||||
|
||||
self.clients.matchAll().then(function(result) {
|
||||
// FIXME(nsm): Bug 1149195 will fix data exposure.
|
||||
if (event instanceof PushEvent && !('data' in event)) {
|
||||
result[0].postMessage({type: "finished", okay: "yes"});
|
||||
if (event instanceof PushEvent) {
|
||||
if (!('data' in event)) {
|
||||
result[0].postMessage({type: "finished", okay: "yes"});
|
||||
return;
|
||||
}
|
||||
var message = {
|
||||
type: "finished",
|
||||
okay: "yes",
|
||||
};
|
||||
if (event.data) {
|
||||
message.data = {
|
||||
text: event.data.text(),
|
||||
arrayBuffer: event.data.arrayBuffer(),
|
||||
json: getJSON(event.data),
|
||||
blob: event.data.blob(),
|
||||
};
|
||||
}
|
||||
result[0].postMessage(message);
|
||||
return;
|
||||
}
|
||||
result[0].postMessage({type: "finished", okay: "no"});
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
// FIXME(kitcambridge): Enable when `ServiceWorkerMessageEvent` is
|
||||
// implemented (bug 1143717).
|
||||
/*
|
||||
if (event.data.type == "publicKey") {
|
||||
self.registration.pushManager.getSubscription().then(subscription => {
|
||||
event.ports[0].postMessage(subscription.getKey("p256dh"));
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {PushDB, PushService, PushServiceWebSocket, PushCrypto} = serviceExports;
|
||||
|
||||
const userAgentID = '4dffd396-6582-471d-8c0c-84f394e9f7db';
|
||||
|
||||
function run_test() {
|
||||
do_get_profile();
|
||||
setPrefs({
|
||||
userAgentID,
|
||||
});
|
||||
disableServiceWorkerEvents(
|
||||
'https://example.com/page/1',
|
||||
'https://example.com/page/2',
|
||||
'https://example.com/page/3'
|
||||
);
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_with_data_enabled() {
|
||||
let db = PushServiceWebSocket.newPushDB();
|
||||
do_register_cleanup(() => {return db.drop().then(_ => db.close());});
|
||||
|
||||
let [publicKey, privateKey] = yield PushCrypto.generateKeys();
|
||||
let records = [{
|
||||
channelID: 'eb18f12a-cc42-4f14-accb-3bfc1227f1aa',
|
||||
pushEndpoint: 'https://example.org/push/no-key/1',
|
||||
scope: 'https://example.com/page/1',
|
||||
originAttributes: '',
|
||||
quota: Infinity,
|
||||
}, {
|
||||
channelID: '0d8886b9-8da1-4778-8f5d-1cf93a877ed6',
|
||||
pushEndpoint: 'https://example.org/push/key',
|
||||
scope: 'https://example.com/page/2',
|
||||
originAttributes: '',
|
||||
p256dhPublicKey: publicKey,
|
||||
p256dhPrivateKey: privateKey,
|
||||
quota: Infinity,
|
||||
}];
|
||||
for (let record of records) {
|
||||
yield db.put(record);
|
||||
}
|
||||
|
||||
PushService.init({
|
||||
serverURI: "wss://push.example.org/",
|
||||
networkInfo: new MockDesktopNetworkInfo(),
|
||||
db,
|
||||
makeWebSocket(uri) {
|
||||
return new MockWebSocket(uri, {
|
||||
onHello(request) {
|
||||
ok(request.use_webpush,
|
||||
'Should use Web Push if data delivery is enabled');
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'hello',
|
||||
status: 200,
|
||||
uaid: request.uaid,
|
||||
use_webpush: true,
|
||||
}));
|
||||
},
|
||||
onRegister(request) {
|
||||
this.serverSendMsg(JSON.stringify({
|
||||
messageType: 'register',
|
||||
status: 200,
|
||||
uaid: userAgentID,
|
||||
channelID: request.channelID,
|
||||
pushEndpoint: 'https://example.org/push/new',
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let newRecord = yield PushNotificationService.register(
|
||||
'https://example.com/page/3',
|
||||
ChromeUtils.originAttributesToSuffix({ appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inBrowser: false })
|
||||
);
|
||||
ok(newRecord.p256dhPublicKey, 'Should generate public keys for new records');
|
||||
ok(newRecord.p256dhPrivateKey, 'Should generate private keys for new records');
|
||||
|
||||
let record = yield db.getByKeyID('eb18f12a-cc42-4f14-accb-3bfc1227f1aa');
|
||||
ok(record.p256dhPublicKey, 'Should add public key to partial record');
|
||||
ok(record.p256dhPrivateKey, 'Should add private key to partial record');
|
||||
|
||||
record = yield db.getByKeyID('0d8886b9-8da1-4778-8f5d-1cf93a877ed6');
|
||||
deepEqual(record.p256dhPublicKey, publicKey,
|
||||
'Should leave existing public key');
|
||||
deepEqual(record.p256dhPrivateKey, privateKey,
|
||||
'Should leave existing private key');
|
||||
});
|
@ -33,12 +33,13 @@ skip-if = toolkit == 'android'
|
||||
[test_unregister_not_found.js]
|
||||
[test_unregister_success.js]
|
||||
[test_webapps_cleardata.js]
|
||||
[test_updateRecordNoEncryptionKeys_ws.js]
|
||||
#http2 test
|
||||
[test_resubscribe_4xxCode_http2.js]
|
||||
[test_resubscribe_5xxCode_http2.js]
|
||||
[test_resubscribe_listening_for_msg_error_http2.js]
|
||||
[test_register_5xxCode_http2.js]
|
||||
[test_updateRecordNoEncryptionKeys.js]
|
||||
[test_updateRecordNoEncryptionKeys_http2.js]
|
||||
[test_register_success_http2.js]
|
||||
skip-if = !hasNode
|
||||
run-sequentially = node server exceptions dont replay well
|
||||
|
@ -86,6 +86,9 @@ interface DeviceStorage : EventTarget {
|
||||
// Indicates if the storage area denoted by storageName is removable
|
||||
readonly attribute boolean isRemovable;
|
||||
|
||||
// True if the storage area is close to being full
|
||||
readonly attribute boolean lowDiskSpace;
|
||||
|
||||
[NewObject]
|
||||
// XXXbz what type does this really return?
|
||||
Promise<any> getRoot();
|
||||
|
@ -11,13 +11,11 @@
|
||||
Func="nsContentUtils::PushEnabled",
|
||||
Exposed=ServiceWorker]
|
||||
interface PushEvent : ExtendableEvent {
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
// readonly attribute PushMessageData data;
|
||||
readonly attribute PushMessageData? data;
|
||||
};
|
||||
|
||||
typedef USVString PushMessageDataInit;
|
||||
typedef (BufferSource or USVString) PushMessageDataInit;
|
||||
|
||||
dictionary PushEventInit : ExtendableEventInit {
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
// PushMessageDataInit data;
|
||||
PushMessageDataInit data;
|
||||
};
|
||||
|
@ -11,10 +11,11 @@
|
||||
Exposed=ServiceWorker]
|
||||
interface PushMessageData
|
||||
{
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
// These methods will be exposed once encryption is supported.
|
||||
// ArrayBuffer arrayBuffer();
|
||||
// Blob blob();
|
||||
// object json();
|
||||
// USVString text();
|
||||
[Throws]
|
||||
ArrayBuffer arrayBuffer();
|
||||
[Throws]
|
||||
Blob blob();
|
||||
[Throws]
|
||||
any json();
|
||||
USVString text();
|
||||
};
|
||||
|
@ -26,6 +26,15 @@
|
||||
#include "mozilla/dom/WorkerScope.h"
|
||||
#include "mozilla/dom/workers/bindings/ServiceWorker.h"
|
||||
|
||||
#ifndef MOZ_SIMPLEPUSH
|
||||
#include "nsIUnicodeDecoder.h"
|
||||
#include "nsIUnicodeEncoder.h"
|
||||
|
||||
#include "mozilla/dom/EncodingUtils.h"
|
||||
#include "mozilla/dom/FetchUtil.h"
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
#endif
|
||||
|
||||
#include "WorkerPrivate.h"
|
||||
|
||||
using namespace mozilla::dom;
|
||||
@ -435,16 +444,82 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(ExtendableEvent, Event, mPromises)
|
||||
|
||||
#ifndef MOZ_SIMPLEPUSH
|
||||
|
||||
PushMessageData::PushMessageData(const nsAString& aData)
|
||||
: mData(aData)
|
||||
namespace {
|
||||
nsresult
|
||||
ExtractBytesFromArrayBufferView(const ArrayBufferView& aView, nsTArray<uint8_t>& aBytes)
|
||||
{
|
||||
MOZ_ASSERT(aBytes.IsEmpty());
|
||||
aView.ComputeLengthAndData();
|
||||
aBytes.InsertElementsAt(0, aView.Data(), aView.Length());
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ExtractBytesFromArrayBuffer(const ArrayBuffer& aBuffer, nsTArray<uint8_t>& aBytes)
|
||||
{
|
||||
MOZ_ASSERT(aBytes.IsEmpty());
|
||||
aBuffer.ComputeLengthAndData();
|
||||
aBytes.InsertElementsAt(0, aBuffer.Data(), aBuffer.Length());
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ExtractBytesFromUSVString(const nsAString& aStr, nsTArray<uint8_t>& aBytes)
|
||||
{
|
||||
MOZ_ASSERT(aBytes.IsEmpty());
|
||||
nsCOMPtr<nsIUnicodeEncoder> encoder = EncodingUtils::EncoderForEncoding("UTF-8");
|
||||
if (!encoder) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
int32_t srcLen = aStr.Length();
|
||||
int32_t destBufferLen;
|
||||
nsresult rv = encoder->GetMaxLength(aStr.BeginReading(), srcLen, &destBufferLen);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
aBytes.SetLength(destBufferLen);
|
||||
|
||||
char* destBuffer = reinterpret_cast<char*>(aBytes.Elements());
|
||||
int32_t outLen = destBufferLen;
|
||||
rv = encoder->Convert(aStr.BeginReading(), &srcLen, destBuffer, &outLen);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(outLen <= destBufferLen);
|
||||
aBytes.SetLength(outLen);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
ExtractBytesFromData(const OwningArrayBufferViewOrArrayBufferOrUSVString& aDataInit, nsTArray<uint8_t>& aBytes)
|
||||
{
|
||||
if (aDataInit.IsArrayBufferView()) {
|
||||
const ArrayBufferView& view = aDataInit.GetAsArrayBufferView();
|
||||
return ExtractBytesFromArrayBufferView(view, aBytes);
|
||||
} else if (aDataInit.IsArrayBuffer()) {
|
||||
const ArrayBuffer& buffer = aDataInit.GetAsArrayBuffer();
|
||||
return ExtractBytesFromArrayBuffer(buffer, aBytes);
|
||||
} else if (aDataInit.IsUSVString()) {
|
||||
return ExtractBytesFromUSVString(aDataInit.GetAsUSVString(), aBytes);
|
||||
}
|
||||
NS_NOTREACHED("Unexpected push message data");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
PushMessageData::PushMessageData(nsISupports* aOwner,
|
||||
const nsTArray<uint8_t>& aBytes)
|
||||
: mOwner(aOwner), mBytes(aBytes) {}
|
||||
|
||||
PushMessageData::~PushMessageData()
|
||||
{
|
||||
}
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(PushMessageData);
|
||||
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushMessageData, mOwner)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushMessageData)
|
||||
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushMessageData)
|
||||
@ -455,38 +530,114 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushMessageData)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
void
|
||||
PushMessageData::Json(JSContext* cx, JS::MutableHandle<JSObject*> aRetval)
|
||||
PushMessageData::Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
//todo bug 1149195. Don't be lazy.
|
||||
NS_ABORT();
|
||||
if (NS_FAILED(EnsureDecodedText())) {
|
||||
aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR);
|
||||
return;
|
||||
}
|
||||
FetchUtil::ConsumeJson(cx, aRetval, mDecodedText, aRv);
|
||||
}
|
||||
|
||||
void
|
||||
PushMessageData::Text(nsAString& aData)
|
||||
{
|
||||
aData = mData;
|
||||
if (NS_SUCCEEDED(EnsureDecodedText())) {
|
||||
aData = mDecodedText;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PushMessageData::ArrayBuffer(JSContext* cx, JS::MutableHandle<JSObject*> aRetval)
|
||||
PushMessageData::ArrayBuffer(JSContext* cx,
|
||||
JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
//todo bug 1149195. Don't be lazy.
|
||||
NS_ABORT();
|
||||
uint8_t* data = GetContentsCopy();
|
||||
if (data) {
|
||||
FetchUtil::ConsumeArrayBuffer(cx, aRetval, mBytes.Length(), data, aRv);
|
||||
}
|
||||
}
|
||||
|
||||
mozilla::dom::Blob*
|
||||
PushMessageData::Blob()
|
||||
already_AddRefed<mozilla::dom::Blob>
|
||||
PushMessageData::Blob(ErrorResult& aRv)
|
||||
{
|
||||
//todo bug 1149195. Don't be lazy.
|
||||
NS_ABORT();
|
||||
uint8_t* data = GetContentsCopy();
|
||||
if (data) {
|
||||
nsRefPtr<mozilla::dom::Blob> blob = FetchUtil::ConsumeBlob(
|
||||
mOwner, EmptyString(), mBytes.Length(), data, aRv);
|
||||
if (blob) {
|
||||
return blob.forget();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
NS_METHOD
|
||||
PushMessageData::EnsureDecodedText()
|
||||
{
|
||||
if (mBytes.IsEmpty() || !mDecodedText.IsEmpty()) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsresult rv = FetchUtil::ConsumeText(
|
||||
mBytes.Length(),
|
||||
reinterpret_cast<uint8_t*>(mBytes.Elements()),
|
||||
mDecodedText
|
||||
);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
mDecodedText.Truncate();
|
||||
return rv;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
uint8_t*
|
||||
PushMessageData::GetContentsCopy()
|
||||
{
|
||||
uint32_t length = mBytes.Length();
|
||||
void* data = malloc(length);
|
||||
if (!data) {
|
||||
return nullptr;
|
||||
}
|
||||
memcpy(data, mBytes.Elements(), length);
|
||||
return reinterpret_cast<uint8_t*>(data);
|
||||
}
|
||||
|
||||
PushEvent::PushEvent(EventTarget* aOwner)
|
||||
: ExtendableEvent(aOwner)
|
||||
{
|
||||
}
|
||||
|
||||
already_AddRefed<PushEvent>
|
||||
PushEvent::Constructor(mozilla::dom::EventTarget* aOwner,
|
||||
const nsAString& aType,
|
||||
const PushEventInit& aOptions,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
nsRefPtr<PushEvent> e = new PushEvent(aOwner);
|
||||
bool trusted = e->Init(aOwner);
|
||||
e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
|
||||
e->SetTrusted(trusted);
|
||||
if(aOptions.mData.WasPassed()){
|
||||
nsTArray<uint8_t> bytes;
|
||||
nsresult rv = ExtractBytesFromData(aOptions.mData.Value(), bytes);
|
||||
if (NS_FAILED(rv)) {
|
||||
aRv.Throw(rv);
|
||||
return nullptr;
|
||||
}
|
||||
e->mData = new PushMessageData(aOwner, bytes);
|
||||
}
|
||||
return e.forget();
|
||||
}
|
||||
|
||||
NS_IMPL_ADDREF_INHERITED(PushEvent, ExtendableEvent)
|
||||
NS_IMPL_RELEASE_INHERITED(PushEvent, ExtendableEvent)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(PushEvent)
|
||||
NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_INHERITED(PushEvent, ExtendableEvent, mData)
|
||||
|
||||
#endif /* ! MOZ_SIMPLEPUSH */
|
||||
|
||||
END_WORKERS_NAMESPACE
|
||||
|
@ -17,6 +17,7 @@
|
||||
#ifndef MOZ_SIMPLEPUSH
|
||||
#include "mozilla/dom/PushEventBinding.h"
|
||||
#include "mozilla/dom/PushMessageDataBinding.h"
|
||||
#include "mozilla/dom/File.h"
|
||||
#endif
|
||||
|
||||
#include "nsProxyRelease.h"
|
||||
@ -168,8 +169,6 @@ public:
|
||||
class PushMessageData final : public nsISupports,
|
||||
public nsWrapperCache
|
||||
{
|
||||
nsString mData;
|
||||
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushMessageData)
|
||||
@ -180,24 +179,30 @@ public:
|
||||
}
|
||||
|
||||
nsISupports* GetParentObject() const {
|
||||
return nullptr;
|
||||
return mOwner;
|
||||
}
|
||||
|
||||
void Json(JSContext* cx, JS::MutableHandle<JSObject*> aRetval);
|
||||
void Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval,
|
||||
ErrorResult& aRv);
|
||||
void Text(nsAString& aData);
|
||||
void ArrayBuffer(JSContext* cx, JS::MutableHandle<JSObject*> aRetval);
|
||||
mozilla::dom::Blob* Blob();
|
||||
void ArrayBuffer(JSContext* cx, JS::MutableHandle<JSObject*> aRetval,
|
||||
ErrorResult& aRv);
|
||||
already_AddRefed<mozilla::dom::Blob> Blob(ErrorResult& aRv);
|
||||
|
||||
explicit PushMessageData(const nsAString& aData);
|
||||
PushMessageData(nsISupports* aOwner, const nsTArray<uint8_t>& aBytes);
|
||||
private:
|
||||
nsCOMPtr<nsISupports> mOwner;
|
||||
nsTArray<uint8_t> mBytes;
|
||||
nsString mDecodedText;
|
||||
~PushMessageData();
|
||||
|
||||
NS_METHOD EnsureDecodedText();
|
||||
uint8_t* GetContentsCopy();
|
||||
};
|
||||
|
||||
class PushEvent final : public ExtendableEvent
|
||||
{
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
// nsRefPtr<PushMessageData> mData;
|
||||
nsRefPtr<PushMessageData> mData;
|
||||
nsMainThreadPtrHandle<ServiceWorker> mServiceWorker;
|
||||
|
||||
protected:
|
||||
@ -205,8 +210,8 @@ protected:
|
||||
~PushEvent() {}
|
||||
|
||||
public:
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
// Add cycle collection macros once data is re-exposed.
|
||||
NS_DECL_ISUPPORTS_INHERITED
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PushEvent, ExtendableEvent)
|
||||
NS_FORWARD_TO_EVENT
|
||||
|
||||
virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
|
||||
@ -217,18 +222,8 @@ public:
|
||||
static already_AddRefed<PushEvent>
|
||||
Constructor(mozilla::dom::EventTarget* aOwner,
|
||||
const nsAString& aType,
|
||||
const PushEventInit& aOptions)
|
||||
{
|
||||
nsRefPtr<PushEvent> e = new PushEvent(aOwner);
|
||||
bool trusted = e->Init(aOwner);
|
||||
e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
|
||||
e->SetTrusted(trusted);
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
//if(aOptions.mData.WasPassed()){
|
||||
// e->mData = new PushMessageData(aOptions.mData.Value());
|
||||
//}
|
||||
return e.forget();
|
||||
}
|
||||
const PushEventInit& aOptions,
|
||||
ErrorResult& aRv);
|
||||
|
||||
static already_AddRefed<PushEvent>
|
||||
Constructor(const GlobalObject& aGlobal,
|
||||
@ -237,7 +232,7 @@ public:
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
return Constructor(owner, aType, aOptions);
|
||||
return Constructor(owner, aType, aOptions, aRv);
|
||||
}
|
||||
|
||||
void PostInit(nsMainThreadPtrHandle<ServiceWorker>& aServiceWorker)
|
||||
@ -245,11 +240,9 @@ public:
|
||||
mServiceWorker = aServiceWorker;
|
||||
}
|
||||
|
||||
PushMessageData* Data()
|
||||
PushMessageData* GetData() const
|
||||
{
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
MOZ_CRASH("Should not be called!");
|
||||
return nullptr;
|
||||
return mData;
|
||||
}
|
||||
};
|
||||
#endif /* ! MOZ_SIMPLEPUSH */
|
||||
|
@ -72,6 +72,10 @@
|
||||
#include "WorkerRunnable.h"
|
||||
#include "WorkerScope.h"
|
||||
|
||||
#ifndef MOZ_SIMPLEPUSH
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
#endif
|
||||
|
||||
#ifdef PostMessage
|
||||
#undef PostMessage
|
||||
#endif
|
||||
@ -2293,22 +2297,22 @@ public:
|
||||
|
||||
class SendPushEventRunnable final : public WorkerRunnable
|
||||
{
|
||||
nsString mData;
|
||||
Maybe<nsTArray<uint8_t>> mData;
|
||||
nsMainThreadPtrHandle<ServiceWorker> mServiceWorker;
|
||||
|
||||
public:
|
||||
SendPushEventRunnable(
|
||||
WorkerPrivate* aWorkerPrivate,
|
||||
const nsAString& aData,
|
||||
nsMainThreadPtrHandle<ServiceWorker>& aServiceWorker)
|
||||
: WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount)
|
||||
, mData(aData)
|
||||
, mServiceWorker(aServiceWorker)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
MOZ_ASSERT(aWorkerPrivate);
|
||||
MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
|
||||
}
|
||||
: SendPushEventRunnable(aWorkerPrivate, aServiceWorker,
|
||||
Nothing()) {}
|
||||
|
||||
SendPushEventRunnable(
|
||||
WorkerPrivate* aWorkerPrivate,
|
||||
const nsTArray<uint8_t>& aData,
|
||||
nsMainThreadPtrHandle<ServiceWorker>& aServiceWorker)
|
||||
: SendPushEventRunnable(aWorkerPrivate, aServiceWorker,
|
||||
Some(aData)) {}
|
||||
|
||||
bool
|
||||
WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
|
||||
@ -2316,9 +2320,16 @@ public:
|
||||
MOZ_ASSERT(aWorkerPrivate);
|
||||
GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper());
|
||||
|
||||
|
||||
PushEventInit pei;
|
||||
// FIXME(nsm): Bug 1149195.
|
||||
// pei.mData.Construct(mData);
|
||||
if (mData) {
|
||||
const nsTArray<uint8_t>& bytes = mData.ref();
|
||||
JSObject* data = Uint8Array::Create(aCx, bytes.Length(), bytes.Elements());
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
pei.mData.Construct().SetAsArrayBufferView().Init(data);
|
||||
}
|
||||
pei.mBubbles = false;
|
||||
pei.mCancelable = false;
|
||||
|
||||
@ -2342,6 +2353,20 @@ public:
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
SendPushEventRunnable(
|
||||
WorkerPrivate* aWorkerPrivate,
|
||||
nsMainThreadPtrHandle<ServiceWorker>& aServiceWorker,
|
||||
Maybe<nsTArray<uint8_t>> aData)
|
||||
: WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount)
|
||||
, mData(aData)
|
||||
, mServiceWorker(aServiceWorker)
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
MOZ_ASSERT(aWorkerPrivate);
|
||||
MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
|
||||
}
|
||||
};
|
||||
|
||||
class SendPushSubscriptionChangeEventRunnable final : public WorkerRunnable
|
||||
@ -2387,7 +2412,9 @@ public:
|
||||
NS_IMETHODIMP
|
||||
ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,
|
||||
const nsACString& aScope,
|
||||
const nsAString& aData)
|
||||
uint32_t aDataLength,
|
||||
uint8_t* aDataBytes,
|
||||
uint8_t optional_argc)
|
||||
{
|
||||
#ifdef MOZ_SIMPLEPUSH
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
@ -2406,9 +2433,19 @@ ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes,
|
||||
nsMainThreadPtrHandle<ServiceWorker> serviceWorkerHandle(
|
||||
new nsMainThreadPtrHolder<ServiceWorker>(serviceWorker));
|
||||
|
||||
nsRefPtr<SendPushEventRunnable> r =
|
||||
new SendPushEventRunnable(serviceWorker->GetWorkerPrivate(), aData,
|
||||
serviceWorkerHandle);
|
||||
nsRefPtr<SendPushEventRunnable> r;
|
||||
if (optional_argc == 2) {
|
||||
nsTArray<uint8_t> data;
|
||||
if (!data.InsertElementsAt(0, aDataBytes, aDataLength, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
r = new SendPushEventRunnable(serviceWorker->GetWorkerPrivate(), data,
|
||||
serviceWorkerHandle);
|
||||
} else {
|
||||
MOZ_ASSERT(optional_argc == 0);
|
||||
r = new SendPushEventRunnable(serviceWorker->GetWorkerPrivate(),
|
||||
serviceWorkerHandle);
|
||||
}
|
||||
|
||||
AutoJSAPI jsapi;
|
||||
jsapi.Init();
|
||||
|
17
dom/xslt/crashtests/527558_1.xml
Normal file
17
dom/xslt/crashtests/527558_1.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet type="text/xml" href="#bug"?>
|
||||
<!DOCTYPE doc [
|
||||
<!ATTLIST xsl:transform
|
||||
id ID #REQUIRED>
|
||||
]>
|
||||
<doc>
|
||||
<xsl:transform id="bug"
|
||||
version="2.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:key name="k0" match="e1" use="key('k0', 'foobar')" />
|
||||
<xsl:template id="t1" name="t1" match="key('k0', '1/2/2003')" />
|
||||
</xsl:transform>
|
||||
|
||||
<e1 a1="foobar" a2="foobar"/>
|
||||
|
||||
</doc>
|
@ -6,6 +6,7 @@ load 406106-1.html
|
||||
load 483444.xml
|
||||
load 485217.xml
|
||||
load 485286.xml
|
||||
load 527558_1.xml
|
||||
load 528300.xml
|
||||
load 528488.xml
|
||||
load 528963.xml
|
||||
|
@ -51,6 +51,14 @@ public:
|
||||
* Callback to be used by the Parser if errors are detected.
|
||||
*/
|
||||
virtual void SetErrorOffset(uint32_t aOffset) = 0;
|
||||
|
||||
enum Allowed {
|
||||
KEY_FUNCTION = 1 << 0
|
||||
};
|
||||
virtual bool allowed(Allowed aAllowed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -9,32 +9,35 @@
|
||||
#include "nsError.h"
|
||||
#include "txStringUtils.h"
|
||||
#include "txXSLTPatterns.h"
|
||||
#include "txIXPathContext.h"
|
||||
#include "txStylesheetCompiler.h"
|
||||
#include "txPatternOptimizer.h"
|
||||
|
||||
|
||||
txPattern* txPatternParser::createPattern(const nsAFlatString& aPattern,
|
||||
txIParseContext* aContext)
|
||||
nsresult txPatternParser::createPattern(const nsAFlatString& aPattern,
|
||||
txIParseContext* aContext,
|
||||
txPattern** aResult)
|
||||
{
|
||||
txExprLexer lexer;
|
||||
nsresult rv = lexer.parse(aPattern);
|
||||
if (NS_FAILED(rv)) {
|
||||
// XXX error report parsing error
|
||||
return 0;
|
||||
return rv;
|
||||
}
|
||||
nsAutoPtr<txPattern> pattern;
|
||||
rv = createUnionPattern(lexer, aContext, *getter_Transfers(pattern));
|
||||
if (NS_FAILED(rv)) {
|
||||
// XXX error report parsing error
|
||||
return 0;
|
||||
return rv;
|
||||
}
|
||||
|
||||
txPatternOptimizer optimizer;
|
||||
txPattern* newPattern = nullptr;
|
||||
rv = optimizer.optimize(pattern, &newPattern);
|
||||
NS_ENSURE_SUCCESS(rv, nullptr);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return newPattern ? newPattern : pattern.forget();
|
||||
*aResult = newPattern ? newPattern : pattern.forget();
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult txPatternParser::createUnionPattern(txExprLexer& aLexer,
|
||||
@ -246,6 +249,9 @@ nsresult txPatternParser::createKeyPattern(txExprLexer& aLexer,
|
||||
if (aLexer.nextToken()->mType != Token::R_PAREN)
|
||||
return NS_ERROR_XPATH_PARSE_FAILURE;
|
||||
|
||||
if (!aContext->allowed(txIParseContext::KEY_FUNCTION))
|
||||
return NS_ERROR_XSLT_CALL_TO_KEY_NOT_ALLOWED;
|
||||
|
||||
const char16_t* colon;
|
||||
if (!XMLUtils::isValidQName(PromiseFlatString(key), &colon))
|
||||
return NS_ERROR_XPATH_PARSE_FAILURE;
|
||||
|
@ -9,11 +9,14 @@
|
||||
#include "txXSLTPatterns.h"
|
||||
#include "txExprParser.h"
|
||||
|
||||
class txStylesheetCompilerState;
|
||||
|
||||
class txPatternParser : public txExprParser
|
||||
{
|
||||
public:
|
||||
static txPattern* createPattern(const nsAFlatString& aPattern,
|
||||
txIParseContext* aContext);
|
||||
static nsresult createPattern(const nsAFlatString& aPattern,
|
||||
txIParseContext* aContext,
|
||||
txPattern** aResult);
|
||||
protected:
|
||||
static nsresult createUnionPattern(txExprLexer& aLexer,
|
||||
txIParseContext* aContext,
|
||||
|
@ -183,7 +183,7 @@ getExprAttr(txStylesheetAttr* aAttributes,
|
||||
|
||||
rv = txExprParser::createExpr(attr->mValue, &aState,
|
||||
getter_Transfers(aExpr));
|
||||
if (NS_FAILED(rv) && aState.fcp()) {
|
||||
if (NS_FAILED(rv) && aState.ignoreError(rv)) {
|
||||
// use default value in fcp for not required exprs
|
||||
if (aRequired) {
|
||||
aExpr = new txErrorExpr(
|
||||
@ -255,10 +255,11 @@ getPatternAttr(txStylesheetAttr* aAttributes,
|
||||
return rv;
|
||||
}
|
||||
|
||||
aPattern = txPatternParser::createPattern(attr->mValue, &aState);
|
||||
if (!aPattern && (aRequired || !aState.fcp())) {
|
||||
rv = txPatternParser::createPattern(attr->mValue, &aState,
|
||||
getter_Transfers(aPattern));
|
||||
if (NS_FAILED(rv) && (aRequired || !aState.ignoreError(rv))) {
|
||||
// XXX ErrorReport: XSLT-Pattern parse failure
|
||||
return NS_ERROR_XPATH_PARSE_FAILURE;
|
||||
return rv;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
@ -838,6 +839,8 @@ txFnStartKey(int32_t aNamespaceID,
|
||||
aState, name);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
aState.mDisAllowed = txIParseContext::KEY_FUNCTION;
|
||||
|
||||
nsAutoPtr<txPattern> match;
|
||||
rv = getPatternAttr(aAttributes, aAttrCount, nsGkAtoms::match, true,
|
||||
aState, match);
|
||||
@ -848,6 +851,8 @@ txFnStartKey(int32_t aNamespaceID,
|
||||
aState, use);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
aState.mDisAllowed = 0;
|
||||
|
||||
rv = aState.mStylesheet->addKey(name, Move(match), Move(use));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
|
@ -528,6 +528,7 @@ txStylesheetCompilerState::txStylesheetCompilerState(txACompileObserver* aObserv
|
||||
mSorter(nullptr),
|
||||
mDOE(false),
|
||||
mSearchingForFallback(false),
|
||||
mDisAllowed(0),
|
||||
mObserver(aObserver),
|
||||
mEmbedStatus(eNoEmbed),
|
||||
mDoneWithThisStylesheet(false),
|
||||
@ -923,6 +924,9 @@ TX_ConstructXSLTFunction(nsIAtom* aName, int32_t aNamespaceID,
|
||||
new DocumentFunctionCall(aState->mElementContext->mBaseURI);
|
||||
}
|
||||
else if (aName == nsGkAtoms::key) {
|
||||
if (!aState->allowed(txIParseContext::KEY_FUNCTION)) {
|
||||
return NS_ERROR_XSLT_CALL_TO_KEY_NOT_ALLOWED;
|
||||
}
|
||||
*aFunction =
|
||||
new txKeyFunctionCall(aState->mElementContext->mMappings);
|
||||
}
|
||||
|
@ -140,6 +140,19 @@ public:
|
||||
|
||||
void SetErrorOffset(uint32_t aOffset) override;
|
||||
|
||||
bool allowed(Allowed aAllowed) override
|
||||
{
|
||||
return !(mDisAllowed & aAllowed);
|
||||
}
|
||||
|
||||
bool ignoreError(nsresult aResult)
|
||||
{
|
||||
// Some errors shouldn't be ignored even in forwards compatible parsing
|
||||
// mode.
|
||||
return aResult != NS_ERROR_XSLT_CALL_TO_KEY_NOT_ALLOWED &&
|
||||
fcp();
|
||||
}
|
||||
|
||||
static void shutdown();
|
||||
|
||||
|
||||
@ -150,6 +163,7 @@ public:
|
||||
nsAutoPtr<txList> mChooseGotoList;
|
||||
bool mDOE;
|
||||
bool mSearchingForFallback;
|
||||
uint16_t mDisAllowed;
|
||||
|
||||
protected:
|
||||
nsRefPtr<txACompileObserver> mObserver;
|
||||
|
@ -401,7 +401,7 @@ Factory::CreateDrawTargetForData(BackendType aBackend,
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
gfxDebug() << "Invalid draw target type specified.";
|
||||
gfxCriticalNote << "Invalid draw target type specified: " << (int)aBackend;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -410,7 +410,7 @@ Factory::CreateDrawTargetForData(BackendType aBackend,
|
||||
}
|
||||
|
||||
if (!retVal) {
|
||||
gfxDebug() << "Failed to create DrawTarget, Type: " << int(aBackend) << " Size: " << aSize;
|
||||
gfxCriticalNote << "Failed to create DrawTarget, Type: " << int(aBackend) << " Size: " << aSize << ", Data: " << hexa(aData) << ", Stride: " << aStride;
|
||||
}
|
||||
|
||||
return retVal.forget();
|
||||
|
@ -151,9 +151,13 @@ already_AddRefed<DrawTarget>
|
||||
ImageDataSerializerBase::GetAsDrawTarget(gfx::BackendType aBackend)
|
||||
{
|
||||
MOZ_ASSERT(IsValid());
|
||||
return gfx::Factory::CreateDrawTargetForData(aBackend,
|
||||
RefPtr<DrawTarget> dt = gfx::Factory::CreateDrawTargetForData(aBackend,
|
||||
GetData(), GetSize(),
|
||||
GetStride(), GetFormat());
|
||||
if (!dt) {
|
||||
gfxCriticalNote << "Failed GetAsDrawTarget " << IsValid() << ", " << hexa(size_t(mData)) << " + " << SurfaceBufferInfo::GetOffset() << ", " << GetSize() << ", " << GetStride() << ", " << (int)GetFormat();
|
||||
}
|
||||
return dt.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<gfx::DataSourceSurface>
|
||||
|
@ -845,6 +845,7 @@ BufferTextureClient::BorrowDrawTarget()
|
||||
|
||||
ImageDataSerializer serializer(GetBuffer(), GetBufferSize());
|
||||
if (!serializer.IsValid()) {
|
||||
gfxCriticalNote << "Invalid serializer " << IsValid() << ", " << IsLocked() << ", " << GetBufferSize();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -854,6 +855,9 @@ BufferTextureClient::BorrowDrawTarget()
|
||||
}
|
||||
|
||||
mDrawTarget = serializer.GetAsDrawTarget(BackendType::CAIRO);
|
||||
if (!mDrawTarget) {
|
||||
gfxCriticalNote << "BorrowDrawTarget failure, original backend " << (int)mBackend;
|
||||
}
|
||||
|
||||
return mDrawTarget;
|
||||
}
|
||||
|
355
image/StreamingLexer.h
Normal file
355
image/StreamingLexer.h
Normal file
@ -0,0 +1,355 @@
|
||||
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* StreamingLexer is a lexing framework designed to make it simple to write
|
||||
* image decoders without worrying about the details of how the data is arriving
|
||||
* from the network.
|
||||
*/
|
||||
|
||||
#ifndef mozilla_image_StreamingLexer_h
|
||||
#define mozilla_image_StreamingLexer_h
|
||||
|
||||
#include <algorithm>
|
||||
#include "mozilla/Assertions.h"
|
||||
#include "mozilla/Maybe.h"
|
||||
#include "mozilla/Vector.h"
|
||||
|
||||
namespace mozilla {
|
||||
namespace image {
|
||||
|
||||
/// Buffering behaviors for StreamingLexer transitions.
|
||||
enum class BufferingStrategy
|
||||
{
|
||||
BUFFERED, // Data will be buffered and processed in one chunk.
|
||||
UNBUFFERED // Data will be processed as it arrives, in multiple chunks.
|
||||
};
|
||||
|
||||
/// @return true if @aState is a terminal state.
|
||||
template <typename State>
|
||||
bool IsTerminalState(State aState)
|
||||
{
|
||||
return aState == State::SUCCESS ||
|
||||
aState == State::FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* LexerTransition is a type used to give commands to the lexing framework.
|
||||
* Code that uses StreamingLexer can create LexerTransition values using the
|
||||
* static methods on Transition, and then return them to the lexing framework
|
||||
* for execution.
|
||||
*/
|
||||
template <typename State>
|
||||
class LexerTransition
|
||||
{
|
||||
public:
|
||||
State NextState() const { return mNextState; }
|
||||
State UnbufferedState() const { return *mUnbufferedState; }
|
||||
size_t Size() const { return mSize; }
|
||||
BufferingStrategy Buffering() const { return mBufferingStrategy; }
|
||||
|
||||
private:
|
||||
friend struct Transition;
|
||||
|
||||
LexerTransition(const State& aNextState,
|
||||
const Maybe<State>& aUnbufferedState,
|
||||
size_t aSize,
|
||||
BufferingStrategy aBufferingStrategy)
|
||||
: mNextState(aNextState)
|
||||
, mUnbufferedState(aUnbufferedState)
|
||||
, mSize(aSize)
|
||||
, mBufferingStrategy(aBufferingStrategy)
|
||||
{
|
||||
MOZ_ASSERT_IF(mBufferingStrategy == BufferingStrategy::UNBUFFERED,
|
||||
mUnbufferedState);
|
||||
MOZ_ASSERT_IF(mUnbufferedState,
|
||||
mBufferingStrategy == BufferingStrategy::UNBUFFERED);
|
||||
}
|
||||
|
||||
State mNextState;
|
||||
Maybe<State> mUnbufferedState;
|
||||
size_t mSize;
|
||||
BufferingStrategy mBufferingStrategy;
|
||||
};
|
||||
|
||||
struct Transition
|
||||
{
|
||||
/// Transition to @aNextState, buffering @aSize bytes of data.
|
||||
template <typename State>
|
||||
static LexerTransition<State>
|
||||
To(const State& aNextState, size_t aSize)
|
||||
{
|
||||
MOZ_ASSERT(!IsTerminalState(aNextState));
|
||||
return LexerTransition<State>(aNextState, Nothing(), aSize,
|
||||
BufferingStrategy::BUFFERED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition to @aNextState via @aUnbufferedState, reading @aSize bytes of
|
||||
* data unbuffered.
|
||||
*
|
||||
* The unbuffered data will be delivered in state @aUnbufferedState, which may
|
||||
* be invoked repeatedly until all @aSize bytes have been delivered. Then,
|
||||
* @aNextState will be invoked with no data. No state transitions are allowed
|
||||
* from @aUnbufferedState except for transitions to a terminal state, so
|
||||
* @aNextState will always be reached unless lexing terminates early.
|
||||
*/
|
||||
template <typename State>
|
||||
static LexerTransition<State>
|
||||
ToUnbuffered(const State& aNextState,
|
||||
const State& aUnbufferedState,
|
||||
size_t aSize)
|
||||
{
|
||||
MOZ_ASSERT(!IsTerminalState(aNextState));
|
||||
MOZ_ASSERT(!IsTerminalState(aUnbufferedState));
|
||||
return LexerTransition<State>(aNextState, Some(aUnbufferedState), aSize,
|
||||
BufferingStrategy::UNBUFFERED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue receiving unbuffered data. @aUnbufferedState should be the same
|
||||
* state as the @aUnbufferedState specified in the preceding call to
|
||||
* ToUnbuffered().
|
||||
*
|
||||
* This should be used during an unbuffered read initiated by ToUnbuffered().
|
||||
*/
|
||||
template <typename State>
|
||||
static LexerTransition<State>
|
||||
ContinueUnbuffered(const State& aUnbufferedState)
|
||||
{
|
||||
MOZ_ASSERT(!IsTerminalState(aUnbufferedState));
|
||||
return LexerTransition<State>(aUnbufferedState, Nothing(), 0,
|
||||
BufferingStrategy::BUFFERED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate lexing, ending up in terminal state @aFinalState.
|
||||
*
|
||||
* No more data will be delivered after Terminate() is used.
|
||||
*/
|
||||
template <typename State>
|
||||
static LexerTransition<State>
|
||||
Terminate(const State& aFinalState)
|
||||
{
|
||||
MOZ_ASSERT(IsTerminalState(aFinalState));
|
||||
return LexerTransition<State>(aFinalState, Nothing(), 0,
|
||||
BufferingStrategy::BUFFERED);
|
||||
}
|
||||
|
||||
private:
|
||||
Transition();
|
||||
};
|
||||
|
||||
/**
|
||||
* StreamingLexer is a lexing framework designed to make it simple to write
|
||||
* image decoders without worrying about the details of how the data is arriving
|
||||
* from the network.
|
||||
*
|
||||
* To use StreamingLexer:
|
||||
*
|
||||
* - Create a State type. This should be an |enum class| listing all of the
|
||||
* states that you can be in while lexing the image format you're trying to
|
||||
* read. It must contain the two terminal states SUCCESS and FAILURE.
|
||||
*
|
||||
* - Add an instance of StreamingLexer<State> to your decoder class. Initialize
|
||||
* it with a Transition::To() the state that you want to start lexing in.
|
||||
*
|
||||
* - In your decoder's WriteInternal method(), call Lex(), passing in the input
|
||||
* data and length that are passed to WriteInternal(). You also need to pass
|
||||
* a lambda which dispatches to lexing code for each state based on the State
|
||||
* value that's passed in. The lambda generally should just continue a
|
||||
* |switch| statement that calls different methods for each State value. Each
|
||||
* method should return a LexerTransition<State>, which the lambda should
|
||||
* return in turn.
|
||||
*
|
||||
* - Write the methods that actually implement lexing for your image format.
|
||||
* These methods should return either Transition::To(), to move on to another
|
||||
* state, or Transition::Terminate(), if lexing has terminated in either
|
||||
* success or failure. (There are also additional transitions for unbuffered
|
||||
* reads; see below.)
|
||||
*
|
||||
* That's all there is to it. The StreamingLexer will track your position in the
|
||||
* input and buffer enough data so that your lexing methods can process
|
||||
* everything in one pass. Lex() returns Nothing() if more data is needed, in
|
||||
* which case you should just return from WriteInternal(). If lexing reaches a
|
||||
* terminal state, Lex() returns Some(State::SUCCESS) or Some(State::FAILURE),
|
||||
* and you can check which one to determine if lexing succeeded or failed and do
|
||||
* any necessary cleanup.
|
||||
*
|
||||
* There's one more wrinkle: some lexers may want to *avoid* buffering in some
|
||||
* cases, and just process the data as it comes in. This is useful if, for
|
||||
* example, you just want to skip over a large section of data; there's no point
|
||||
* in buffering data you're just going to ignore.
|
||||
*
|
||||
* You can begin an unbuffered read with Transition::ToUnbuffered(). This works
|
||||
* a little differently than Transition::To() in that you specify *two* states.
|
||||
* The @aUnbufferedState argument specifies a state that will be called
|
||||
* repeatedly with unbuffered data, as soon as it arrives. The implementation
|
||||
* for that state should return either a transition to a terminal state, or
|
||||
* Transition::ContinueUnbuffered(). Once the amount of data requested in the
|
||||
* original call to Transition::ToUnbuffered() has been delivered, Lex() will
|
||||
* transition to the @aNextState state specified via Transition::ToUnbuffered().
|
||||
* That state will be invoked with *no* data; it's just called to signal that
|
||||
* the unbuffered read is over.
|
||||
*
|
||||
* XXX(seth): We should be able to get of the |State| stuff totally once bug
|
||||
* 1198451 lands, since we can then just return a function representing the next
|
||||
* state directly.
|
||||
*/
|
||||
template <typename State, size_t InlineBufferSize = 16>
|
||||
class StreamingLexer
|
||||
{
|
||||
public:
|
||||
explicit StreamingLexer(LexerTransition<State> aStartState)
|
||||
: mTransition(aStartState)
|
||||
, mToReadUnbuffered(0)
|
||||
{ }
|
||||
|
||||
template <typename Func>
|
||||
Maybe<State> Lex(const char* aInput, size_t aLength, Func aFunc)
|
||||
{
|
||||
if (IsTerminalState(mTransition.NextState())) {
|
||||
// We've already reached a terminal state. We never deliver any more data
|
||||
// in this case; just return the terminal state again immediately.
|
||||
return Some(mTransition.NextState());
|
||||
}
|
||||
|
||||
if (mToReadUnbuffered > 0) {
|
||||
// We're continuing an unbuffered read.
|
||||
|
||||
MOZ_ASSERT(mBuffer.empty(),
|
||||
"Shouldn't be continuing an unbuffered read and a buffered "
|
||||
"read at the same time");
|
||||
|
||||
size_t toRead = std::min(mToReadUnbuffered, aLength);
|
||||
|
||||
// Call aFunc with the unbuffered state to indicate that we're in the middle
|
||||
// of an unbuffered read. We enforce that any state transition passed back
|
||||
// to us is either a terminal states or takes us back to the unbuffered
|
||||
// state.
|
||||
LexerTransition<State> unbufferedTransition =
|
||||
aFunc(mTransition.UnbufferedState(), aInput, toRead);
|
||||
if (IsTerminalState(unbufferedTransition.NextState())) {
|
||||
mTransition = unbufferedTransition;
|
||||
return Some(mTransition.NextState()); // Done!
|
||||
}
|
||||
MOZ_ASSERT(mTransition.UnbufferedState() ==
|
||||
unbufferedTransition.NextState());
|
||||
|
||||
aInput += toRead;
|
||||
aLength -= toRead;
|
||||
mToReadUnbuffered -= toRead;
|
||||
if (mToReadUnbuffered != 0) {
|
||||
return Nothing(); // Need more input.
|
||||
}
|
||||
|
||||
// We're done with the unbuffered read, so transition to the next state.
|
||||
mTransition = aFunc(mTransition.NextState(), nullptr, 0);
|
||||
if (IsTerminalState(mTransition.NextState())) {
|
||||
return Some(mTransition.NextState()); // Done!
|
||||
}
|
||||
} else if (0 < mBuffer.length()) {
|
||||
// We're continuing a buffered read.
|
||||
|
||||
MOZ_ASSERT(mToReadUnbuffered == 0,
|
||||
"Shouldn't be continuing an unbuffered read and a buffered "
|
||||
"read at the same time");
|
||||
MOZ_ASSERT(mBuffer.length() < mTransition.Size(),
|
||||
"Buffered more than we needed?");
|
||||
|
||||
size_t toRead = std::min(aLength, mTransition.Size() - mBuffer.length());
|
||||
|
||||
mBuffer.append(aInput, toRead);
|
||||
aInput += toRead;
|
||||
aLength -= toRead;
|
||||
if (mBuffer.length() != mTransition.Size()) {
|
||||
return Nothing(); // Need more input.
|
||||
}
|
||||
|
||||
// We've buffered everything, so transition to the next state.
|
||||
mTransition =
|
||||
aFunc(mTransition.NextState(), mBuffer.begin(), mBuffer.length());
|
||||
mBuffer.clear();
|
||||
if (IsTerminalState(mTransition.NextState())) {
|
||||
return Some(mTransition.NextState()); // Done!
|
||||
}
|
||||
}
|
||||
|
||||
MOZ_ASSERT(mToReadUnbuffered == 0);
|
||||
MOZ_ASSERT(mBuffer.empty());
|
||||
|
||||
// Process states as long as we continue to have enough input to do so.
|
||||
while (mTransition.Size() <= aLength) {
|
||||
size_t toRead = mTransition.Size();
|
||||
|
||||
if (mTransition.Buffering() == BufferingStrategy::BUFFERED) {
|
||||
mTransition = aFunc(mTransition.NextState(), aInput, toRead);
|
||||
} else {
|
||||
MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::UNBUFFERED);
|
||||
|
||||
// Call aFunc with the unbuffered state to indicate that we're in the
|
||||
// middle of an unbuffered read. We enforce that any state transition
|
||||
// passed back to us is either a terminal states or takes us back to the
|
||||
// unbuffered state.
|
||||
LexerTransition<State> unbufferedTransition =
|
||||
aFunc(mTransition.UnbufferedState(), aInput, toRead);
|
||||
if (IsTerminalState(unbufferedTransition.NextState())) {
|
||||
mTransition = unbufferedTransition;
|
||||
return Some(mTransition.NextState()); // Done!
|
||||
}
|
||||
MOZ_ASSERT(mTransition.UnbufferedState() ==
|
||||
unbufferedTransition.NextState());
|
||||
|
||||
// We're done with the unbuffered read, so transition to the next state.
|
||||
mTransition = aFunc(mTransition.NextState(), nullptr, 0);
|
||||
}
|
||||
|
||||
aInput += toRead;
|
||||
aLength -= toRead;
|
||||
|
||||
if (IsTerminalState(mTransition.NextState())) {
|
||||
return Some(mTransition.NextState()); // Done!
|
||||
}
|
||||
}
|
||||
|
||||
if (aLength == 0) {
|
||||
// We finished right at a transition point. Just wait for more data.
|
||||
return Nothing();
|
||||
}
|
||||
|
||||
// If the next state is unbuffered, deliver what we can and then wait.
|
||||
if (mTransition.Buffering() == BufferingStrategy::UNBUFFERED) {
|
||||
LexerTransition<State> unbufferedTransition =
|
||||
aFunc(mTransition.UnbufferedState(), aInput, aLength);
|
||||
if (IsTerminalState(unbufferedTransition.NextState())) {
|
||||
mTransition = unbufferedTransition;
|
||||
return Some(mTransition.NextState()); // Done!
|
||||
}
|
||||
MOZ_ASSERT(mTransition.UnbufferedState() ==
|
||||
unbufferedTransition.NextState());
|
||||
|
||||
mToReadUnbuffered = mTransition.Size() - aLength;
|
||||
return Nothing(); // Need more input.
|
||||
}
|
||||
|
||||
// If the next state is buffered, buffer what we can and then wait.
|
||||
MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::BUFFERED);
|
||||
if (!mBuffer.reserve(mTransition.Size())) {
|
||||
return Some(State::FAILURE); // Done due to allocation failure.
|
||||
}
|
||||
mBuffer.append(aInput, aLength);
|
||||
return Nothing(); // Need more input.
|
||||
}
|
||||
|
||||
private:
|
||||
Vector<char, InlineBufferSize> mBuffer;
|
||||
LexerTransition<State> mTransition;
|
||||
size_t mToReadUnbuffered;
|
||||
};
|
||||
|
||||
} // namespace image
|
||||
} // namespace mozilla
|
||||
|
||||
#endif // mozilla_image_StreamingLexer_h
|
@ -17,10 +17,10 @@
|
||||
namespace mozilla {
|
||||
namespace image {
|
||||
|
||||
#define ICONCOUNTOFFSET 4
|
||||
#define DIRENTRYOFFSET 6
|
||||
#define BITMAPINFOSIZE 40
|
||||
#define PREFICONSIZE 16
|
||||
// Constants.
|
||||
static const uint32_t ICOHEADERSIZE = 6;
|
||||
static const uint32_t BITMAPINFOSIZE = 40;
|
||||
static const uint32_t PREFICONSIZE = 16;
|
||||
|
||||
// ----------------------------------------
|
||||
// Actual Data Processing
|
||||
@ -57,22 +57,17 @@ nsICODecoder::GetNumColors()
|
||||
return numColors;
|
||||
}
|
||||
|
||||
|
||||
nsICODecoder::nsICODecoder(RasterImage* aImage)
|
||||
: Decoder(aImage)
|
||||
{
|
||||
mPos = mImageOffset = mCurrIcon = mNumIcons = mBPP = mRowBytes = 0;
|
||||
mIsPNG = false;
|
||||
mRow = nullptr;
|
||||
mOldLine = mCurLine = 1; // Otherwise decoder will never start
|
||||
}
|
||||
|
||||
nsICODecoder::~nsICODecoder()
|
||||
{
|
||||
if (mRow) {
|
||||
free(mRow);
|
||||
}
|
||||
}
|
||||
: Decoder(aImage)
|
||||
, mLexer(Transition::To(ICOState::HEADER, ICOHEADERSIZE))
|
||||
, mBestResourceDelta(INT_MIN)
|
||||
, mBestResourceColorDepth(0)
|
||||
, mNumIcons(0)
|
||||
, mCurrIcon(0)
|
||||
, mBPP(0)
|
||||
, mMaskRowSize(0)
|
||||
, mCurrMaskLine(0)
|
||||
{ }
|
||||
|
||||
void
|
||||
nsICODecoder::FinishInternal()
|
||||
@ -80,11 +75,6 @@ nsICODecoder::FinishInternal()
|
||||
// We shouldn't be called in error cases
|
||||
MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!");
|
||||
|
||||
// Finish the internally used decoder as well.
|
||||
if (mContainedDecoder && !mContainedDecoder->HasError()) {
|
||||
mContainedDecoder->FinishInternal();
|
||||
}
|
||||
|
||||
GetFinalStateFromContainedDecoder();
|
||||
}
|
||||
|
||||
@ -101,6 +91,9 @@ nsICODecoder::GetFinalStateFromContainedDecoder()
|
||||
return;
|
||||
}
|
||||
|
||||
// Finish the internally used decoder.
|
||||
mContainedDecoder->CompleteDecode();
|
||||
|
||||
mDecodeDone = mContainedDecoder->GetDecodeDone();
|
||||
mDataError = mDataError || mContainedDecoder->HasDataError();
|
||||
mFailCode = NS_SUCCEEDED(mFailCode) ? mContainedDecoder->GetDecoderError()
|
||||
@ -109,6 +102,8 @@ nsICODecoder::GetFinalStateFromContainedDecoder()
|
||||
mProgress |= mContainedDecoder->TakeProgress();
|
||||
mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect());
|
||||
mCurrentFrame = mContainedDecoder->GetCurrentFrameRef();
|
||||
|
||||
MOZ_ASSERT(HasError() || !mCurrentFrame || mCurrentFrame->IsImageComplete());
|
||||
}
|
||||
|
||||
// Returns a buffer filled with the bitmap file header in little endian:
|
||||
@ -206,10 +201,11 @@ nsICODecoder::FixBitmapWidth(int8_t* bih)
|
||||
}
|
||||
|
||||
// The BMP information header's bits per pixel should be trusted
|
||||
// more than what we have. Usually the ICO's BPP is set to 0
|
||||
// more than what we have. Usually the ICO's BPP is set to 0.
|
||||
int32_t
|
||||
nsICODecoder::ExtractBPPFromBitmap(int8_t* bih)
|
||||
nsICODecoder::ReadBPP(const char* aBIH)
|
||||
{
|
||||
const int8_t* bih = reinterpret_cast<const int8_t*>(aBIH);
|
||||
int32_t bitsPerPixel;
|
||||
memcpy(&bitsPerPixel, bih + 14, sizeof(bitsPerPixel));
|
||||
NativeEndian::swapFromLittleEndianInPlace(&bitsPerPixel, 1);
|
||||
@ -217,8 +213,9 @@ nsICODecoder::ExtractBPPFromBitmap(int8_t* bih)
|
||||
}
|
||||
|
||||
int32_t
|
||||
nsICODecoder::ExtractBIHSizeFromBitmap(int8_t* bih)
|
||||
nsICODecoder::ReadBIHSize(const char* aBIH)
|
||||
{
|
||||
const int8_t* bih = reinterpret_cast<const int8_t*>(aBIH);
|
||||
int32_t headerSize;
|
||||
memcpy(&headerSize, bih, sizeof(headerSize));
|
||||
NativeEndian::swapFromLittleEndianInPlace(&headerSize, 1);
|
||||
@ -235,205 +232,126 @@ nsICODecoder::SetHotSpotIfCursor()
|
||||
mImageMetadata.SetHotspot(mDirEntry.mXHotspot, mDirEntry.mYHotspot);
|
||||
}
|
||||
|
||||
void
|
||||
nsICODecoder::WriteInternal(const char* aBuffer, uint32_t aCount)
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::ReadHeader(const char* aData)
|
||||
{
|
||||
MOZ_ASSERT(!HasError(), "Shouldn't call WriteInternal after error!");
|
||||
MOZ_ASSERT(aBuffer);
|
||||
MOZ_ASSERT(aCount > 0);
|
||||
|
||||
while (aCount && (mPos < ICONCOUNTOFFSET)) { // Skip to the # of icons.
|
||||
if (mPos == 2) { // if the third byte is 1: This is an icon, 2: a cursor
|
||||
if ((*aBuffer != 1) && (*aBuffer != 2)) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
mIsCursor = (*aBuffer == 2);
|
||||
}
|
||||
mPos++; aBuffer++; aCount--;
|
||||
}
|
||||
|
||||
if (mPos == ICONCOUNTOFFSET && aCount >= 2) {
|
||||
mNumIcons =
|
||||
LittleEndian::readUint16(reinterpret_cast<const uint16_t*>(aBuffer));
|
||||
aBuffer += 2;
|
||||
mPos += 2;
|
||||
aCount -= 2;
|
||||
// If the third byte is 1, this is an icon. If 2, a cursor.
|
||||
if ((aData[2] != 1) && (aData[2] != 2)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
mIsCursor = (aData[2] == 2);
|
||||
|
||||
// The fifth and sixth bytes specify the number of resources in the file.
|
||||
mNumIcons =
|
||||
LittleEndian::readUint16(reinterpret_cast<const uint16_t*>(aData + 4));
|
||||
if (mNumIcons == 0) {
|
||||
return; // Nothing to do.
|
||||
return Transition::Terminate(ICOState::SUCCESS); // Nothing to do.
|
||||
}
|
||||
|
||||
uint16_t colorDepth = 0;
|
||||
|
||||
// If we didn't get a #-moz-resolution, default to PREFICONSIZE.
|
||||
if (mResolution.width == 0 && mResolution.height == 0) {
|
||||
mResolution.SizeTo(PREFICONSIZE, PREFICONSIZE);
|
||||
}
|
||||
|
||||
// A measure of the difference in size between the entry we've found
|
||||
// and the requested size. We will choose the smallest image that is
|
||||
// >= requested size (i.e. we assume it's better to downscale a larger
|
||||
return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE);
|
||||
}
|
||||
|
||||
size_t
|
||||
nsICODecoder::FirstResourceOffset() const
|
||||
{
|
||||
MOZ_ASSERT(mNumIcons > 0,
|
||||
"Calling FirstResourceOffset before processing header");
|
||||
|
||||
// The first resource starts right after the directory, which starts right
|
||||
// after the ICO header.
|
||||
return ICOHEADERSIZE + mNumIcons * ICODIRENTRYSIZE;
|
||||
}
|
||||
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::ReadDirEntry(const char* aData)
|
||||
{
|
||||
mCurrIcon++;
|
||||
|
||||
// Read the directory entry.
|
||||
IconDirEntry e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
memcpy(&e.mWidth, aData, sizeof(e.mWidth));
|
||||
memcpy(&e.mHeight, aData + 1, sizeof(e.mHeight));
|
||||
memcpy(&e.mColorCount, aData + 2, sizeof(e.mColorCount));
|
||||
memcpy(&e.mReserved, aData + 3, sizeof(e.mReserved));
|
||||
memcpy(&e.mPlanes, aData + 4, sizeof(e.mPlanes));
|
||||
e.mPlanes = LittleEndian::readUint16(&e.mPlanes);
|
||||
memcpy(&e.mBitCount, aData + 6, sizeof(e.mBitCount));
|
||||
e.mBitCount = LittleEndian::readUint16(&e.mBitCount);
|
||||
memcpy(&e.mBytesInRes, aData + 8, sizeof(e.mBytesInRes));
|
||||
e.mBytesInRes = LittleEndian::readUint32(&e.mBytesInRes);
|
||||
memcpy(&e.mImageOffset, aData + 12, sizeof(e.mImageOffset));
|
||||
e.mImageOffset = LittleEndian::readUint32(&e.mImageOffset);
|
||||
|
||||
// Calculate the delta between this image's size and the desired size, so we
|
||||
// can see if it is better than our current-best option. In the case of
|
||||
// several equally-good images, we use the last one. "Better" in this case is
|
||||
// determined by |delta|, a measure of the difference in size between the
|
||||
// entry we've found and the requested size. We will choose the smallest image
|
||||
// that is >= requested size (i.e. we assume it's better to downscale a larger
|
||||
// icon than to upscale a smaller one).
|
||||
int32_t diff = INT_MIN;
|
||||
int32_t delta = GetRealWidth(e) - mResolution.width +
|
||||
GetRealHeight(e) - mResolution.height;
|
||||
if (e.mBitCount >= mBestResourceColorDepth &&
|
||||
((mBestResourceDelta < 0 && delta >= mBestResourceDelta) ||
|
||||
(delta >= 0 && delta <= mBestResourceDelta))) {
|
||||
mBestResourceDelta = delta;
|
||||
|
||||
// Loop through each entry's dir entry
|
||||
while (mCurrIcon < mNumIcons) {
|
||||
if (mPos >= DIRENTRYOFFSET + (mCurrIcon * sizeof(mDirEntryArray)) &&
|
||||
mPos < DIRENTRYOFFSET + ((mCurrIcon + 1) * sizeof(mDirEntryArray))) {
|
||||
uint32_t toCopy = sizeof(mDirEntryArray) -
|
||||
(mPos - DIRENTRYOFFSET - mCurrIcon *
|
||||
sizeof(mDirEntryArray));
|
||||
if (toCopy > aCount) {
|
||||
toCopy = aCount;
|
||||
}
|
||||
memcpy(mDirEntryArray + sizeof(mDirEntryArray) - toCopy, aBuffer, toCopy);
|
||||
mPos += toCopy;
|
||||
aCount -= toCopy;
|
||||
aBuffer += toCopy;
|
||||
}
|
||||
if (aCount == 0) {
|
||||
return; // Need more data
|
||||
// Ensure mImageOffset is >= size of the direntry headers (bug #245631).
|
||||
if (e.mImageOffset < FirstResourceOffset()) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
IconDirEntry e;
|
||||
if (mPos == (DIRENTRYOFFSET + ICODIRENTRYSIZE) +
|
||||
(mCurrIcon * sizeof(mDirEntryArray))) {
|
||||
mCurrIcon++;
|
||||
ProcessDirEntry(e);
|
||||
// We can't use GetRealWidth and GetRealHeight here because those operate
|
||||
// on mDirEntry, here we are going through each item in the directory.
|
||||
// Calculate the delta between this image's size and the desired size,
|
||||
// so we can see if it is better than our current-best option.
|
||||
// In the case of several equally-good images, we use the last one.
|
||||
int32_t delta = (e.mWidth == 0 ? 256 : e.mWidth) - mResolution.width +
|
||||
(e.mHeight == 0 ? 256 : e.mHeight) - mResolution.height;
|
||||
if (e.mBitCount >= colorDepth &&
|
||||
((diff < 0 && delta >= diff) || (delta >= 0 && delta <= diff))) {
|
||||
diff = delta;
|
||||
mImageOffset = e.mImageOffset;
|
||||
|
||||
// ensure mImageOffset is >= size of the direntry headers (bug #245631)
|
||||
uint32_t minImageOffset = DIRENTRYOFFSET +
|
||||
mNumIcons * sizeof(mDirEntryArray);
|
||||
if (mImageOffset < minImageOffset) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
|
||||
colorDepth = e.mBitCount;
|
||||
memcpy(&mDirEntry, &e, sizeof(IconDirEntry));
|
||||
}
|
||||
}
|
||||
mBestResourceColorDepth = e.mBitCount;
|
||||
mDirEntry = e;
|
||||
}
|
||||
|
||||
if (mPos < mImageOffset) {
|
||||
// Skip to (or at least towards) the desired image offset
|
||||
uint32_t toSkip = mImageOffset - mPos;
|
||||
if (toSkip > aCount) {
|
||||
toSkip = aCount;
|
||||
}
|
||||
|
||||
mPos += toSkip;
|
||||
aBuffer += toSkip;
|
||||
aCount -= toSkip;
|
||||
if (mCurrIcon == mNumIcons) {
|
||||
size_t offsetToResource = mDirEntry.mImageOffset - FirstResourceOffset();
|
||||
return Transition::ToUnbuffered(ICOState::FOUND_RESOURCE,
|
||||
ICOState::SKIP_TO_RESOURCE,
|
||||
offsetToResource);
|
||||
}
|
||||
|
||||
// If we are within the first PNGSIGNATURESIZE bytes of the image data,
|
||||
// then we have either a BMP or a PNG. We use the first PNGSIGNATURESIZE
|
||||
// bytes to determine which one we have.
|
||||
if (mCurrIcon == mNumIcons && mPos >= mImageOffset &&
|
||||
mPos < mImageOffset + PNGSIGNATURESIZE) {
|
||||
uint32_t toCopy = PNGSIGNATURESIZE - (mPos - mImageOffset);
|
||||
if (toCopy > aCount) {
|
||||
toCopy = aCount;
|
||||
return Transition::To(ICOState::DIR_ENTRY, ICODIRENTRYSIZE);
|
||||
}
|
||||
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::SniffResource(const char* aData)
|
||||
{
|
||||
// We use the first PNGSIGNATURESIZE bytes to determine whether this resource
|
||||
// is a PNG or a BMP.
|
||||
bool isPNG = !memcmp(aData, nsPNGDecoder::pngSignatureBytes,
|
||||
PNGSIGNATURESIZE);
|
||||
if (isPNG) {
|
||||
// Create a PNG decoder which will do the rest of the work for us.
|
||||
mContainedDecoder = new nsPNGDecoder(mImage);
|
||||
mContainedDecoder->SetMetadataDecode(IsMetadataDecode());
|
||||
mContainedDecoder->SetDecoderFlags(GetDecoderFlags());
|
||||
mContainedDecoder->SetSurfaceFlags(GetSurfaceFlags());
|
||||
mContainedDecoder->Init();
|
||||
|
||||
if (!WriteToContainedDecoder(aData, PNGSIGNATURESIZE)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
memcpy(mSignature + (mPos - mImageOffset), aBuffer, toCopy);
|
||||
mPos += toCopy;
|
||||
aCount -= toCopy;
|
||||
aBuffer += toCopy;
|
||||
|
||||
mIsPNG = !memcmp(mSignature, nsPNGDecoder::pngSignatureBytes,
|
||||
PNGSIGNATURESIZE);
|
||||
if (mIsPNG) {
|
||||
mContainedDecoder = new nsPNGDecoder(mImage);
|
||||
mContainedDecoder->SetMetadataDecode(IsMetadataDecode());
|
||||
mContainedDecoder->SetDecoderFlags(GetDecoderFlags());
|
||||
mContainedDecoder->SetSurfaceFlags(GetSurfaceFlags());
|
||||
mContainedDecoder->Init();
|
||||
if (!WriteToContainedDecoder(mSignature, PNGSIGNATURESIZE)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a PNG, let the PNG decoder do all of the rest of the work
|
||||
if (mIsPNG && mContainedDecoder && mPos >= mImageOffset + PNGSIGNATURESIZE) {
|
||||
if (!WriteToContainedDecoder(aBuffer, aCount)) {
|
||||
return;
|
||||
if (mDirEntry.mBytesInRes <= PNGSIGNATURESIZE) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
if (!HasSize() && mContainedDecoder->HasSize()) {
|
||||
nsIntSize size = mContainedDecoder->GetSize();
|
||||
PostSize(size.width, size.height);
|
||||
}
|
||||
|
||||
mPos += aCount;
|
||||
aBuffer += aCount;
|
||||
aCount = 0;
|
||||
|
||||
// Raymond Chen says that 32bpp only are valid PNG ICOs
|
||||
// http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx
|
||||
if (!IsMetadataDecode() &&
|
||||
!static_cast<nsPNGDecoder*>(mContainedDecoder.get())->IsValidICO()) {
|
||||
PostDataError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We've processed all of the icon dir entries and are within the
|
||||
// bitmap info size
|
||||
if (!mIsPNG && mCurrIcon == mNumIcons && mPos >= mImageOffset &&
|
||||
mPos >= mImageOffset + PNGSIGNATURESIZE &&
|
||||
mPos < mImageOffset + BITMAPINFOSIZE) {
|
||||
|
||||
// As we were decoding, we did not know if we had a PNG signature or the
|
||||
// start of a bitmap information header. At this point we know we had
|
||||
// a bitmap information header and not a PNG signature, so fill the bitmap
|
||||
// information header with the data it should already have.
|
||||
memcpy(mBIHraw, mSignature, PNGSIGNATURESIZE);
|
||||
|
||||
// We've found the icon.
|
||||
uint32_t toCopy = sizeof(mBIHraw) - (mPos - mImageOffset);
|
||||
if (toCopy > aCount) {
|
||||
toCopy = aCount;
|
||||
}
|
||||
|
||||
memcpy(mBIHraw + (mPos - mImageOffset), aBuffer, toCopy);
|
||||
mPos += toCopy;
|
||||
aCount -= toCopy;
|
||||
aBuffer += toCopy;
|
||||
}
|
||||
|
||||
// If we have a BMP inside the ICO and we have read the BIH header
|
||||
if (!mIsPNG && mPos == mImageOffset + BITMAPINFOSIZE) {
|
||||
|
||||
// Make sure we have a sane value for the bitmap information header
|
||||
int32_t bihSize = ExtractBIHSizeFromBitmap(reinterpret_cast<int8_t*>
|
||||
(mBIHraw));
|
||||
if (bihSize != BITMAPINFOSIZE) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
// We are extracting the BPP from the BIH header as it should be trusted
|
||||
// over the one we have from the icon header
|
||||
mBPP = ExtractBPPFromBitmap(reinterpret_cast<int8_t*>(mBIHraw));
|
||||
|
||||
// Init the bitmap decoder which will do most of the work for us
|
||||
// It will do everything except the AND mask which isn't present in bitmaps
|
||||
// bmpDecoder is for local scope ease, it will be freed by mContainedDecoder
|
||||
// Read in the rest of the PNG unbuffered.
|
||||
size_t toRead = mDirEntry.mBytesInRes - PNGSIGNATURESIZE;
|
||||
return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE,
|
||||
ICOState::READ_PNG,
|
||||
toRead);
|
||||
} else {
|
||||
// Create a BMP decoder which will do most of the work for us; the exception
|
||||
// is the AND mask, which isn't present in standalone BMPs.
|
||||
nsBMPDecoder* bmpDecoder = new nsBMPDecoder(mImage);
|
||||
mContainedDecoder = bmpDecoder;
|
||||
bmpDecoder->SetUseAlphaData(true);
|
||||
@ -442,169 +360,271 @@ nsICODecoder::WriteInternal(const char* aBuffer, uint32_t aCount)
|
||||
mContainedDecoder->SetSurfaceFlags(GetSurfaceFlags());
|
||||
mContainedDecoder->Init();
|
||||
|
||||
// The ICO format when containing a BMP does not include the 14 byte
|
||||
// bitmap file header. To use the code of the BMP decoder we need to
|
||||
// generate this header ourselves and feed it to the BMP decoder.
|
||||
int8_t bfhBuffer[BMPFILEHEADERSIZE];
|
||||
if (!FillBitmapFileHeaderBuffer(bfhBuffer)) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
if (!WriteToContainedDecoder((const char*)bfhBuffer, sizeof(bfhBuffer))) {
|
||||
return;
|
||||
// Make sure we have a sane size for the bitmap information header.
|
||||
int32_t bihSize = ReadBIHSize(aData);
|
||||
if (bihSize != static_cast<int32_t>(BITMAPINFOSIZE)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
// Setup the cursor hot spot if one is present
|
||||
SetHotSpotIfCursor();
|
||||
// Buffer the first part of the bitmap information header.
|
||||
memcpy(mBIHraw, aData, PNGSIGNATURESIZE);
|
||||
|
||||
// Fix the ICO height from the BIH.
|
||||
// Fix the height on the BIH to be /2 so our BMP decoder will understand.
|
||||
if (!FixBitmapHeight(reinterpret_cast<int8_t*>(mBIHraw))) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
// Read in the rest of the bitmap information header.
|
||||
return Transition::To(ICOState::READ_BIH,
|
||||
BITMAPINFOSIZE - PNGSIGNATURESIZE);
|
||||
}
|
||||
}
|
||||
|
||||
// Fix the ICO width from the BIH.
|
||||
if (!FixBitmapWidth(reinterpret_cast<int8_t*>(mBIHraw))) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
|
||||
// Write out the BMP's bitmap info header
|
||||
if (!WriteToContainedDecoder(mBIHraw, sizeof(mBIHraw))) {
|
||||
return;
|
||||
}
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::ReadPNG(const char* aData, uint32_t aLen)
|
||||
{
|
||||
if (!WriteToContainedDecoder(aData, aLen)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
if (!HasSize() && mContainedDecoder->HasSize()) {
|
||||
nsIntSize size = mContainedDecoder->GetSize();
|
||||
PostSize(size.width, size.height);
|
||||
|
||||
// We have the size. If we're doing a metadata decode, we're done.
|
||||
if (IsMetadataDecode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sometimes the ICO BPP header field is not filled out
|
||||
// so we should trust the contained resource over our own
|
||||
// information.
|
||||
mBPP = bmpDecoder->GetBitsPerPixel();
|
||||
|
||||
// Check to make sure we have valid color settings
|
||||
uint16_t numColors = GetNumColors();
|
||||
if (numColors == (uint16_t)-1) {
|
||||
PostDataError();
|
||||
return;
|
||||
return Transition::Terminate(ICOState::SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a BMP
|
||||
if (!mIsPNG && mContainedDecoder && mPos >= mImageOffset + BITMAPINFOSIZE) {
|
||||
uint16_t numColors = GetNumColors();
|
||||
if (numColors == (uint16_t)-1) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
// Feed the actual image data (not including headers) into the BMP decoder
|
||||
uint32_t bmpDataOffset = mDirEntry.mImageOffset + BITMAPINFOSIZE;
|
||||
uint32_t bmpDataEnd = mDirEntry.mImageOffset + BITMAPINFOSIZE +
|
||||
static_cast<nsBMPDecoder*>(mContainedDecoder.get())->
|
||||
GetCompressedImageSize() +
|
||||
4 * numColors;
|
||||
// Raymond Chen says that 32bpp only are valid PNG ICOs
|
||||
// http://blogs.msdn.com/b/oldnewthing/archive/2010/10/22/10079192.aspx
|
||||
if (!IsMetadataDecode() &&
|
||||
!static_cast<nsPNGDecoder*>(mContainedDecoder.get())->IsValidICO()) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
// If we are feeding in the core image data, but we have not yet
|
||||
// reached the ICO's 'AND buffer mask'
|
||||
if (mPos >= bmpDataOffset && mPos < bmpDataEnd) {
|
||||
return Transition::ContinueUnbuffered(ICOState::READ_PNG);
|
||||
}
|
||||
|
||||
// Figure out how much data the BMP decoder wants
|
||||
uint32_t toFeed = bmpDataEnd - mPos;
|
||||
if (toFeed > aCount) {
|
||||
toFeed = aCount;
|
||||
}
|
||||
|
||||
if (!WriteToContainedDecoder(aBuffer, toFeed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mPos += toFeed;
|
||||
aCount -= toFeed;
|
||||
aBuffer += toFeed;
|
||||
}
|
||||
|
||||
// If the bitmap is fully processed, treat any left over data as the ICO's
|
||||
// 'AND buffer mask' which appears after the bitmap resource.
|
||||
if (!mIsPNG && mPos >= bmpDataEnd) {
|
||||
nsRefPtr<nsBMPDecoder> bmpDecoder =
|
||||
static_cast<nsBMPDecoder*>(mContainedDecoder.get());
|
||||
|
||||
// There may be an optional AND bit mask after the data. This is
|
||||
// only used if the alpha data is not already set. The alpha data
|
||||
// is used for 32bpp bitmaps as per the comment in ICODecoder.h
|
||||
// The alpha mask should be checked in all other cases.
|
||||
if (bmpDecoder->GetBitsPerPixel() != 32 || !bmpDecoder->HasAlphaData()) {
|
||||
uint32_t rowSize = ((GetRealWidth() + 31) / 32) * 4; // + 31 to round up
|
||||
if (mPos == bmpDataEnd) {
|
||||
mPos++;
|
||||
mRowBytes = 0;
|
||||
mCurLine = GetRealHeight();
|
||||
mRow = (uint8_t*)realloc(mRow, rowSize);
|
||||
if (!mRow) {
|
||||
PostDecoderError(NS_ERROR_OUT_OF_MEMORY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure memory has been allocated before decoding.
|
||||
MOZ_ASSERT(mRow, "mRow is null");
|
||||
if (!mRow) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t sawTransparency = 0;
|
||||
|
||||
while (mCurLine > 0 && aCount > 0) {
|
||||
uint32_t toCopy = std::min(rowSize - mRowBytes, aCount);
|
||||
if (toCopy) {
|
||||
memcpy(mRow + mRowBytes, aBuffer, toCopy);
|
||||
aCount -= toCopy;
|
||||
aBuffer += toCopy;
|
||||
mRowBytes += toCopy;
|
||||
}
|
||||
if (rowSize == mRowBytes) {
|
||||
mCurLine--;
|
||||
mRowBytes = 0;
|
||||
|
||||
uint32_t* imageData = bmpDecoder->GetImageData();
|
||||
if (!imageData) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
uint32_t* decoded = imageData + mCurLine * GetRealWidth();
|
||||
uint32_t* decoded_end = decoded + GetRealWidth();
|
||||
uint8_t* p = mRow;
|
||||
uint8_t* p_end = mRow + rowSize;
|
||||
while (p < p_end) {
|
||||
uint8_t idx = *p++;
|
||||
sawTransparency |= idx;
|
||||
for (uint8_t bit = 0x80; bit && decoded<decoded_end; bit >>= 1) {
|
||||
// Clear pixel completely for transparency.
|
||||
if (idx & bit) {
|
||||
*decoded = 0;
|
||||
}
|
||||
decoded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If any bits are set in sawTransparency, then we know at least one
|
||||
// pixel was transparent.
|
||||
if (sawTransparency) {
|
||||
PostHasTransparency();
|
||||
bmpDecoder->SetHasAlphaData();
|
||||
}
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::ReadBIH(const char* aData)
|
||||
{
|
||||
// Buffer the rest of the bitmap information header.
|
||||
memcpy(mBIHraw + PNGSIGNATURESIZE, aData, BITMAPINFOSIZE - PNGSIGNATURESIZE);
|
||||
|
||||
// Extracting the BPP from the BIH header; it should be trusted over the one
|
||||
// we have from the ICO header.
|
||||
mBPP = ReadBPP(mBIHraw);
|
||||
|
||||
// The ICO format when containing a BMP does not include the 14 byte
|
||||
// bitmap file header. To use the code of the BMP decoder we need to
|
||||
// generate this header ourselves and feed it to the BMP decoder.
|
||||
int8_t bfhBuffer[BMPFILEHEADERSIZE];
|
||||
if (!FillBitmapFileHeaderBuffer(bfhBuffer)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
if (!WriteToContainedDecoder(reinterpret_cast<const char*>(bfhBuffer),
|
||||
sizeof(bfhBuffer))) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
// Set up the cursor hot spot if one is present.
|
||||
SetHotSpotIfCursor();
|
||||
|
||||
// Fix the ICO height from the BIH. It needs to be halved so our BMP decoder
|
||||
// will understand, because the BMP decoder doesn't expect the alpha mask that
|
||||
// follows the BMP data in an ICO.
|
||||
if (!FixBitmapHeight(reinterpret_cast<int8_t*>(mBIHraw))) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
// Fix the ICO width from the BIH.
|
||||
if (!FixBitmapWidth(reinterpret_cast<int8_t*>(mBIHraw))) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
// Write out the BMP's bitmap info header.
|
||||
if (!WriteToContainedDecoder(mBIHraw, sizeof(mBIHraw))) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
nsIntSize size = mContainedDecoder->GetSize();
|
||||
PostSize(size.width, size.height);
|
||||
|
||||
// We have the size. If we're doing a metadata decode, we're done.
|
||||
if (IsMetadataDecode()) {
|
||||
return Transition::Terminate(ICOState::SUCCESS);
|
||||
}
|
||||
|
||||
// Sometimes the ICO BPP header field is not filled out so we should trust the
|
||||
// contained resource over our own information.
|
||||
// XXX(seth): Is this ever different than the value we obtained from
|
||||
// ReadBPP() above?
|
||||
nsRefPtr<nsBMPDecoder> bmpDecoder =
|
||||
static_cast<nsBMPDecoder*>(mContainedDecoder.get());
|
||||
mBPP = bmpDecoder->GetBitsPerPixel();
|
||||
|
||||
// Check to make sure we have valid color settings.
|
||||
uint16_t numColors = GetNumColors();
|
||||
if (numColors == uint16_t(-1)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
// Do we have an AND mask on this BMP? If so, we need to read it after we read
|
||||
// the BMP data itself.
|
||||
uint32_t bmpDataLength = bmpDecoder->GetCompressedImageSize() + 4 * numColors;
|
||||
bool hasANDMask = (BITMAPINFOSIZE + bmpDataLength) < mDirEntry.mBytesInRes;
|
||||
ICOState afterBMPState = hasANDMask ? ICOState::PREPARE_FOR_MASK
|
||||
: ICOState::FINISHED_RESOURCE;
|
||||
|
||||
// Read in the rest of the BMP unbuffered.
|
||||
return Transition::ToUnbuffered(afterBMPState,
|
||||
ICOState::READ_BMP,
|
||||
bmpDataLength);
|
||||
}
|
||||
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::ReadBMP(const char* aData, uint32_t aLen)
|
||||
{
|
||||
if (!WriteToContainedDecoder(aData, aLen)) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
return Transition::ContinueUnbuffered(ICOState::READ_BMP);
|
||||
}
|
||||
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::PrepareForMask()
|
||||
{
|
||||
nsRefPtr<nsBMPDecoder> bmpDecoder =
|
||||
static_cast<nsBMPDecoder*>(mContainedDecoder.get());
|
||||
|
||||
uint16_t numColors = GetNumColors();
|
||||
MOZ_ASSERT(numColors != uint16_t(-1));
|
||||
|
||||
// Determine the length of the AND mask.
|
||||
uint32_t bmpLengthWithHeader =
|
||||
BITMAPINFOSIZE + bmpDecoder->GetCompressedImageSize() + 4 * numColors;
|
||||
MOZ_ASSERT(bmpLengthWithHeader < mDirEntry.mBytesInRes);
|
||||
uint32_t maskLength = mDirEntry.mBytesInRes - bmpLengthWithHeader;
|
||||
|
||||
// If we have a 32-bpp BMP with alpha data, we ignore the AND mask. We can
|
||||
// also obviously ignore it if the image has zero width or zero height.
|
||||
if ((bmpDecoder->GetBitsPerPixel() == 32 && bmpDecoder->HasAlphaData()) ||
|
||||
GetRealWidth() == 0 || GetRealHeight() == 0) {
|
||||
return Transition::ToUnbuffered(ICOState::FINISHED_RESOURCE,
|
||||
ICOState::SKIP_MASK,
|
||||
maskLength);
|
||||
}
|
||||
|
||||
// Compute the row size for the mask.
|
||||
mMaskRowSize = ((GetRealWidth() + 31) / 32) * 4; // + 31 to round up
|
||||
|
||||
// If the expected size of the AND mask is larger than its actual size, then
|
||||
// we must have a truncated (and therefore corrupt) AND mask.
|
||||
uint32_t expectedLength = mMaskRowSize * GetRealHeight();
|
||||
if (maskLength < expectedLength) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
mCurrMaskLine = GetRealHeight();
|
||||
return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize);
|
||||
}
|
||||
|
||||
|
||||
LexerTransition<ICOState>
|
||||
nsICODecoder::ReadMaskRow(const char* aData)
|
||||
{
|
||||
mCurrMaskLine--;
|
||||
|
||||
nsRefPtr<nsBMPDecoder> bmpDecoder =
|
||||
static_cast<nsBMPDecoder*>(mContainedDecoder.get());
|
||||
|
||||
uint32_t* imageData = bmpDecoder->GetImageData();
|
||||
if (!imageData) {
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
|
||||
uint8_t sawTransparency = 0;
|
||||
uint32_t* decoded = imageData + mCurrMaskLine * GetRealWidth();
|
||||
uint32_t* decodedRowEnd = decoded + GetRealWidth();
|
||||
const uint8_t* mask = reinterpret_cast<const uint8_t*>(aData);
|
||||
const uint8_t* maskRowEnd = mask + mMaskRowSize;
|
||||
|
||||
// Iterate simultaneously through the AND mask and the image data.
|
||||
while (mask < maskRowEnd) {
|
||||
uint8_t idx = *mask++;
|
||||
sawTransparency |= idx;
|
||||
for (uint8_t bit = 0x80; bit && decoded < decodedRowEnd; bit >>= 1) {
|
||||
// Clear pixel completely for transparency.
|
||||
if (idx & bit) {
|
||||
*decoded = 0;
|
||||
}
|
||||
decoded++;
|
||||
}
|
||||
}
|
||||
|
||||
// If any bits are set in sawTransparency, then we know at least one pixel was
|
||||
// transparent.
|
||||
if (sawTransparency) {
|
||||
PostHasTransparency();
|
||||
bmpDecoder->SetHasAlphaData();
|
||||
}
|
||||
|
||||
if (mCurrMaskLine == 0) {
|
||||
return Transition::To(ICOState::FINISHED_RESOURCE, 0);
|
||||
}
|
||||
|
||||
return Transition::To(ICOState::READ_MASK_ROW, mMaskRowSize);
|
||||
}
|
||||
|
||||
void
|
||||
nsICODecoder::WriteInternal(const char* aBuffer, uint32_t aCount)
|
||||
{
|
||||
MOZ_ASSERT(!HasError(), "Shouldn't call WriteInternal after error!");
|
||||
MOZ_ASSERT(aBuffer);
|
||||
MOZ_ASSERT(aCount > 0);
|
||||
|
||||
Maybe<ICOState> terminalState =
|
||||
mLexer.Lex(aBuffer, aCount,
|
||||
[=](ICOState aState, const char* aData, size_t aLength) {
|
||||
switch (aState) {
|
||||
case ICOState::HEADER:
|
||||
return ReadHeader(aData);
|
||||
case ICOState::DIR_ENTRY:
|
||||
return ReadDirEntry(aData);
|
||||
case ICOState::SKIP_TO_RESOURCE:
|
||||
return Transition::ContinueUnbuffered(ICOState::SKIP_TO_RESOURCE);
|
||||
case ICOState::FOUND_RESOURCE:
|
||||
return Transition::To(ICOState::SNIFF_RESOURCE, PNGSIGNATURESIZE);
|
||||
case ICOState::SNIFF_RESOURCE:
|
||||
return SniffResource(aData);
|
||||
case ICOState::READ_PNG:
|
||||
return ReadPNG(aData, aLength);
|
||||
case ICOState::READ_BIH:
|
||||
return ReadBIH(aData);
|
||||
case ICOState::READ_BMP:
|
||||
return ReadBMP(aData, aLength);
|
||||
case ICOState::PREPARE_FOR_MASK:
|
||||
return PrepareForMask();
|
||||
case ICOState::READ_MASK_ROW:
|
||||
return ReadMaskRow(aData);
|
||||
case ICOState::SKIP_MASK:
|
||||
return Transition::ContinueUnbuffered(ICOState::SKIP_MASK);
|
||||
case ICOState::FINISHED_RESOURCE:
|
||||
return Transition::Terminate(ICOState::SUCCESS);
|
||||
default:
|
||||
MOZ_ASSERT_UNREACHABLE("Unknown ICOState");
|
||||
return Transition::Terminate(ICOState::FAILURE);
|
||||
}
|
||||
});
|
||||
|
||||
if (!terminalState) {
|
||||
return; // Need more data.
|
||||
}
|
||||
|
||||
if (*terminalState == ICOState::FAILURE) {
|
||||
PostDataError();
|
||||
return;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(*terminalState == ICOState::SUCCESS);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -614,7 +634,7 @@ nsICODecoder::WriteToContainedDecoder(const char* aBuffer, uint32_t aCount)
|
||||
mProgress |= mContainedDecoder->TakeProgress();
|
||||
mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect());
|
||||
if (mContainedDecoder->HasDataError()) {
|
||||
mDataError = mContainedDecoder->HasDataError();
|
||||
PostDataError();
|
||||
}
|
||||
if (mContainedDecoder->HasDecoderError()) {
|
||||
PostDecoderError(mContainedDecoder->GetDecoderError());
|
||||
@ -622,24 +642,5 @@ nsICODecoder::WriteToContainedDecoder(const char* aBuffer, uint32_t aCount)
|
||||
return !HasError();
|
||||
}
|
||||
|
||||
void
|
||||
nsICODecoder::ProcessDirEntry(IconDirEntry& aTarget)
|
||||
{
|
||||
memset(&aTarget, 0, sizeof(aTarget));
|
||||
memcpy(&aTarget.mWidth, mDirEntryArray, sizeof(aTarget.mWidth));
|
||||
memcpy(&aTarget.mHeight, mDirEntryArray + 1, sizeof(aTarget.mHeight));
|
||||
memcpy(&aTarget.mColorCount, mDirEntryArray + 2, sizeof(aTarget.mColorCount));
|
||||
memcpy(&aTarget.mReserved, mDirEntryArray + 3, sizeof(aTarget.mReserved));
|
||||
memcpy(&aTarget.mPlanes, mDirEntryArray + 4, sizeof(aTarget.mPlanes));
|
||||
aTarget.mPlanes = LittleEndian::readUint16(&aTarget.mPlanes);
|
||||
memcpy(&aTarget.mBitCount, mDirEntryArray + 6, sizeof(aTarget.mBitCount));
|
||||
aTarget.mBitCount = LittleEndian::readUint16(&aTarget.mBitCount);
|
||||
memcpy(&aTarget.mBytesInRes, mDirEntryArray + 8, sizeof(aTarget.mBytesInRes));
|
||||
aTarget.mBytesInRes = LittleEndian::readUint32(&aTarget.mBytesInRes);
|
||||
memcpy(&aTarget.mImageOffset, mDirEntryArray + 12,
|
||||
sizeof(aTarget.mImageOffset));
|
||||
aTarget.mImageOffset = LittleEndian::readUint32(&aTarget.mImageOffset);
|
||||
}
|
||||
|
||||
} // namespace image
|
||||
} // namespace mozilla
|
||||
|
@ -8,6 +8,7 @@
|
||||
#define mozilla_image_decoders_nsICODecoder_h
|
||||
|
||||
#include "nsAutoPtr.h"
|
||||
#include "StreamingLexer.h"
|
||||
#include "Decoder.h"
|
||||
#include "imgFrame.h"
|
||||
#include "nsBMPDecoder.h"
|
||||
@ -19,28 +20,55 @@ namespace image {
|
||||
|
||||
class RasterImage;
|
||||
|
||||
enum class ICOState
|
||||
{
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
HEADER,
|
||||
DIR_ENTRY,
|
||||
SKIP_TO_RESOURCE,
|
||||
FOUND_RESOURCE,
|
||||
SNIFF_RESOURCE,
|
||||
READ_PNG,
|
||||
READ_BIH,
|
||||
READ_BMP,
|
||||
PREPARE_FOR_MASK,
|
||||
READ_MASK_ROW,
|
||||
SKIP_MASK,
|
||||
FINISHED_RESOURCE
|
||||
};
|
||||
|
||||
class nsICODecoder : public Decoder
|
||||
{
|
||||
public:
|
||||
virtual ~nsICODecoder();
|
||||
virtual ~nsICODecoder() { }
|
||||
|
||||
// Obtains the width of the icon directory entry
|
||||
uint32_t GetRealWidth() const
|
||||
/// @return the width of the icon directory entry @aEntry.
|
||||
static uint32_t GetRealWidth(const IconDirEntry& aEntry)
|
||||
{
|
||||
return mDirEntry.mWidth == 0 ? 256 : mDirEntry.mWidth;
|
||||
return aEntry.mWidth == 0 ? 256 : aEntry.mWidth;
|
||||
}
|
||||
|
||||
// Obtains the height of the icon directory entry
|
||||
uint32_t GetRealHeight() const
|
||||
/// @return the width of the selected directory entry (mDirEntry).
|
||||
uint32_t GetRealWidth() const { return GetRealWidth(mDirEntry); }
|
||||
|
||||
/// @return the height of the icon directory entry @aEntry.
|
||||
static uint32_t GetRealHeight(const IconDirEntry& aEntry)
|
||||
{
|
||||
return mDirEntry.mHeight == 0 ? 256 : mDirEntry.mHeight;
|
||||
return aEntry.mHeight == 0 ? 256 : aEntry.mHeight;
|
||||
}
|
||||
|
||||
/// @return the height of the selected directory entry (mDirEntry).
|
||||
uint32_t GetRealHeight() const { return GetRealHeight(mDirEntry); }
|
||||
|
||||
virtual void SetResolution(const gfx::IntSize& aResolution) override
|
||||
{
|
||||
mResolution = aResolution;
|
||||
}
|
||||
|
||||
/// @return The offset from the beginning of the ICO to the first resource.
|
||||
size_t FirstResourceOffset() const;
|
||||
|
||||
virtual void WriteInternal(const char* aBuffer, uint32_t aCount) override;
|
||||
virtual void FinishInternal() override;
|
||||
virtual void FinishWithErrorInternal() override;
|
||||
@ -58,8 +86,6 @@ private:
|
||||
// Gets decoder state from the contained decoder so it's visible externally.
|
||||
void GetFinalStateFromContainedDecoder();
|
||||
|
||||
// Processes a single dir entry of the icon resource
|
||||
void ProcessDirEntry(IconDirEntry& aTarget);
|
||||
// Sets the hotspot property of if we have a cursor
|
||||
void SetHotSpotIfCursor();
|
||||
// Creates a bitmap file header buffer, returns true if successful
|
||||
@ -73,36 +99,36 @@ private:
|
||||
// Returns false if invalid information is contained within.
|
||||
bool FixBitmapWidth(int8_t* bih);
|
||||
// Extract bitmap info header size count from BMP information header
|
||||
int32_t ExtractBIHSizeFromBitmap(int8_t* bih);
|
||||
int32_t ReadBIHSize(const char* aBIH);
|
||||
// Extract bit count from BMP information header
|
||||
int32_t ExtractBPPFromBitmap(int8_t* bih);
|
||||
int32_t ReadBPP(const char* aBIH);
|
||||
// Calculates the row size in bytes for the AND mask table
|
||||
uint32_t CalcAlphaRowSize();
|
||||
// Obtains the number of colors from the BPP, mBPP must be filled in
|
||||
uint16_t GetNumColors();
|
||||
|
||||
gfx::IntSize mResolution; // The requested -moz-resolution for this icon.
|
||||
uint16_t mBPP; // Stores the images BPP
|
||||
uint32_t mPos; // Keeps track of the position we have decoded up until
|
||||
uint16_t mNumIcons; // Stores the number of icons in the ICO file
|
||||
uint16_t mCurrIcon; // Stores the current dir entry index we are processing
|
||||
uint32_t mImageOffset; // Stores the offset of the image data we want
|
||||
uint8_t* mRow; // Holds one raw line of the image
|
||||
int32_t mCurLine; // Line index of the image that's currently being decoded
|
||||
uint32_t mRowBytes; // How many bytes of the row were already received
|
||||
int32_t mOldLine; // Previous index of the line
|
||||
nsRefPtr<Decoder> mContainedDecoder; // Contains either a BMP or PNG resource
|
||||
LexerTransition<ICOState> ReadHeader(const char* aData);
|
||||
LexerTransition<ICOState> ReadDirEntry(const char* aData);
|
||||
LexerTransition<ICOState> SniffResource(const char* aData);
|
||||
LexerTransition<ICOState> ReadPNG(const char* aData, uint32_t aLen);
|
||||
LexerTransition<ICOState> ReadBIH(const char* aData);
|
||||
LexerTransition<ICOState> ReadBMP(const char* aData, uint32_t aLen);
|
||||
LexerTransition<ICOState> PrepareForMask();
|
||||
LexerTransition<ICOState> ReadMaskRow(const char* aData);
|
||||
|
||||
char mDirEntryArray[ICODIRENTRYSIZE]; // Holds the current dir entry buffer
|
||||
IconDirEntry mDirEntry; // Holds a decoded dir entry
|
||||
// Holds the potential bytes that can be a PNG signature
|
||||
char mSignature[PNGSIGNATURESIZE];
|
||||
// Holds the potential bytes for a bitmap information header
|
||||
char mBIHraw[40];
|
||||
// Stores whether or not the icon file we are processing has type 1 (icon)
|
||||
bool mIsCursor;
|
||||
// Stores whether or not the contained resource is a PNG
|
||||
bool mIsPNG;
|
||||
StreamingLexer<ICOState, 32> mLexer; // The lexer.
|
||||
nsRefPtr<Decoder> mContainedDecoder; // Either a BMP or PNG decoder.
|
||||
gfx::IntSize mResolution; // The requested -moz-resolution.
|
||||
char mBIHraw[40]; // The bitmap information header.
|
||||
IconDirEntry mDirEntry; // The dir entry for the selected resource.
|
||||
int32_t mBestResourceDelta; // Used to select the best resource.
|
||||
uint16_t mBestResourceColorDepth; // Used to select the best resource.
|
||||
uint16_t mNumIcons; // Stores the number of icons in the ICO file.
|
||||
uint16_t mCurrIcon; // Stores the current dir entry index we are processing.
|
||||
uint16_t mBPP; // The BPP of the resource we're decoding.
|
||||
uint32_t mMaskRowSize; // The size in bytes of each row in the BMP alpha mask.
|
||||
uint32_t mCurrMaskLine; // The line of the BMP alpha mask we're processing.
|
||||
bool mIsCursor; // Is this ICO a cursor?
|
||||
};
|
||||
|
||||
} // namespace image
|
||||
|
@ -212,8 +212,7 @@ TEST(ImageDecoders, ICOSingleChunk)
|
||||
CheckDecoderSingleChunk(GreenICOTestCase());
|
||||
}
|
||||
|
||||
// XXX(seth): Disabled. We'll fix this in bug 1196066.
|
||||
TEST(ImageDecoders, DISABLED_ICOMultiChunk)
|
||||
TEST(ImageDecoders, ICOMultiChunk)
|
||||
{
|
||||
CheckDecoderMultiChunk(GreenICOTestCase());
|
||||
}
|
||||
|
266
image/test/gtest/TestStreamingLexer.cpp
Normal file
266
image/test/gtest/TestStreamingLexer.cpp
Normal file
@ -0,0 +1,266 @@
|
||||
/* 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 "gtest/gtest.h"
|
||||
|
||||
#include "mozilla/Vector.h"
|
||||
#include "StreamingLexer.h"
|
||||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::image;
|
||||
|
||||
enum class TestState
|
||||
{
|
||||
ONE,
|
||||
TWO,
|
||||
THREE,
|
||||
UNBUFFERED,
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
};
|
||||
|
||||
void
|
||||
CheckData(const char* aData, size_t aLength)
|
||||
{
|
||||
EXPECT_TRUE(aLength == 3);
|
||||
EXPECT_EQ(1, aData[0]);
|
||||
EXPECT_EQ(2, aData[1]);
|
||||
EXPECT_EQ(3, aData[2]);
|
||||
}
|
||||
|
||||
LexerTransition<TestState>
|
||||
DoLex(TestState aState, const char* aData, size_t aLength)
|
||||
{
|
||||
switch (aState) {
|
||||
case TestState::ONE:
|
||||
CheckData(aData, aLength);
|
||||
return Transition::To(TestState::TWO, 3);
|
||||
case TestState::TWO:
|
||||
CheckData(aData, aLength);
|
||||
return Transition::To(TestState::THREE, 3);
|
||||
case TestState::THREE:
|
||||
CheckData(aData, aLength);
|
||||
return Transition::Terminate(TestState::SUCCESS);
|
||||
default:
|
||||
EXPECT_TRUE(false); // Shouldn't get here.
|
||||
return Transition::Terminate(TestState::FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
LexerTransition<TestState>
|
||||
DoLexWithUnbuffered(TestState aState, const char* aData, size_t aLength,
|
||||
Vector<char>& aUnbufferedVector)
|
||||
{
|
||||
switch (aState) {
|
||||
case TestState::ONE:
|
||||
CheckData(aData, aLength);
|
||||
return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
|
||||
case TestState::UNBUFFERED:
|
||||
EXPECT_TRUE(aLength <= 3);
|
||||
aUnbufferedVector.append(aData, aLength);
|
||||
return Transition::ContinueUnbuffered(TestState::UNBUFFERED);
|
||||
case TestState::TWO:
|
||||
CheckData(aUnbufferedVector.begin(), aUnbufferedVector.length());
|
||||
return Transition::To(TestState::THREE, 3);
|
||||
case TestState::THREE:
|
||||
CheckData(aData, aLength);
|
||||
return Transition::Terminate(TestState::SUCCESS);
|
||||
default:
|
||||
EXPECT_TRUE(false);
|
||||
return Transition::Terminate(TestState::FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
LexerTransition<TestState>
|
||||
DoLexWithUnbufferedTerminate(TestState aState, const char* aData, size_t aLength)
|
||||
{
|
||||
switch (aState) {
|
||||
case TestState::ONE:
|
||||
CheckData(aData, aLength);
|
||||
return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
|
||||
case TestState::UNBUFFERED:
|
||||
return Transition::Terminate(TestState::SUCCESS);
|
||||
default:
|
||||
EXPECT_TRUE(false);
|
||||
return Transition::Terminate(TestState::FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, SingleChunk)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
|
||||
// Test delivering all the data at once.
|
||||
Maybe<TestState> result = lexer.Lex(data, sizeof(data), DoLex);
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, SingleChunkWithUnbuffered)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
Vector<char> unbufferedVector;
|
||||
|
||||
// Test delivering all the data at once.
|
||||
Maybe<TestState> result =
|
||||
lexer.Lex(data, sizeof(data),
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
|
||||
});
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, ChunkPerState)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
|
||||
// Test delivering in perfectly-sized chunks, one per state.
|
||||
for (unsigned i = 0 ; i < 3 ; ++i) {
|
||||
Maybe<TestState> result = lexer.Lex(data + 3 * i, 3, DoLex);
|
||||
|
||||
if (i == 2) {
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
} else {
|
||||
EXPECT_TRUE(result.isNothing());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, ChunkPerStateWithUnbuffered)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
Vector<char> unbufferedVector;
|
||||
|
||||
// Test delivering in perfectly-sized chunks, one per state.
|
||||
for (unsigned i = 0 ; i < 3 ; ++i) {
|
||||
Maybe<TestState> result =
|
||||
lexer.Lex(data + 3 * i, 3,
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
|
||||
});
|
||||
|
||||
if (i == 2) {
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
} else {
|
||||
EXPECT_TRUE(result.isNothing());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, OneByteChunks)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
|
||||
// Test delivering in one byte chunks.
|
||||
for (unsigned i = 0 ; i < 9 ; ++i) {
|
||||
Maybe<TestState> result = lexer.Lex(data + i, 1, DoLex);
|
||||
|
||||
if (i == 8) {
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
} else {
|
||||
EXPECT_TRUE(result.isNothing());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, OneByteChunksWithUnbuffered)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
Vector<char> unbufferedVector;
|
||||
|
||||
// Test delivering in one byte chunks.
|
||||
for (unsigned i = 0 ; i < 9 ; ++i) {
|
||||
Maybe<TestState> result =
|
||||
lexer.Lex(data + i, 1,
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
|
||||
});
|
||||
|
||||
if (i == 8) {
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
} else {
|
||||
EXPECT_TRUE(result.isNothing());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, TerminateSuccess)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
|
||||
// Test that Terminate is "sticky".
|
||||
Maybe<TestState> result =
|
||||
lexer.Lex(data, sizeof(data),
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
EXPECT_TRUE(aState == TestState::ONE);
|
||||
return Transition::Terminate(TestState::SUCCESS);
|
||||
});
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
|
||||
result =
|
||||
lexer.Lex(data, sizeof(data),
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
EXPECT_TRUE(false); // Shouldn't get here.
|
||||
return Transition::Terminate(TestState::FAILURE);
|
||||
});
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, TerminateFailure)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
|
||||
// Test that Terminate is "sticky".
|
||||
Maybe<TestState> result =
|
||||
lexer.Lex(data, sizeof(data),
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
EXPECT_TRUE(aState == TestState::ONE);
|
||||
return Transition::Terminate(TestState::FAILURE);
|
||||
});
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::FAILURE, *result);
|
||||
|
||||
result =
|
||||
lexer.Lex(data, sizeof(data),
|
||||
[&](TestState aState, const char* aData, size_t aLength) {
|
||||
EXPECT_TRUE(false); // Shouldn't get here.
|
||||
return Transition::Terminate(TestState::FAILURE);
|
||||
});
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::FAILURE, *result);
|
||||
}
|
||||
|
||||
TEST(ImageStreamingLexer, TerminateUnbuffered)
|
||||
{
|
||||
StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
|
||||
char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
|
||||
|
||||
// Test that Terminate works during an unbuffered read.
|
||||
for (unsigned i = 0 ; i < 9 ; ++i) {
|
||||
Maybe<TestState> result =
|
||||
lexer.Lex(data + i, 1, DoLexWithUnbufferedTerminate);
|
||||
|
||||
if (i > 2) {
|
||||
EXPECT_TRUE(result.isSome());
|
||||
EXPECT_EQ(TestState::SUCCESS, *result);
|
||||
} else {
|
||||
EXPECT_TRUE(result.isNothing());
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ UNIFIED_SOURCES = [
|
||||
'TestDecoders.cpp',
|
||||
'TestDecodeToSurface.cpp',
|
||||
'TestMetadata.cpp',
|
||||
'TestStreamingLexer.cpp',
|
||||
]
|
||||
|
||||
TEST_HARNESS_FILES.gtest += [
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 92 B After Width: | Height: | Size: 92 B |
Binary file not shown.
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
@ -52,7 +52,8 @@ class ProfileEntry
|
||||
enum Flags {
|
||||
// Indicate whether a profile entry represents a CPP frame. If not set,
|
||||
// a JS frame is assumed by default. You're not allowed to publicly
|
||||
// change the frame type. Instead, call `setJsFrame` or `setCppFrame`.
|
||||
// change the frame type. Instead, initialize the ProfileEntry as either
|
||||
// a JS or CPP frame with `initJsFrame` or `initCppFrame` respectively.
|
||||
IS_CPP_ENTRY = 0x01,
|
||||
|
||||
// Indicate that copying the frame label is not necessary when taking a
|
||||
@ -106,12 +107,12 @@ class ProfileEntry
|
||||
void setLabel(const char* aString) volatile { string = aString; }
|
||||
const char* label() const volatile { return string; }
|
||||
|
||||
void setJsFrame(JSScript* aScript, jsbytecode* aPc) volatile {
|
||||
void initJsFrame(JSScript* aScript, jsbytecode* aPc) volatile {
|
||||
flags_ = 0;
|
||||
spOrScript = aScript;
|
||||
setPC(aPc);
|
||||
}
|
||||
void setCppFrame(void* aSp, uint32_t aLine) volatile {
|
||||
void initCppFrame(void* aSp, uint32_t aLine) volatile {
|
||||
flags_ = IS_CPP_ENTRY;
|
||||
spOrScript = aSp;
|
||||
lineOrPc = static_cast<int32_t>(aLine);
|
||||
@ -137,6 +138,8 @@ class ProfileEntry
|
||||
return flags_ & CATEGORY_MASK;
|
||||
}
|
||||
void setCategory(Category c) volatile {
|
||||
MOZ_ASSERT(c >= Category::FIRST);
|
||||
MOZ_ASSERT(c <= Category::LAST);
|
||||
flags_ &= ~CATEGORY_MASK;
|
||||
setFlag(static_cast<uint32_t>(c));
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
using js::gc::Cell;
|
||||
|
||||
mozilla::Atomic<int> MemProfiler::sActiveProfilerCount;
|
||||
mozilla::Atomic<uint32_t, mozilla::Relaxed> MemProfiler::sActiveProfilerCount;
|
||||
NativeProfiler* MemProfiler::sNativeProfiler;
|
||||
|
||||
GCHeapProfiler*
|
||||
|
8
js/src/jit-test/tests/basic/bug1206265.js
Normal file
8
js/src/jit-test/tests/basic/bug1206265.js
Normal file
@ -0,0 +1,8 @@
|
||||
x = [
|
||||
objectEmulatingUndefined(),
|
||||
function() {}
|
||||
];
|
||||
x.forEach(function() {});
|
||||
this.x.sort(function() {});
|
||||
assertEq(x[0] instanceof Function, false);
|
||||
assertEq(x[1] instanceof Function, true);
|
10
js/src/jit-test/tests/ion/bug1201850.js
Normal file
10
js/src/jit-test/tests/ion/bug1201850.js
Normal file
@ -0,0 +1,10 @@
|
||||
// |jit-test| error: too much recursion
|
||||
var tokenCodes = {
|
||||
get finally() {
|
||||
if (tokenCodes[arr[i]] !== i) {}
|
||||
}
|
||||
};
|
||||
var arr = ['finally'];
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (tokenCodes[arr[i]] !== i) {}
|
||||
}
|
26
js/src/jit-test/tests/ion/bug1203791.js
Normal file
26
js/src/jit-test/tests/ion/bug1203791.js
Normal file
@ -0,0 +1,26 @@
|
||||
function n(x) {
|
||||
try {
|
||||
Object.create(x);
|
||||
} catch(e){};
|
||||
}
|
||||
function m() {
|
||||
n();
|
||||
}
|
||||
var g = newGlobal();
|
||||
g.parent = this;
|
||||
g.eval(`
|
||||
var dbg = new Debugger();
|
||||
var parentw = dbg.addDebuggee(parent);
|
||||
var pw = parentw.makeDebuggeeValue(parent.p);
|
||||
var scriptw = pw.script;
|
||||
`);
|
||||
g.dbg.onIonCompilation = function(graph) {
|
||||
if (graph.scripts[0] != g.scriptw)
|
||||
return;
|
||||
m();
|
||||
};
|
||||
function p() {
|
||||
for (var res = false; !res; res = inIon()) {}
|
||||
}
|
||||
p();
|
||||
(function() {})();
|
@ -3517,7 +3517,7 @@ SetElemAddHasSameShapes(ICSetElem_DenseOrUnboxedArrayAdd* stub, JSObject* obj)
|
||||
return false;
|
||||
if (proto->as<NativeObject>().lastProperty() != nstub->shape(i + 1))
|
||||
return false;
|
||||
proto = obj->getProto();
|
||||
proto = proto->getProto();
|
||||
if (!proto) {
|
||||
if (i != stub->protoChainDepth() - 1)
|
||||
return false;
|
||||
@ -3535,22 +3535,53 @@ DenseOrUnboxedArraySetElemStubExists(JSContext* cx, ICStub::Kind kind,
|
||||
MOZ_ASSERT(kind == ICStub::SetElem_DenseOrUnboxedArray ||
|
||||
kind == ICStub::SetElem_DenseOrUnboxedArrayAdd);
|
||||
|
||||
if (obj->isSingleton())
|
||||
return false;
|
||||
|
||||
for (ICStubConstIterator iter = stub->beginChainConst(); !iter.atEnd(); iter++) {
|
||||
if (kind == ICStub::SetElem_DenseOrUnboxedArray && iter->isSetElem_DenseOrUnboxedArray()) {
|
||||
ICSetElem_DenseOrUnboxedArray* nstub = iter->toSetElem_DenseOrUnboxedArray();
|
||||
if (obj->maybeShape() == nstub->shape() && obj->getGroup(cx) == nstub->group())
|
||||
if (obj->maybeShape() == nstub->shape() && obj->group() == nstub->group())
|
||||
return true;
|
||||
}
|
||||
|
||||
if (kind == ICStub::SetElem_DenseOrUnboxedArrayAdd && iter->isSetElem_DenseOrUnboxedArrayAdd()) {
|
||||
ICSetElem_DenseOrUnboxedArrayAdd* nstub = iter->toSetElem_DenseOrUnboxedArrayAdd();
|
||||
if (obj->getGroup(cx) == nstub->group() && SetElemAddHasSameShapes(nstub, obj))
|
||||
if (obj->group() == nstub->group() && SetElemAddHasSameShapes(nstub, obj))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
RemoveMatchingDenseOrUnboxedArraySetElemAddStub(JSContext* cx,
|
||||
ICSetElem_Fallback* stub, HandleObject obj)
|
||||
{
|
||||
if (obj->isSingleton())
|
||||
return;
|
||||
|
||||
// Before attaching a new stub to add elements to a dense or unboxed array,
|
||||
// remove any other stub with the same group/shape but different prototype
|
||||
// shapes.
|
||||
for (ICStubIterator iter = stub->beginChain(); !iter.atEnd(); iter++) {
|
||||
if (!iter->isSetElem_DenseOrUnboxedArrayAdd())
|
||||
continue;
|
||||
|
||||
ICSetElem_DenseOrUnboxedArrayAdd* nstub = iter->toSetElem_DenseOrUnboxedArrayAdd();
|
||||
if (obj->group() != nstub->group())
|
||||
continue;
|
||||
|
||||
static const size_t MAX_DEPTH = ICSetElem_DenseOrUnboxedArrayAdd::MAX_PROTO_CHAIN_DEPTH;
|
||||
ICSetElem_DenseOrUnboxedArrayAddImpl<MAX_DEPTH>* nostub = nstub->toImplUnchecked<MAX_DEPTH>();
|
||||
|
||||
if (obj->maybeShape() == nostub->shape(0)) {
|
||||
iter.unlink(cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
TypedArraySetElemStubExists(ICSetElem_Fallback* stub, HandleObject obj, bool expectOOB)
|
||||
{
|
||||
@ -3732,6 +3763,8 @@ DoSetElemFallback(JSContext* cx, BaselineFrame* frame, ICSetElem_Fallback* stub_
|
||||
!DenseOrUnboxedArraySetElemStubExists(cx, ICStub::SetElem_DenseOrUnboxedArrayAdd,
|
||||
stub, obj))
|
||||
{
|
||||
RemoveMatchingDenseOrUnboxedArraySetElemAddStub(cx, stub, obj);
|
||||
|
||||
JitSpew(JitSpew_BaselineIC,
|
||||
" Generating SetElem_DenseOrUnboxedArrayAdd stub "
|
||||
"(shape=%p, group=%p, protoDepth=%u)",
|
||||
|
@ -409,12 +409,14 @@ JitCompartment::ensureIonStubsExist(JSContext* cx)
|
||||
struct OnIonCompilationInfo {
|
||||
size_t numBlocks;
|
||||
size_t scriptIndex;
|
||||
LifoAlloc alloc;
|
||||
LSprinter graph;
|
||||
|
||||
explicit OnIonCompilationInfo(LifoAlloc* alloc)
|
||||
OnIonCompilationInfo()
|
||||
: numBlocks(0),
|
||||
scriptIndex(0),
|
||||
graph(alloc)
|
||||
alloc(4096),
|
||||
graph(&alloc)
|
||||
{ }
|
||||
|
||||
bool filled() const {
|
||||
@ -594,7 +596,7 @@ jit::LazyLink(JSContext* cx, HandleScript calleeScript)
|
||||
|
||||
// See PrepareForDebuggerOnIonCompilationHook
|
||||
Rooted<ScriptVector> debugScripts(cx, ScriptVector(cx));
|
||||
OnIonCompilationInfo info(builder->alloc().lifoAlloc());
|
||||
OnIonCompilationInfo info;
|
||||
|
||||
{
|
||||
AutoEnterAnalysis enterTypes(cx);
|
||||
@ -606,14 +608,14 @@ jit::LazyLink(JSContext* cx, HandleScript calleeScript)
|
||||
}
|
||||
}
|
||||
|
||||
if (info.filled())
|
||||
Debugger::onIonCompilation(cx, debugScripts, info.graph);
|
||||
|
||||
{
|
||||
AutoLockHelperThreadState lock;
|
||||
FinishOffThreadBuilder(cx, builder);
|
||||
}
|
||||
|
||||
if (info.filled())
|
||||
Debugger::onIonCompilation(cx, debugScripts, info.graph);
|
||||
|
||||
MOZ_ASSERT(calleeScript->hasBaselineScript());
|
||||
MOZ_ASSERT(calleeScript->baselineOrIonRawPointer());
|
||||
}
|
||||
@ -2227,7 +2229,7 @@ IonCompile(JSContext* cx, JSScript* script,
|
||||
|
||||
// See PrepareForDebuggerOnIonCompilationHook
|
||||
Rooted<ScriptVector> debugScripts(cx, ScriptVector(cx));
|
||||
OnIonCompilationInfo debugInfo(alloc);
|
||||
OnIonCompilationInfo debugInfo;
|
||||
|
||||
ScopedJSDeletePtr<CodeGenerator> codegen;
|
||||
{
|
||||
|
@ -7587,21 +7587,21 @@ ClassHasEffectlessLookup(const Class* clasp)
|
||||
// Return whether an object might have a property for name which is not
|
||||
// accounted for by type information.
|
||||
static bool
|
||||
ObjectHasExtraOwnProperty(CompileCompartment* comp, TypeSet::ObjectKey* object, PropertyName* name)
|
||||
ObjectHasExtraOwnProperty(CompileCompartment* comp, TypeSet::ObjectKey* object, jsid id)
|
||||
{
|
||||
// Some typed object properties are not reflected in type information.
|
||||
if (object->isGroup() && object->group()->maybeTypeDescr())
|
||||
return object->group()->typeDescr().hasProperty(comp->runtime()->names(), NameToId(name));
|
||||
return object->group()->typeDescr().hasProperty(comp->runtime()->names(), id);
|
||||
|
||||
const Class* clasp = object->clasp();
|
||||
|
||||
// Array |length| properties are not reflected in type information.
|
||||
if (clasp == &ArrayObject::class_)
|
||||
return name == comp->runtime()->names().length;
|
||||
return JSID_IS_ATOM(id, comp->runtime()->names().length);
|
||||
|
||||
// Resolve hooks can install new properties on objects on demand.
|
||||
JSObject* singleton = object->isSingleton() ? object->singleton() : nullptr;
|
||||
return ClassMayResolveId(comp->runtime()->names(), clasp, NameToId(name), singleton);
|
||||
return ClassMayResolveId(comp->runtime()->names(), clasp, id, singleton);
|
||||
}
|
||||
|
||||
void
|
||||
@ -7629,7 +7629,7 @@ IonBuilder::insertRecompileCheck()
|
||||
}
|
||||
|
||||
JSObject*
|
||||
IonBuilder::testSingletonProperty(JSObject* obj, PropertyName* name)
|
||||
IonBuilder::testSingletonProperty(JSObject* obj, jsid id)
|
||||
{
|
||||
// We would like to completely no-op property/global accesses which can
|
||||
// produce only a particular JSObject. When indicating the access result is
|
||||
@ -7651,19 +7651,19 @@ IonBuilder::testSingletonProperty(JSObject* obj, PropertyName* name)
|
||||
|
||||
TypeSet::ObjectKey* objKey = TypeSet::ObjectKey::get(obj);
|
||||
if (analysisContext)
|
||||
objKey->ensureTrackedProperty(analysisContext, NameToId(name));
|
||||
objKey->ensureTrackedProperty(analysisContext, id);
|
||||
|
||||
if (objKey->unknownProperties())
|
||||
return nullptr;
|
||||
|
||||
HeapTypeSetKey property = objKey->property(NameToId(name));
|
||||
HeapTypeSetKey property = objKey->property(id);
|
||||
if (property.isOwnProperty(constraints())) {
|
||||
if (obj->isSingleton())
|
||||
return property.singleton(constraints());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (ObjectHasExtraOwnProperty(compartment, objKey, name))
|
||||
if (ObjectHasExtraOwnProperty(compartment, objKey, id))
|
||||
return nullptr;
|
||||
|
||||
obj = checkNurseryObject(obj->getProto());
|
||||
@ -7673,7 +7673,7 @@ IonBuilder::testSingletonProperty(JSObject* obj, PropertyName* name)
|
||||
}
|
||||
|
||||
JSObject*
|
||||
IonBuilder::testSingletonPropertyTypes(MDefinition* obj, PropertyName* name)
|
||||
IonBuilder::testSingletonPropertyTypes(MDefinition* obj, jsid id)
|
||||
{
|
||||
// As for TestSingletonProperty, but the input is any value in a type set
|
||||
// rather than a specific object.
|
||||
@ -7684,7 +7684,7 @@ IonBuilder::testSingletonPropertyTypes(MDefinition* obj, PropertyName* name)
|
||||
|
||||
JSObject* objectSingleton = types ? types->maybeSingleton() : nullptr;
|
||||
if (objectSingleton)
|
||||
return testSingletonProperty(objectSingleton, name);
|
||||
return testSingletonProperty(objectSingleton, id);
|
||||
|
||||
MIRType objType = obj->type();
|
||||
if (objType == MIRType_Value && types)
|
||||
@ -7722,20 +7722,20 @@ IonBuilder::testSingletonPropertyTypes(MDefinition* obj, PropertyName* name)
|
||||
if (!key)
|
||||
continue;
|
||||
if (analysisContext)
|
||||
key->ensureTrackedProperty(analysisContext, NameToId(name));
|
||||
key->ensureTrackedProperty(analysisContext, id);
|
||||
|
||||
const Class* clasp = key->clasp();
|
||||
if (!ClassHasEffectlessLookup(clasp) || ObjectHasExtraOwnProperty(compartment, key, name))
|
||||
if (!ClassHasEffectlessLookup(clasp) || ObjectHasExtraOwnProperty(compartment, key, id))
|
||||
return nullptr;
|
||||
if (key->unknownProperties())
|
||||
return nullptr;
|
||||
HeapTypeSetKey property = key->property(NameToId(name));
|
||||
HeapTypeSetKey property = key->property(id);
|
||||
if (property.isOwnProperty(constraints()))
|
||||
return nullptr;
|
||||
|
||||
if (JSObject* proto = checkNurseryObject(key->proto().toObjectOrNull())) {
|
||||
// Test this type.
|
||||
JSObject* thisSingleton = testSingletonProperty(proto, name);
|
||||
JSObject* thisSingleton = testSingletonProperty(proto, id);
|
||||
if (!thisSingleton)
|
||||
return nullptr;
|
||||
if (singleton) {
|
||||
@ -7757,7 +7757,7 @@ IonBuilder::testSingletonPropertyTypes(MDefinition* obj, PropertyName* name)
|
||||
|
||||
JSObject* proto = GetBuiltinPrototypePure(&script()->global(), key);
|
||||
if (proto)
|
||||
return testSingletonProperty(proto, name);
|
||||
return testSingletonProperty(proto, id);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
@ -7986,7 +7986,7 @@ IonBuilder::getStaticName(JSObject* staticObject, PropertyName* name, bool* psuc
|
||||
if (barrier == BarrierKind::NoBarrier) {
|
||||
// Try to inline properties holding a known constant object.
|
||||
if (singleton) {
|
||||
if (testSingletonProperty(staticObject, name) == singleton)
|
||||
if (testSingletonProperty(staticObject, id) == singleton)
|
||||
return pushConstant(ObjectValue(*singleton));
|
||||
}
|
||||
|
||||
@ -8221,6 +8221,11 @@ IonBuilder::jsop_getelem()
|
||||
if (!getElemTryTypedObject(&emitted, obj, index) || emitted)
|
||||
return emitted;
|
||||
|
||||
// Note: no trackOptimizationAttempt call is needed, getElemTryGetProp
|
||||
// will call it.
|
||||
if (!getElemTryGetProp(&emitted, obj, index) || emitted)
|
||||
return emitted;
|
||||
|
||||
trackOptimizationAttempt(TrackedStrategy::GetElem_Dense);
|
||||
if (!getElemTryDense(&emitted, obj, index) || emitted)
|
||||
return emitted;
|
||||
@ -8459,6 +8464,12 @@ IonBuilder::pushScalarLoadFromTypedObject(MDefinition* obj,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
BarrierMustTestTypeTag(BarrierKind kind)
|
||||
{
|
||||
return kind == BarrierKind::TypeSet || kind == BarrierKind::TypeTagOnly;
|
||||
}
|
||||
|
||||
bool
|
||||
IonBuilder::pushReferenceLoadFromTypedObject(MDefinition* typedObj,
|
||||
const LinearSum& byteOffset,
|
||||
@ -8494,7 +8505,7 @@ IonBuilder::pushReferenceLoadFromTypedObject(MDefinition* typedObj,
|
||||
// MLoadUnboxedObjectOrNull, which avoids the need to box the result
|
||||
// for a type barrier instruction.
|
||||
MLoadUnboxedObjectOrNull::NullBehavior nullBehavior;
|
||||
if (barrier == BarrierKind::NoBarrier && !observedTypes->hasType(TypeSet::NullType()))
|
||||
if (!observedTypes->hasType(TypeSet::NullType()) && !BarrierMustTestTypeTag(barrier))
|
||||
nullBehavior = MLoadUnboxedObjectOrNull::BailOnNull;
|
||||
else
|
||||
nullBehavior = MLoadUnboxedObjectOrNull::HandleNull;
|
||||
@ -8615,6 +8626,36 @@ IonBuilder::pushDerivedTypedObject(bool* emitted,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
IonBuilder::getElemTryGetProp(bool* emitted, MDefinition* obj, MDefinition* index)
|
||||
{
|
||||
// If index is a constant string or symbol, try to optimize this GETELEM
|
||||
// as a GETPROP.
|
||||
|
||||
MOZ_ASSERT(*emitted == false);
|
||||
|
||||
if (!index->isConstantValue())
|
||||
return true;
|
||||
|
||||
jsid id;
|
||||
if (!ValueToIdPure(index->constantValue(), &id))
|
||||
return true;
|
||||
|
||||
if (id != IdToTypeId(id))
|
||||
return true;
|
||||
|
||||
TemporaryTypeSet* types = bytecodeTypes(pc);
|
||||
|
||||
trackOptimizationAttempt(TrackedStrategy::GetProp_Constant);
|
||||
if (!getPropTryConstant(emitted, obj, id, types) || *emitted) {
|
||||
if (*emitted)
|
||||
index->setImplicitlyUsedUnchecked();
|
||||
return *emitted;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
IonBuilder::getElemTryDense(bool* emitted, MDefinition* obj, MDefinition* index)
|
||||
{
|
||||
@ -10189,7 +10230,7 @@ IonBuilder::objectsHaveCommonPrototype(TemporaryTypeSet* types, PropertyName* na
|
||||
if (!ClassHasEffectlessLookup(clasp))
|
||||
return false;
|
||||
JSObject* singleton = key->isSingleton() ? key->singleton() : nullptr;
|
||||
if (ObjectHasExtraOwnProperty(compartment, key, name)) {
|
||||
if (ObjectHasExtraOwnProperty(compartment, key, NameToId(name))) {
|
||||
if (!singleton || !singleton->is<GlobalObject>())
|
||||
return false;
|
||||
*guardGlobal = true;
|
||||
@ -10366,14 +10407,14 @@ IonBuilder::annotateGetPropertyCache(MDefinition* obj, MGetPropertyCache* getPro
|
||||
JSObject* proto = checkNurseryObject(key->proto().toObject());
|
||||
|
||||
const Class* clasp = key->clasp();
|
||||
if (!ClassHasEffectlessLookup(clasp) || ObjectHasExtraOwnProperty(compartment, key, name))
|
||||
if (!ClassHasEffectlessLookup(clasp) || ObjectHasExtraOwnProperty(compartment, key, NameToId(name)))
|
||||
continue;
|
||||
|
||||
HeapTypeSetKey ownTypes = key->property(NameToId(name));
|
||||
if (ownTypes.isOwnProperty(constraints()))
|
||||
continue;
|
||||
|
||||
JSObject* singleton = testSingletonProperty(proto, name);
|
||||
JSObject* singleton = testSingletonProperty(proto, NameToId(name));
|
||||
if (!singleton || !singleton->is<JSFunction>())
|
||||
continue;
|
||||
|
||||
@ -10624,7 +10665,7 @@ IonBuilder::jsop_getprop(PropertyName* name)
|
||||
// In this case we still need the getprop call so that the later
|
||||
// analysis knows when the |this| value has been read from.
|
||||
if (info().isAnalysis()) {
|
||||
if (!getPropTryConstant(&emitted, obj, name, types) || emitted)
|
||||
if (!getPropTryConstant(&emitted, obj, NameToId(name), types) || emitted)
|
||||
return emitted;
|
||||
}
|
||||
|
||||
@ -10643,7 +10684,7 @@ IonBuilder::jsop_getprop(PropertyName* name)
|
||||
if (!forceInlineCaches()) {
|
||||
// Try to hardcode known constants.
|
||||
trackOptimizationAttempt(TrackedStrategy::GetProp_Constant);
|
||||
if (!getPropTryConstant(&emitted, obj, name, types) || emitted)
|
||||
if (!getPropTryConstant(&emitted, obj, NameToId(name), types) || emitted)
|
||||
return emitted;
|
||||
|
||||
// Try to emit SIMD getter loads
|
||||
@ -10855,8 +10896,7 @@ IonBuilder::getPropTryArgumentsCallee(bool* emitted, MDefinition* obj, PropertyN
|
||||
}
|
||||
|
||||
bool
|
||||
IonBuilder::getPropTryConstant(bool* emitted, MDefinition* obj, PropertyName* name,
|
||||
TemporaryTypeSet* types)
|
||||
IonBuilder::getPropTryConstant(bool* emitted, MDefinition* obj, jsid id, TemporaryTypeSet* types)
|
||||
{
|
||||
MOZ_ASSERT(*emitted == false);
|
||||
|
||||
@ -10867,7 +10907,7 @@ IonBuilder::getPropTryConstant(bool* emitted, MDefinition* obj, PropertyName* na
|
||||
return true;
|
||||
}
|
||||
|
||||
JSObject* singleton = testSingletonPropertyTypes(obj, name);
|
||||
JSObject* singleton = testSingletonPropertyTypes(obj, id);
|
||||
if (!singleton) {
|
||||
trackOptimizationOutcome(TrackedOutcome::NotSingleton);
|
||||
return true;
|
||||
@ -11182,7 +11222,7 @@ IonBuilder::loadUnboxedValue(MDefinition* elements, size_t elementsOffset,
|
||||
|
||||
case JSVAL_TYPE_OBJECT: {
|
||||
MLoadUnboxedObjectOrNull::NullBehavior nullBehavior;
|
||||
if (types->hasType(TypeSet::NullType()) || barrier != BarrierKind::NoBarrier)
|
||||
if (types->hasType(TypeSet::NullType()) || BarrierMustTestTypeTag(barrier))
|
||||
nullBehavior = MLoadUnboxedObjectOrNull::HandleNull;
|
||||
else
|
||||
nullBehavior = MLoadUnboxedObjectOrNull::NullNotPossible;
|
||||
@ -11579,10 +11619,7 @@ IonBuilder::getPropTryCache(bool* emitted, MDefinition* obj, PropertyName* name,
|
||||
if (barrier != BarrierKind::TypeSet) {
|
||||
BarrierKind protoBarrier =
|
||||
PropertyReadOnPrototypeNeedsTypeBarrier(this, obj, name, types);
|
||||
if (protoBarrier != BarrierKind::NoBarrier) {
|
||||
MOZ_ASSERT(barrier <= protoBarrier);
|
||||
barrier = protoBarrier;
|
||||
}
|
||||
barrier = CombineBarrierKinds(barrier, protoBarrier);
|
||||
}
|
||||
|
||||
MGetPropertyCache* load = MGetPropertyCache::New(alloc(), obj, name,
|
||||
@ -11689,7 +11726,7 @@ IonBuilder::getPropTryInnerize(bool* emitted, MDefinition* obj, PropertyName* na
|
||||
// we'd produce here.
|
||||
|
||||
trackOptimizationAttempt(TrackedStrategy::GetProp_Constant);
|
||||
if (!getPropTryConstant(emitted, inner, name, types) || *emitted)
|
||||
if (!getPropTryConstant(emitted, inner, NameToId(name), types) || *emitted)
|
||||
return *emitted;
|
||||
|
||||
trackOptimizationAttempt(TrackedStrategy::GetProp_StaticName);
|
||||
|
@ -425,8 +425,7 @@ class IonBuilder
|
||||
TemporaryTypeSet* types);
|
||||
bool getPropTryArgumentsLength(bool* emitted, MDefinition* obj);
|
||||
bool getPropTryArgumentsCallee(bool* emitted, MDefinition* obj, PropertyName* name);
|
||||
bool getPropTryConstant(bool* emitted, MDefinition* obj, PropertyName* name,
|
||||
TemporaryTypeSet* types);
|
||||
bool getPropTryConstant(bool* emitted, MDefinition* obj, jsid id, TemporaryTypeSet* types);
|
||||
bool getPropTryDefiniteSlot(bool* emitted, MDefinition* obj, PropertyName* name,
|
||||
BarrierKind barrier, TemporaryTypeSet* types);
|
||||
bool getPropTryUnboxed(bool* emitted, MDefinition* obj, PropertyName* name,
|
||||
@ -585,6 +584,7 @@ class IonBuilder
|
||||
|
||||
// jsop_getelem() helpers.
|
||||
bool getElemTryDense(bool* emitted, MDefinition* obj, MDefinition* index);
|
||||
bool getElemTryGetProp(bool* emitted, MDefinition* obj, MDefinition* index);
|
||||
bool getElemTryTypedStatic(bool* emitted, MDefinition* obj, MDefinition* index);
|
||||
bool getElemTryTypedArray(bool* emitted, MDefinition* obj, MDefinition* index);
|
||||
bool getElemTryTypedObject(bool* emitted, MDefinition* obj, MDefinition* index);
|
||||
@ -960,8 +960,8 @@ class IonBuilder
|
||||
|
||||
MGetPropertyCache* getInlineableGetPropertyCache(CallInfo& callInfo);
|
||||
|
||||
JSObject* testSingletonProperty(JSObject* obj, PropertyName* name);
|
||||
JSObject* testSingletonPropertyTypes(MDefinition* obj, PropertyName* name);
|
||||
JSObject* testSingletonProperty(JSObject* obj, jsid id);
|
||||
JSObject* testSingletonPropertyTypes(MDefinition* obj, jsid id);
|
||||
|
||||
uint32_t getDefiniteSlot(TemporaryTypeSet* types, PropertyName* name, uint32_t* pnfixed);
|
||||
MDefinition* convertUnboxedObjects(MDefinition* obj);
|
||||
|
@ -3416,6 +3416,7 @@ GetElementIC::attachGetProp(JSContext* cx, HandleScript outerScript, IonScript*
|
||||
JitSpew(JitSpew_IonIC, "GETELEM uncacheable property");
|
||||
return true;
|
||||
}
|
||||
MOZ_ASSERT(monitoredResult());
|
||||
} else if (obj->is<UnboxedPlainObject>()) {
|
||||
MOZ_ASSERT(canCache == GetPropertyIC::CanAttachNone);
|
||||
const UnboxedLayout::Property* property =
|
||||
|
@ -745,11 +745,34 @@ enum class BarrierKind : uint32_t {
|
||||
// Specific object types don't have to be checked.
|
||||
TypeTagOnly,
|
||||
|
||||
// The barrier only has to check that object values are in the type set.
|
||||
// Non-object types don't have to be checked.
|
||||
ObjectTypesOnly,
|
||||
|
||||
// Check if the value is in the TypeSet, including the object type if it's
|
||||
// an object.
|
||||
TypeSet
|
||||
};
|
||||
|
||||
static inline BarrierKind
|
||||
CombineBarrierKinds(BarrierKind first, BarrierKind second)
|
||||
{
|
||||
// Barrier kinds form the following lattice:
|
||||
//
|
||||
// TypeSet
|
||||
// | |
|
||||
// TypeTagOnly ObjectTypesOnly
|
||||
// | |
|
||||
// NoBarrier
|
||||
//
|
||||
// This function computes the least upper bound of two barrier kinds.
|
||||
if (first == BarrierKind::NoBarrier || first == second)
|
||||
return second;
|
||||
if (second == BarrierKind::NoBarrier)
|
||||
return first;
|
||||
return BarrierKind::TypeSet;
|
||||
}
|
||||
|
||||
} // namespace jit
|
||||
} // namespace js
|
||||
|
||||
|
@ -3303,6 +3303,9 @@ LIRGenerator::visitGetElementCache(MGetElementCache* ins)
|
||||
{
|
||||
MOZ_ASSERT(ins->object()->type() == MIRType_Object);
|
||||
|
||||
if (ins->monitoredResult())
|
||||
gen->setPerformsCall(); // See visitGetPropertyCache.
|
||||
|
||||
if (ins->type() == MIRType_Value) {
|
||||
MOZ_ASSERT(ins->index()->type() == MIRType_Value);
|
||||
LGetElementCacheV* lir = new(alloc()) LGetElementCacheV(useRegister(ins->object()));
|
||||
@ -3519,6 +3522,8 @@ LIRGenerator::visitSetElementCache(MSetElementCache* ins)
|
||||
MOZ_ASSERT(ins->object()->type() == MIRType_Object);
|
||||
MOZ_ASSERT(ins->index()->type() == MIRType_Value);
|
||||
|
||||
gen->setPerformsCall(); // See visitSetPropertyCache.
|
||||
|
||||
// Due to lack of registers on x86, we reuse the object register as a
|
||||
// temporary. This register may be used in a 1-byte store, which on x86
|
||||
// again has constraints; thus the use of |useByteOpRegister| over
|
||||
|
@ -4985,6 +4985,12 @@ PropertyReadNeedsTypeBarrier(CompilerConstraintList* constraints,
|
||||
property.freeze(constraints);
|
||||
return BarrierKind::TypeTagOnly;
|
||||
}
|
||||
// If all possible primitives have been observed, we don't have to
|
||||
// guard on those primitives.
|
||||
if (property.maybeTypes()->primitivesAreSubset(observed)) {
|
||||
property.freeze(constraints);
|
||||
return BarrierKind::ObjectTypesOnly;
|
||||
}
|
||||
return BarrierKind::TypeSet;
|
||||
}
|
||||
}
|
||||
@ -5006,34 +5012,6 @@ PropertyReadNeedsTypeBarrier(CompilerConstraintList* constraints,
|
||||
return BarrierKind::NoBarrier;
|
||||
}
|
||||
|
||||
static bool
|
||||
ObjectSubsumes(TypeSet::ObjectKey* first, TypeSet::ObjectKey* second)
|
||||
{
|
||||
if (first->isSingleton() ||
|
||||
second->isSingleton() ||
|
||||
first->clasp() != second->clasp() ||
|
||||
first->unknownProperties() ||
|
||||
second->unknownProperties())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first->clasp() == &ArrayObject::class_) {
|
||||
HeapTypeSetKey firstElements = first->property(JSID_VOID);
|
||||
HeapTypeSetKey secondElements = second->property(JSID_VOID);
|
||||
|
||||
return firstElements.maybeTypes() && secondElements.maybeTypes() &&
|
||||
firstElements.maybeTypes()->equals(secondElements.maybeTypes());
|
||||
}
|
||||
|
||||
if (first->clasp() == &UnboxedArrayObject::class_) {
|
||||
return first->group()->unboxedLayout().elementType() ==
|
||||
second->group()->unboxedLayout().elementType();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
BarrierKind
|
||||
jit::PropertyReadNeedsTypeBarrier(JSContext* propertycx,
|
||||
CompilerConstraintList* constraints,
|
||||
@ -5078,30 +5056,6 @@ jit::PropertyReadNeedsTypeBarrier(JSContext* propertycx,
|
||||
}
|
||||
}
|
||||
|
||||
// If any objects which could be observed are similar to ones that have
|
||||
// already been observed, add them to the observed type set.
|
||||
if (!key->unknownProperties()) {
|
||||
HeapTypeSetKey property = key->property(name ? NameToId(name) : JSID_VOID);
|
||||
|
||||
if (property.maybeTypes() && !property.maybeTypes()->unknownObject()) {
|
||||
for (size_t i = 0; i < property.maybeTypes()->getObjectCount(); i++) {
|
||||
TypeSet::ObjectKey* key = property.maybeTypes()->getObject(i);
|
||||
if (!key || observed->unknownObject())
|
||||
continue;
|
||||
|
||||
for (size_t j = 0; j < observed->getObjectCount(); j++) {
|
||||
TypeSet::ObjectKey* observedKey = observed->getObject(j);
|
||||
if (observedKey && ObjectSubsumes(observedKey, key)) {
|
||||
// Note: the return value here is ignored.
|
||||
observed->addType(TypeSet::ObjectType(key),
|
||||
GetJitContext()->temp->lifoAlloc());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PropertyReadNeedsTypeBarrier(constraints, key, name, observed);
|
||||
}
|
||||
|
||||
@ -5125,15 +5079,9 @@ jit::PropertyReadNeedsTypeBarrier(JSContext* propertycx,
|
||||
if (TypeSet::ObjectKey* key = types->getObject(i)) {
|
||||
BarrierKind kind = PropertyReadNeedsTypeBarrier(propertycx, constraints, key, name,
|
||||
observed, updateObserved);
|
||||
if (kind == BarrierKind::TypeSet)
|
||||
res = CombineBarrierKinds(res, kind);
|
||||
if (res == BarrierKind::TypeSet)
|
||||
return BarrierKind::TypeSet;
|
||||
|
||||
if (kind == BarrierKind::TypeTagOnly) {
|
||||
MOZ_ASSERT(res == BarrierKind::NoBarrier || res == BarrierKind::TypeTagOnly);
|
||||
res = BarrierKind::TypeTagOnly;
|
||||
} else {
|
||||
MOZ_ASSERT(kind == BarrierKind::NoBarrier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5167,15 +5115,9 @@ jit::PropertyReadOnPrototypeNeedsTypeBarrier(IonBuilder* builder,
|
||||
key = TypeSet::ObjectKey::get(proto);
|
||||
BarrierKind kind = PropertyReadNeedsTypeBarrier(builder->constraints(),
|
||||
key, name, observed);
|
||||
if (kind == BarrierKind::TypeSet)
|
||||
res = CombineBarrierKinds(res, kind);
|
||||
if (res == BarrierKind::TypeSet)
|
||||
return BarrierKind::TypeSet;
|
||||
|
||||
if (kind == BarrierKind::TypeTagOnly) {
|
||||
MOZ_ASSERT(res == BarrierKind::NoBarrier || res == BarrierKind::TypeTagOnly);
|
||||
res = BarrierKind::TypeTagOnly;
|
||||
} else {
|
||||
MOZ_ASSERT(kind == BarrierKind::NoBarrier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5400,8 +5342,12 @@ TryAddTypeBarrierForWrite(TempAllocator& alloc, CompilerConstraintList* constrai
|
||||
// If all possible objects can be stored without a barrier, we don't have to
|
||||
// guard on the specific object types.
|
||||
BarrierKind kind = BarrierKind::TypeSet;
|
||||
if ((*pvalue)->resultTypeSet() && (*pvalue)->resultTypeSet()->objectsAreSubset(types))
|
||||
kind = BarrierKind::TypeTagOnly;
|
||||
if ((*pvalue)->resultTypeSet()) {
|
||||
if ((*pvalue)->resultTypeSet()->objectsAreSubset(types))
|
||||
kind = BarrierKind::TypeTagOnly;
|
||||
else if ((*pvalue)->resultTypeSet()->primitivesAreSubset(types))
|
||||
kind = BarrierKind::ObjectTypesOnly;
|
||||
}
|
||||
|
||||
MInstruction* ins = MMonitorTypes::New(alloc, *pvalue, types, kind);
|
||||
current->add(ins);
|
||||
|
@ -795,7 +795,7 @@ class MDefinition : public MNode
|
||||
MIR_OPCODE_LIST(OPCODE_CASTS)
|
||||
# undef OPCODE_CASTS
|
||||
|
||||
bool isConstantValue() {
|
||||
bool isConstantValue() const {
|
||||
return isConstant() || (isBox() && getOperand(0)->isConstant());
|
||||
}
|
||||
const Value& constantValue();
|
||||
@ -12387,7 +12387,7 @@ class MTypeBarrier
|
||||
: MUnaryInstruction(def),
|
||||
barrierKind_(kind)
|
||||
{
|
||||
MOZ_ASSERT(kind == BarrierKind::TypeTagOnly || kind == BarrierKind::TypeSet);
|
||||
MOZ_ASSERT(kind != BarrierKind::NoBarrier);
|
||||
|
||||
MOZ_ASSERT(!types->unknown());
|
||||
setResultType(types->getKnownMIRType());
|
||||
@ -12447,7 +12447,7 @@ class MMonitorTypes
|
||||
typeSet_(types),
|
||||
barrierKind_(kind)
|
||||
{
|
||||
MOZ_ASSERT(kind == BarrierKind::TypeTagOnly || kind == BarrierKind::TypeSet);
|
||||
MOZ_ASSERT(kind != BarrierKind::NoBarrier);
|
||||
|
||||
setGuard();
|
||||
MOZ_ASSERT(!types->unknown());
|
||||
|
@ -35,7 +35,7 @@ template <typename Source> void
|
||||
MacroAssembler::guardTypeSet(const Source& address, const TypeSet* types, BarrierKind kind,
|
||||
Register scratch, Label* miss)
|
||||
{
|
||||
MOZ_ASSERT(kind == BarrierKind::TypeTagOnly || kind == BarrierKind::TypeSet);
|
||||
MOZ_ASSERT(kind != BarrierKind::NoBarrier);
|
||||
MOZ_ASSERT(!types->unknown());
|
||||
|
||||
Label matched;
|
||||
@ -50,24 +50,49 @@ MacroAssembler::guardTypeSet(const Source& address, const TypeSet* types, Barrie
|
||||
TypeSet::AnyObjectType()
|
||||
};
|
||||
|
||||
// The double type also implies Int32.
|
||||
// So replace the int32 test with the double one.
|
||||
if (types->hasType(TypeSet::DoubleType())) {
|
||||
MOZ_ASSERT(types->hasType(TypeSet::Int32Type()));
|
||||
tests[0] = TypeSet::DoubleType();
|
||||
}
|
||||
|
||||
Register tag = extractTag(address, scratch);
|
||||
|
||||
// Emit all typed tests.
|
||||
BranchType lastBranch;
|
||||
for (size_t i = 0; i < mozilla::ArrayLength(tests); i++) {
|
||||
if (!types->hasType(tests[i]))
|
||||
continue;
|
||||
|
||||
if (lastBranch.isInitialized())
|
||||
lastBranch.emit(*this);
|
||||
lastBranch = BranchType(Equal, tag, tests[i], &matched);
|
||||
if (kind != BarrierKind::ObjectTypesOnly) {
|
||||
// The double type also implies Int32.
|
||||
// So replace the int32 test with the double one.
|
||||
if (types->hasType(TypeSet::DoubleType())) {
|
||||
MOZ_ASSERT(types->hasType(TypeSet::Int32Type()));
|
||||
tests[0] = TypeSet::DoubleType();
|
||||
}
|
||||
|
||||
// Emit all typed tests.
|
||||
for (size_t i = 0; i < mozilla::ArrayLength(tests); i++) {
|
||||
if (!types->hasType(tests[i]))
|
||||
continue;
|
||||
|
||||
if (lastBranch.isInitialized())
|
||||
lastBranch.emit(*this);
|
||||
lastBranch = BranchType(Equal, tag, tests[i], &matched);
|
||||
}
|
||||
} else {
|
||||
#ifdef DEBUG
|
||||
// Any non-object will be considered to match the type set. Make sure
|
||||
// such values encountered are actually in the type set.
|
||||
|
||||
if (types->hasType(TypeSet::DoubleType())) {
|
||||
MOZ_ASSERT(types->hasType(TypeSet::Int32Type()));
|
||||
tests[0] = TypeSet::DoubleType();
|
||||
}
|
||||
|
||||
Label matchedPrimitive;
|
||||
for (size_t i = 0; i < mozilla::ArrayLength(tests); i++) {
|
||||
if (!types->hasType(tests[i]))
|
||||
continue;
|
||||
BranchType branch(Equal, tag, tests[i], &matchedPrimitive);
|
||||
branch.emit(*this);
|
||||
}
|
||||
branchTestObject(Equal, tag, &matchedPrimitive);
|
||||
|
||||
assumeUnreachable("Unexpected primitive type");
|
||||
|
||||
bind(&matchedPrimitive);
|
||||
#endif
|
||||
}
|
||||
|
||||
// If this is the last check, invert the last branch.
|
||||
@ -90,7 +115,7 @@ MacroAssembler::guardTypeSet(const Source& address, const TypeSet* types, Barrie
|
||||
|
||||
// Test specific objects.
|
||||
MOZ_ASSERT(scratch != InvalidReg);
|
||||
branchTestObject(NotEqual, tag, miss);
|
||||
branchTestObject(NotEqual, tag, kind == BarrierKind::ObjectTypesOnly ? &matched : miss);
|
||||
if (kind != BarrierKind::TypeTagOnly) {
|
||||
Register obj = extractObject(address, scratch);
|
||||
guardObjectType(obj, types, scratch, miss);
|
||||
|
@ -2874,7 +2874,7 @@ class GCHeapProfiler
|
||||
|
||||
class MemProfiler
|
||||
{
|
||||
static mozilla::Atomic<int> sActiveProfilerCount;
|
||||
static mozilla::Atomic<uint32_t, mozilla::Relaxed> sActiveProfilerCount;
|
||||
static NativeProfiler* sNativeProfiler;
|
||||
|
||||
static GCHeapProfiler* GetGCHeapProfiler(void* addr);
|
||||
@ -2897,7 +2897,7 @@ class MemProfiler
|
||||
return mGCHeapProfiler;
|
||||
}
|
||||
|
||||
static bool enabled() {
|
||||
static MOZ_ALWAYS_INLINE bool enabled() {
|
||||
return sActiveProfilerCount > 0;
|
||||
}
|
||||
|
||||
@ -2907,7 +2907,7 @@ class MemProfiler
|
||||
sNativeProfiler = aProfiler;
|
||||
}
|
||||
|
||||
static void SampleNative(void* addr, uint32_t size) {
|
||||
static MOZ_ALWAYS_INLINE void SampleNative(void* addr, uint32_t size) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2918,7 +2918,7 @@ class MemProfiler
|
||||
profiler->sampleNative(addr, size);
|
||||
}
|
||||
|
||||
static void SampleTenured(void* addr, uint32_t size) {
|
||||
static MOZ_ALWAYS_INLINE void SampleTenured(void* addr, uint32_t size) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2929,7 +2929,7 @@ class MemProfiler
|
||||
profiler->sampleTenured(addr, size);
|
||||
}
|
||||
|
||||
static void SampleNursery(void* addr, uint32_t size) {
|
||||
static MOZ_ALWAYS_INLINE void SampleNursery(void* addr, uint32_t size) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2940,7 +2940,7 @@ class MemProfiler
|
||||
profiler->sampleNursery(addr, size);
|
||||
}
|
||||
|
||||
static void RemoveNative(void* addr) {
|
||||
static MOZ_ALWAYS_INLINE void RemoveNative(void* addr) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2951,7 +2951,7 @@ class MemProfiler
|
||||
profiler->removeNative(addr);
|
||||
}
|
||||
|
||||
static void MarkTenuredStart(JSRuntime* runtime) {
|
||||
static MOZ_ALWAYS_INLINE void MarkTenuredStart(JSRuntime* runtime) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2962,7 +2962,7 @@ class MemProfiler
|
||||
profiler->markTenuredStart();
|
||||
}
|
||||
|
||||
static void MarkTenured(void* addr) {
|
||||
static MOZ_ALWAYS_INLINE void MarkTenured(void* addr) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2973,7 +2973,7 @@ class MemProfiler
|
||||
profiler->markTenured(addr);
|
||||
}
|
||||
|
||||
static void SweepTenured(JSRuntime* runtime) {
|
||||
static MOZ_ALWAYS_INLINE void SweepTenured(JSRuntime* runtime) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2984,7 +2984,7 @@ class MemProfiler
|
||||
profiler->sweepTenured();
|
||||
}
|
||||
|
||||
static void SweepNursery(JSRuntime* runtime) {
|
||||
static MOZ_ALWAYS_INLINE void SweepNursery(JSRuntime* runtime) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
@ -2995,7 +2995,7 @@ class MemProfiler
|
||||
profiler->sweepNursery();
|
||||
}
|
||||
|
||||
static void MoveNurseryToTenured(void* addrOld, void* addrNew) {
|
||||
static MOZ_ALWAYS_INLINE void MoveNurseryToTenured(void* addrOld, void* addrNew) {
|
||||
JS::AutoSuppressGCAnalysis nogc;
|
||||
|
||||
if (MOZ_LIKELY(!enabled()))
|
||||
|
@ -601,6 +601,13 @@ Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool frame
|
||||
if (frameRange.empty())
|
||||
return frameOk;
|
||||
|
||||
auto frameMapsGuard = MakeScopeExit([&] {
|
||||
// Clean up all Debugger.Frame instances. This call creates a fresh
|
||||
// FrameRange, as one debugger's onPop handler could have caused another
|
||||
// debugger to create its own Debugger.Frame instance.
|
||||
removeFromFrameMapsAndClearBreakpointsIn(cx, frame);
|
||||
});
|
||||
|
||||
/* Save the frame's completion value. */
|
||||
JSTrapStatus status;
|
||||
RootedValue value(cx);
|
||||
@ -661,13 +668,6 @@ Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool frame
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Clean up all Debugger.Frame instances. This call creates a fresh
|
||||
* FrameRange, as one debugger's onPop handler could have caused another
|
||||
* debugger to create its own Debugger.Frame instance.
|
||||
*/
|
||||
removeFromFrameMapsAndClearBreakpointsIn(cx, frame);
|
||||
|
||||
/* Establish (status, value) as our resumption value. */
|
||||
switch (status) {
|
||||
case JSTRAP_RETURN:
|
||||
|
@ -253,7 +253,7 @@ SPSProfiler::beginPseudoJS(const char* string, void* sp)
|
||||
MOZ_ASSERT(installed());
|
||||
if (current < max_) {
|
||||
stack[current].setLabel(string);
|
||||
stack[current].setCppFrame(sp, 0);
|
||||
stack[current].initCppFrame(sp, 0);
|
||||
stack[current].setFlag(ProfileEntry::BEGIN_PSEUDO_JS);
|
||||
}
|
||||
*size = current + 1;
|
||||
@ -274,18 +274,19 @@ SPSProfiler::push(const char* string, void* sp, JSScript* script, jsbytecode* pc
|
||||
MOZ_ASSERT(installed());
|
||||
if (current < max_) {
|
||||
volatile ProfileEntry& entry = stack[current];
|
||||
entry.setLabel(string);
|
||||
entry.setCategory(category);
|
||||
|
||||
if (sp != nullptr) {
|
||||
entry.setCppFrame(sp, 0);
|
||||
entry.initCppFrame(sp, 0);
|
||||
MOZ_ASSERT(entry.flags() == js::ProfileEntry::IS_CPP_ENTRY);
|
||||
}
|
||||
else {
|
||||
entry.setJsFrame(script, pc);
|
||||
entry.initJsFrame(script, pc);
|
||||
MOZ_ASSERT(entry.flags() == 0);
|
||||
}
|
||||
|
||||
entry.setLabel(string);
|
||||
entry.setCategory(category);
|
||||
|
||||
// Track if mLabel needs a copy.
|
||||
if (copy)
|
||||
entry.setFlag(js::ProfileEntry::FRAME_LABEL_COPY);
|
||||
|
@ -398,6 +398,13 @@ TypeSet::objectsAreSubset(TypeSet* other)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
TypeSet::primitivesAreSubset(TypeSet* other)
|
||||
{
|
||||
uint32_t primitiveFlags = baseFlags() & TYPE_FLAG_PRIMITIVE;
|
||||
return (primitiveFlags & other->baseFlags()) == primitiveFlags;
|
||||
}
|
||||
|
||||
bool
|
||||
TypeSet::isSubset(const TypeSet* other) const
|
||||
{
|
||||
|
@ -66,7 +66,7 @@ enum : uint32_t {
|
||||
/* Mask containing all primitives */
|
||||
TYPE_FLAG_PRIMITIVE = TYPE_FLAG_UNDEFINED | TYPE_FLAG_NULL | TYPE_FLAG_BOOLEAN |
|
||||
TYPE_FLAG_INT32 | TYPE_FLAG_DOUBLE | TYPE_FLAG_STRING |
|
||||
TYPE_FLAG_SYMBOL,
|
||||
TYPE_FLAG_SYMBOL | TYPE_FLAG_LAZYARGS,
|
||||
|
||||
/* Mask/shift for the number of objects in objectSet */
|
||||
TYPE_FLAG_OBJECT_COUNT_MASK = 0x3e00,
|
||||
@ -483,11 +483,10 @@ class TypeSet
|
||||
*/
|
||||
bool isSubset(const TypeSet* other) const;
|
||||
|
||||
/*
|
||||
* Get whether the objects in this TypeSet are a subset of the objects
|
||||
* in other.
|
||||
*/
|
||||
// Return whether this is a subset of other, ignoring primitive or object
|
||||
// types respectively.
|
||||
bool objectsAreSubset(TypeSet* other);
|
||||
bool primitivesAreSubset(TypeSet* other);
|
||||
|
||||
/* Whether this TypeSet contains exactly the same types as other. */
|
||||
bool equals(const TypeSet* other) const {
|
||||
|
@ -502,10 +502,10 @@ SetOrExtendBoxedOrUnboxedDenseElements(ExclusiveContext* cx, JSObject* obj,
|
||||
// that were left alone.
|
||||
size_t i = 0;
|
||||
if (updateTypes == ShouldUpdateTypes::DontUpdate) {
|
||||
for (size_t j = start; i < count && j < oldInitlen; i++)
|
||||
for (size_t j = start; i < count && j < oldInitlen; i++, j++)
|
||||
nobj->setElementNoTypeChangeSpecific<Type>(j, vp[i]);
|
||||
} else {
|
||||
for (size_t j = start; i < count && j < oldInitlen; i++) {
|
||||
for (size_t j = start; i < count && j < oldInitlen; i++, j++) {
|
||||
if (!nobj->setElementSpecific<Type>(cx, j, vp[i]))
|
||||
return DenseElementResult::Incomplete;
|
||||
}
|
||||
|
@ -1974,6 +1974,12 @@ js::TryConvertToUnboxedLayout(ExclusiveContext* cx, Shape* templateShape,
|
||||
// element type for the objects.
|
||||
if (UnboxedTypeSize(elementType) == 0)
|
||||
return true;
|
||||
|
||||
// Don't use an unboxed representation if objects in the group have
|
||||
// ever had holes in the past. Even if they have been filled in, future
|
||||
// objects that are created might be given holes as well.
|
||||
if (group->flags() & OBJECT_FLAG_NON_PACKED)
|
||||
return true;
|
||||
} else {
|
||||
if (objectCount <= 1) {
|
||||
// If only one of the objects has been created, it is more likely
|
||||
|
@ -1508,10 +1508,6 @@ already_AddRefed<LayerManager> nsDisplayList::PaintRoot(nsDisplayListBuilder* aB
|
||||
layerBuilder->SetLayerTreeCompressionMode();
|
||||
}
|
||||
|
||||
if (aFlags & PAINT_FLUSH_LAYERS) {
|
||||
FrameLayerBuilder::InvalidateAllLayers(layerManager);
|
||||
}
|
||||
|
||||
if (doBeginTransaction) {
|
||||
if (aCtx) {
|
||||
layerManager->BeginTransactionWithTarget(aCtx->ThebesContext());
|
||||
@ -1712,10 +1708,6 @@ already_AddRefed<LayerManager> nsDisplayList::PaintRoot(nsDisplayListBuilder* aB
|
||||
}
|
||||
}
|
||||
|
||||
if (aFlags & PAINT_FLUSH_LAYERS) {
|
||||
FrameLayerBuilder::InvalidateAllLayers(layerManager);
|
||||
}
|
||||
|
||||
layerManager->SetUserData(&gLayerManagerLayerBuilder, oldBuilder);
|
||||
return layerManager.forget();
|
||||
}
|
||||
|
@ -1982,9 +1982,6 @@ public:
|
||||
* otherwise we will use a temporary BasicLayerManager and ctx must
|
||||
* not be null.
|
||||
*
|
||||
* If PAINT_FLUSH_LAYERS is set, we'll force a completely new layer
|
||||
* tree to be created for this paint *and* the next paint.
|
||||
*
|
||||
* If PAINT_EXISTING_TRANSACTION is set, the reference frame's widget's
|
||||
* layer manager has already had BeginTransaction() called on it and
|
||||
* we should not call it again.
|
||||
@ -2001,7 +1998,6 @@ public:
|
||||
enum {
|
||||
PAINT_DEFAULT = 0,
|
||||
PAINT_USE_WIDGET_LAYERS = 0x01,
|
||||
PAINT_FLUSH_LAYERS = 0x02,
|
||||
PAINT_EXISTING_TRANSACTION = 0x04,
|
||||
PAINT_NO_COMPOSITE = 0x08,
|
||||
PAINT_COMPRESSED = 0x10
|
||||
@ -2411,40 +2407,11 @@ protected:
|
||||
* is not yet a frame tree to go in the frame/iframe so we use the subdoc
|
||||
* frame of the parent document as a standin.
|
||||
*/
|
||||
class nsDisplaySolidColor : public nsDisplayItem {
|
||||
class nsDisplaySolidColorBase : public nsDisplayItem {
|
||||
public:
|
||||
nsDisplaySolidColor(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
||||
const nsRect& aBounds, nscolor aColor)
|
||||
: nsDisplayItem(aBuilder, aFrame), mBounds(aBounds), mColor(aColor)
|
||||
{
|
||||
NS_ASSERTION(NS_GET_A(aColor) > 0, "Don't create invisible nsDisplaySolidColors!");
|
||||
MOZ_COUNT_CTOR(nsDisplaySolidColor);
|
||||
}
|
||||
#ifdef NS_BUILD_REFCNT_LOGGING
|
||||
virtual ~nsDisplaySolidColor() {
|
||||
MOZ_COUNT_DTOR(nsDisplaySolidColor);
|
||||
}
|
||||
#endif
|
||||
|
||||
virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) override;
|
||||
|
||||
virtual nsRegion GetOpaqueRegion(nsDisplayListBuilder* aBuilder,
|
||||
bool* aSnap) override {
|
||||
*aSnap = false;
|
||||
nsRegion result;
|
||||
if (NS_GET_A(mColor) == 255) {
|
||||
result = GetBounds(aBuilder, aSnap);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual bool IsUniform(nsDisplayListBuilder* aBuilder, nscolor* aColor) override
|
||||
{
|
||||
*aColor = mColor;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void Paint(nsDisplayListBuilder* aBuilder, nsRenderingContext* aCtx) override;
|
||||
nsDisplaySolidColorBase(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, nscolor aColor)
|
||||
: nsDisplayItem(aBuilder, aFrame), mColor(aColor)
|
||||
{}
|
||||
|
||||
virtual nsDisplayItemGeometry* AllocateGeometry(nsDisplayListBuilder* aBuilder) override
|
||||
{
|
||||
@ -2465,13 +2432,51 @@ public:
|
||||
ComputeInvalidationRegionDifference(aBuilder, geometry, aInvalidRegion);
|
||||
}
|
||||
|
||||
virtual nsRegion GetOpaqueRegion(nsDisplayListBuilder* aBuilder,
|
||||
bool* aSnap) override {
|
||||
*aSnap = false;
|
||||
nsRegion result;
|
||||
if (NS_GET_A(mColor) == 255) {
|
||||
result = GetBounds(aBuilder, aSnap);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual bool IsUniform(nsDisplayListBuilder* aBuilder, nscolor* aColor) override
|
||||
{
|
||||
*aColor = mColor;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
nscolor mColor;
|
||||
};
|
||||
|
||||
class nsDisplaySolidColor : public nsDisplaySolidColorBase {
|
||||
public:
|
||||
nsDisplaySolidColor(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
|
||||
const nsRect& aBounds, nscolor aColor)
|
||||
: nsDisplaySolidColorBase(aBuilder, aFrame, aColor), mBounds(aBounds)
|
||||
{
|
||||
NS_ASSERTION(NS_GET_A(aColor) > 0, "Don't create invisible nsDisplaySolidColors!");
|
||||
MOZ_COUNT_CTOR(nsDisplaySolidColor);
|
||||
}
|
||||
#ifdef NS_BUILD_REFCNT_LOGGING
|
||||
virtual ~nsDisplaySolidColor() {
|
||||
MOZ_COUNT_DTOR(nsDisplaySolidColor);
|
||||
}
|
||||
#endif
|
||||
|
||||
virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) override;
|
||||
|
||||
virtual void Paint(nsDisplayListBuilder* aBuilder, nsRenderingContext* aCtx) override;
|
||||
|
||||
virtual void WriteDebugInfo(std::stringstream& aStream) override;
|
||||
|
||||
NS_DISPLAY_DECL_NAME("SolidColor", TYPE_SOLID_COLOR)
|
||||
|
||||
private:
|
||||
nsRect mBounds;
|
||||
nscolor mColor;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -3136,17 +3136,11 @@ nsLayoutUtils::PaintFrame(nsRenderingContext* aRenderingContext, nsIFrame* aFram
|
||||
visibleRegion = aDirtyRegion;
|
||||
}
|
||||
|
||||
// If we're going to display something different from what we'd normally
|
||||
// paint in a window then we will flush out any retained layer trees before
|
||||
// *and after* we draw.
|
||||
bool willFlushRetainedLayers = (aFlags & PAINT_HIDE_CARET) != 0;
|
||||
|
||||
nsDisplayList list;
|
||||
|
||||
// If the root has embedded plugins, flag the builder so we know we'll need
|
||||
// to update plugin geometry after painting.
|
||||
if ((aFlags & PAINT_WIDGET_LAYERS) &&
|
||||
!willFlushRetainedLayers &&
|
||||
!(aFlags & PAINT_DOCUMENT_RELATIVE) &&
|
||||
rootPresContext->NeedToComputePluginGeometryUpdates()) {
|
||||
builder.SetWillComputePluginGeometry(true);
|
||||
@ -3262,33 +3256,12 @@ nsLayoutUtils::PaintFrame(nsRenderingContext* aRenderingContext, nsIFrame* aFram
|
||||
buildingDisplayList(&builder, aFrame, canvasArea, false);
|
||||
presShell->AddCanvasBackgroundColorItem(
|
||||
builder, list, aFrame, canvasArea, aBackstop);
|
||||
|
||||
// If the passed in backstop color makes us draw something different from
|
||||
// normal, we need to flush layers.
|
||||
if ((aFlags & PAINT_WIDGET_LAYERS) && !willFlushRetainedLayers) {
|
||||
nsView* view = aFrame->GetView();
|
||||
if (view) {
|
||||
nscolor backstop = presShell->ComputeBackstopColor(view);
|
||||
// The PresShell's canvas background color doesn't get updated until
|
||||
// EnterPresShell, so this check has to be done after that.
|
||||
nscolor canvasColor = presShell->GetCanvasBackground();
|
||||
if (NS_ComposeColors(aBackstop, canvasColor) !=
|
||||
NS_ComposeColors(backstop, canvasColor)) {
|
||||
willFlushRetainedLayers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.LeavePresShell(aFrame);
|
||||
Telemetry::AccumulateTimeDelta(Telemetry::PAINT_BUILD_DISPLAYLIST_TIME,
|
||||
startBuildDisplayList);
|
||||
|
||||
if (builder.GetHadToIgnorePaintSuppression()) {
|
||||
willFlushRetainedLayers = true;
|
||||
}
|
||||
|
||||
|
||||
bool profilerNeedsDisplayList = profiler_feature_active("displaylistdump");
|
||||
bool consoleNeedsDisplayList = gfxUtils::DumpDisplayList() || gfxUtils::sDumpPainting;
|
||||
#ifdef MOZ_DUMP_PAINTING
|
||||
@ -3332,16 +3305,7 @@ nsLayoutUtils::PaintFrame(nsRenderingContext* aRenderingContext, nsIFrame* aFram
|
||||
uint32_t flags = nsDisplayList::PAINT_DEFAULT;
|
||||
if (aFlags & PAINT_WIDGET_LAYERS) {
|
||||
flags |= nsDisplayList::PAINT_USE_WIDGET_LAYERS;
|
||||
if (willFlushRetainedLayers) {
|
||||
// The caller wanted to paint from retained layers, but set up
|
||||
// the paint in such a way that we can't use them. We're going
|
||||
// to display something different from what we'd normally paint
|
||||
// in a window, so make sure we flush out any retained layer
|
||||
// trees before *and after* we draw. Callers should be fixed to
|
||||
// not do this.
|
||||
NS_WARNING("Flushing retained layers!");
|
||||
flags |= nsDisplayList::PAINT_FLUSH_LAYERS;
|
||||
} else if (!(aFlags & PAINT_DOCUMENT_RELATIVE)) {
|
||||
if (!(aFlags & PAINT_DOCUMENT_RELATIVE)) {
|
||||
nsIWidget *widget = aFrame->GetNearestWidget();
|
||||
if (widget) {
|
||||
// If we're finished building display list items for painting of the outermost
|
||||
@ -3416,7 +3380,6 @@ nsLayoutUtils::PaintFrame(nsRenderingContext* aRenderingContext, nsIFrame* aFram
|
||||
// glass boundaries on Windows. Also set up the window dragging region
|
||||
// and plugin clip regions and bounds.
|
||||
if ((aFlags & PAINT_WIDGET_LAYERS) &&
|
||||
!willFlushRetainedLayers &&
|
||||
!(aFlags & PAINT_DOCUMENT_RELATIVE)) {
|
||||
nsIWidget *widget = aFrame->GetNearestWidget();
|
||||
if (widget) {
|
||||
|
@ -195,11 +195,10 @@ protected:
|
||||
* We can also paint an "extra background color" behind the normal
|
||||
* background.
|
||||
*/
|
||||
class nsDisplayCanvasBackgroundColor : public nsDisplayItem {
|
||||
class nsDisplayCanvasBackgroundColor : public nsDisplaySolidColorBase {
|
||||
public:
|
||||
nsDisplayCanvasBackgroundColor(nsDisplayListBuilder* aBuilder, nsIFrame *aFrame)
|
||||
: nsDisplayItem(aBuilder, aFrame)
|
||||
, mColor(NS_RGBA(0,0,0,0))
|
||||
: nsDisplaySolidColorBase(aBuilder, aFrame, NS_RGBA(0,0,0,0))
|
||||
{
|
||||
}
|
||||
|
||||
@ -208,19 +207,6 @@ public:
|
||||
{
|
||||
return NS_GET_A(mColor) > 0;
|
||||
}
|
||||
virtual nsRegion GetOpaqueRegion(nsDisplayListBuilder* aBuilder,
|
||||
bool* aSnap) override
|
||||
{
|
||||
if (NS_GET_A(mColor) == 255) {
|
||||
return nsRegion(GetBounds(aBuilder, aSnap));
|
||||
}
|
||||
return nsRegion();
|
||||
}
|
||||
virtual bool IsUniform(nsDisplayListBuilder* aBuilder, nscolor* aColor) override
|
||||
{
|
||||
*aColor = mColor;
|
||||
return true;
|
||||
}
|
||||
virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) override
|
||||
{
|
||||
nsCanvasFrame* frame = static_cast<nsCanvasFrame*>(mFrame);
|
||||
@ -234,19 +220,6 @@ public:
|
||||
aOutFrames->AppendElement(mFrame);
|
||||
}
|
||||
|
||||
virtual nsDisplayItemGeometry* AllocateGeometry(nsDisplayListBuilder* aBuilder) override
|
||||
{
|
||||
return new nsDisplayItemBoundsGeometry(this, aBuilder);
|
||||
}
|
||||
|
||||
virtual void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder,
|
||||
const nsDisplayItemGeometry* aGeometry,
|
||||
nsRegion* aInvalidRegion) override
|
||||
{
|
||||
const nsDisplayItemBoundsGeometry* geometry = static_cast<const nsDisplayItemBoundsGeometry*>(aGeometry);
|
||||
ComputeInvalidationRegionDifference(aBuilder, geometry, aInvalidRegion);
|
||||
}
|
||||
|
||||
virtual void Paint(nsDisplayListBuilder* aBuilder,
|
||||
nsRenderingContext* aCtx) override;
|
||||
|
||||
@ -259,9 +232,6 @@ public:
|
||||
#ifdef MOZ_DUMP_PAINTING
|
||||
virtual void WriteDebugInfo(std::stringstream& aStream) override;
|
||||
#endif
|
||||
|
||||
private:
|
||||
nscolor mColor;
|
||||
};
|
||||
|
||||
class nsDisplayCanvasBackgroundImage : public nsDisplayBackgroundImage {
|
||||
|
@ -22,6 +22,7 @@ public class GeckoJavaSampler {
|
||||
|
||||
// Use the same timer primitive as the profiler
|
||||
// to get a perfect sample syncing.
|
||||
@WrapForJNI
|
||||
private static native double getProfilerTime();
|
||||
|
||||
private static class Sample {
|
||||
@ -208,6 +209,3 @@ public class GeckoJavaSampler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -517,9 +517,6 @@ pref("media.video-queue.send-to-compositor-size", 9999);
|
||||
// Whether to disable the video stats to prevent fingerprinting
|
||||
pref("media.video_stats.enabled", true);
|
||||
|
||||
// Whether to enable the audio writing APIs on the audio element
|
||||
pref("media.audio_data.enabled", false);
|
||||
|
||||
// Whether to use async panning and zooming
|
||||
pref("layers.async-pan-zoom.enabled", false);
|
||||
|
||||
|
@ -20,7 +20,7 @@ const DEVICE_PREF="sanity-test.device-id";
|
||||
const VERSION_PREF="sanity-test.version";
|
||||
const DISABLE_VIDEO_PREF="media.hardware-video-decoding.failed";
|
||||
const RUNNING_PREF="sanity-test.running";
|
||||
const TIMEOUT_SEC=6;
|
||||
const TIMEOUT_SEC=20;
|
||||
|
||||
// GRAPHICS_SANITY_TEST histogram enumeration values
|
||||
const TEST_PASSED=0;
|
||||
@ -220,11 +220,14 @@ var listener = {
|
||||
this.utils = null;
|
||||
this.canvas = null;
|
||||
|
||||
this.messages.forEach((msgName) => {
|
||||
this.mm.removeMessageListener(msgName, this);
|
||||
});
|
||||
if (this.mm) {
|
||||
// We don't have a MessageManager if onWindowLoaded never fired.
|
||||
this.messages.forEach((msgName) => {
|
||||
this.mm.removeMessageListener(msgName, this);
|
||||
});
|
||||
|
||||
this.mm = null;
|
||||
this.mm = null;
|
||||
}
|
||||
|
||||
// Remove the annotation after we've cleaned everything up, to catch any
|
||||
// incidental crashes from having performed the sanity test.
|
||||
|
@ -35,6 +35,10 @@
|
||||
#include "AndroidBridge.h"
|
||||
#endif
|
||||
|
||||
#if defined(SPS_OS_android) && !defined(MOZ_WIDGET_GONK)
|
||||
#include "GeneratedJNINatives.h"
|
||||
#endif
|
||||
|
||||
#ifndef SPS_STANDALONE
|
||||
#if defined(SPS_PLAT_amd64_linux) || defined(SPS_PLAT_x86_linux)
|
||||
# define USE_LUL_STACKWALK
|
||||
@ -43,6 +47,22 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(SPS_OS_android) && !defined(MOZ_WIDGET_GONK)
|
||||
class GeckoJavaSampler : public widget::GeckoJavaSampler::Natives<GeckoJavaSampler>
|
||||
{
|
||||
private:
|
||||
GeckoJavaSampler();
|
||||
|
||||
public:
|
||||
static double GetProfilerTime() {
|
||||
if (!profiler_is_active()) {
|
||||
return 0.0;
|
||||
}
|
||||
return profiler_time();
|
||||
};
|
||||
};
|
||||
#endif
|
||||
|
||||
mozilla::ThreadLocal<PseudoStack *> tlsPseudoStack;
|
||||
mozilla::ThreadLocal<GeckoSampler *> tlsTicker;
|
||||
mozilla::ThreadLocal<void *> tlsStackTop;
|
||||
@ -473,6 +493,12 @@ void mozilla_sampler_init(void* stackTop)
|
||||
set_stderr_callback(mozilla_sampler_log);
|
||||
#endif
|
||||
|
||||
#if defined(SPS_OS_android) && !defined(MOZ_WIDGET_GONK)
|
||||
if (mozilla::jni::IsAvailable()) {
|
||||
GeckoJavaSampler::Init();
|
||||
}
|
||||
#endif
|
||||
|
||||
// We can't open pref so we use an environment variable
|
||||
// to know if we should trigger the profiler on startup
|
||||
// NOTE: Default
|
||||
|
@ -276,16 +276,10 @@ public:
|
||||
|
||||
// Make sure we increment the pointer after the name has
|
||||
// been written such that mStack is always consistent.
|
||||
entry.initCppFrame(aStackAddress, line);
|
||||
entry.setLabel(aName);
|
||||
entry.setCppFrame(aStackAddress, line);
|
||||
MOZ_ASSERT(entry.flags() == js::ProfileEntry::IS_CPP_ENTRY);
|
||||
|
||||
uint32_t uint_category = static_cast<uint32_t>(aCategory);
|
||||
MOZ_ASSERT(
|
||||
uint_category >= static_cast<uint32_t>(js::ProfileEntry::Category::FIRST) &&
|
||||
uint_category <= static_cast<uint32_t>(js::ProfileEntry::Category::LAST));
|
||||
|
||||
entry.setFlag(uint_category);
|
||||
entry.setCategory(aCategory);
|
||||
|
||||
// Track if mLabel needs a copy.
|
||||
if (aCopy)
|
||||
@ -466,4 +460,3 @@ public:
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -680,7 +680,7 @@ Java_org_mozilla_gecko_GeckoAppShell_notifyFilePickerResult(JNIEnv* jenv, jclass
|
||||
{
|
||||
class NotifyFilePickerResultRunnable : public nsRunnable {
|
||||
public:
|
||||
NotifyFilePickerResultRunnable(nsString& fileDir, long callback) :
|
||||
NotifyFilePickerResultRunnable(nsString& fileDir, long callback) :
|
||||
mFileDir(fileDir), mCallback(callback) {}
|
||||
|
||||
NS_IMETHODIMP Run() {
|
||||
@ -694,7 +694,7 @@ Java_org_mozilla_gecko_GeckoAppShell_notifyFilePickerResult(JNIEnv* jenv, jclass
|
||||
long mCallback;
|
||||
};
|
||||
nsString path = nsJNIString(filePath, jenv);
|
||||
|
||||
|
||||
nsCOMPtr<nsIRunnable> runnable =
|
||||
new NotifyFilePickerResultRunnable(path, (long)callback);
|
||||
NS_DispatchToMainThread(runnable);
|
||||
@ -767,7 +767,7 @@ Java_org_mozilla_gecko_GeckoAppShell_getSurfaceBits(JNIEnv* jenv, jclass, jobjec
|
||||
for (int i = 0; i < srcHeight; i++) {
|
||||
memcpy(bitsCopy + ((dstHeight - i - 1) * dstWidth * bpp), bits + (i * srcStride * bpp), srcStride * bpp);
|
||||
}
|
||||
|
||||
|
||||
if (!jSurfaceBitsClass) {
|
||||
jSurfaceBitsClass = (jclass)jenv->NewGlobalRef(jenv->FindClass("org/mozilla/gecko/SurfaceBits"));
|
||||
jSurfaceBitsCtor = jenv->GetMethodID(jSurfaceBitsClass, "<init>", "()V");
|
||||
@ -852,15 +852,6 @@ Java_org_mozilla_gecko_GeckoAppShell_dispatchMemoryPressure(JNIEnv* jenv, jclass
|
||||
NS_DispatchMemoryPressure(MemPressure_New);
|
||||
}
|
||||
|
||||
NS_EXPORT jdouble JNICALL
|
||||
Java_org_mozilla_gecko_GeckoJavaSampler_getProfilerTime(JNIEnv *jenv, jclass jc)
|
||||
{
|
||||
if (!profiler_is_active()) {
|
||||
return 0.0;
|
||||
}
|
||||
return profiler_time();
|
||||
}
|
||||
|
||||
NS_EXPORT void JNICALL
|
||||
Java_org_mozilla_gecko_gfx_NativePanZoomController_abortAnimation(JNIEnv* env, jobject instance)
|
||||
{
|
||||
|
@ -36,6 +36,21 @@ public:
|
||||
template<class Impl>
|
||||
constexpr JNINativeMethod ANRReporter::Natives<Impl>::methods[];
|
||||
|
||||
template<class Impl>
|
||||
class GeckoJavaSampler::Natives : public mozilla::jni::NativeImpl<GeckoJavaSampler, Impl>
|
||||
{
|
||||
public:
|
||||
static constexpr JNINativeMethod methods[] = {
|
||||
|
||||
mozilla::jni::MakeNativeMethod<GeckoJavaSampler::GetProfilerTime_t>(
|
||||
mozilla::jni::NativeStub<GeckoJavaSampler::GetProfilerTime_t, Impl>
|
||||
::template Wrap<&Impl::GetProfilerTime>)
|
||||
};
|
||||
};
|
||||
|
||||
template<class Impl>
|
||||
constexpr JNINativeMethod GeckoJavaSampler::Natives<Impl>::methods[];
|
||||
|
||||
template<class Impl>
|
||||
class GeckoThread::Natives : public mozilla::jni::NativeImpl<GeckoThread, Impl>
|
||||
{
|
||||
|
@ -715,6 +715,9 @@ auto GeckoJavaSampler::GetFrameNameJavaProfilingWrapper(int32_t a0, int32_t a1,
|
||||
return mozilla::jni::Method<GetFrameNameJavaProfilingWrapper_t>::Call(nullptr, nullptr, a0, a1, a2);
|
||||
}
|
||||
|
||||
constexpr char GeckoJavaSampler::GetProfilerTime_t::name[];
|
||||
constexpr char GeckoJavaSampler::GetProfilerTime_t::signature[];
|
||||
|
||||
constexpr char GeckoJavaSampler::GetSampleTimeJavaProfiling_t::name[];
|
||||
constexpr char GeckoJavaSampler::GetSampleTimeJavaProfiling_t::signature[];
|
||||
|
||||
|
@ -1694,6 +1694,21 @@ public:
|
||||
|
||||
static auto GetFrameNameJavaProfilingWrapper(int32_t, int32_t, int32_t) -> mozilla::jni::String::LocalRef;
|
||||
|
||||
public:
|
||||
struct GetProfilerTime_t {
|
||||
typedef GeckoJavaSampler Owner;
|
||||
typedef double ReturnType;
|
||||
typedef double SetterType;
|
||||
typedef mozilla::jni::Args<> Args;
|
||||
static constexpr char name[] = "getProfilerTime";
|
||||
static constexpr char signature[] =
|
||||
"()D";
|
||||
static const bool isStatic = true;
|
||||
static const bool isMultithreaded = false;
|
||||
static const mozilla::jni::ExceptionMode exceptionMode =
|
||||
mozilla::jni::ExceptionMode::ABORT;
|
||||
};
|
||||
|
||||
public:
|
||||
struct GetSampleTimeJavaProfiling_t {
|
||||
typedef GeckoJavaSampler Owner;
|
||||
@ -1801,6 +1816,8 @@ public:
|
||||
|
||||
static auto UnpauseJavaProfiling() -> void;
|
||||
|
||||
public:
|
||||
template<class Impl> class Natives;
|
||||
};
|
||||
|
||||
class GeckoThread : public mozilla::jni::Class<GeckoThread>
|
||||
|
@ -19,6 +19,7 @@ EXPORTS += [
|
||||
'AndroidBridge.h',
|
||||
'AndroidJavaWrappers.h',
|
||||
'AndroidJNIWrapper.h',
|
||||
'GeneratedJNINatives.h',
|
||||
'GeneratedJNIWrappers.h',
|
||||
]
|
||||
|
||||
|
@ -699,28 +699,6 @@ public:
|
||||
|
||||
void OnSelectionChange(const IMENotification& aIMENotification);
|
||||
|
||||
/**
|
||||
* DispatchCompositionChangeEvent() dispatches a compositionchange event on
|
||||
* mWidget.
|
||||
*
|
||||
* @param aText User text input.
|
||||
* @param aAttrString An NSAttributedString instance which indicates
|
||||
* current composition string.
|
||||
* @param aSelectedRange Current selected range (or caret position).
|
||||
*/
|
||||
bool DispatchCompositionChangeEvent(const nsString& aText,
|
||||
NSAttributedString* aAttrString,
|
||||
NSRange& aSelectedRange);
|
||||
|
||||
/**
|
||||
* DispatchCompositionCommitEvent() dispatches a compositioncommit event or
|
||||
* compositioncommitasis event. If aCommitString is null, dispatches
|
||||
* compositioncommitasis event. I.e., if aCommitString is null, this
|
||||
* commits the composition with the last data. Otherwise, commits the
|
||||
* composition with aCommitString value.
|
||||
*/
|
||||
bool DispatchCompositionCommitEvent(const nsAString* aCommitString = nullptr);
|
||||
|
||||
/**
|
||||
* SetMarkedText() is a handler of setMarkedText of NSTextInput.
|
||||
*
|
||||
@ -887,9 +865,8 @@ protected:
|
||||
private:
|
||||
// If mIsIMEComposing is true, the composition string is stored here.
|
||||
NSString* mIMECompositionString;
|
||||
// mLastDispatchedCompositionString stores the lastest dispatched composition
|
||||
// string by compositionupdate event.
|
||||
nsString mLastDispatchedCompositionString;
|
||||
// If mIsIMEComposing is true, the start offset of the composition string.
|
||||
uint32_t mIMECompositionStart;
|
||||
|
||||
NSRange mMarkedRange;
|
||||
NSRange mSelectedRange;
|
||||
@ -974,19 +951,43 @@ private:
|
||||
void InitCompositionEvent(WidgetCompositionEvent& aCompositionEvent);
|
||||
|
||||
/**
|
||||
* When a composition starts, OnStartIMEComposition() is called.
|
||||
* DispatchCompositionStartEvent() dispatches a compositionstart event and
|
||||
* initializes the members indicating composition state.
|
||||
*
|
||||
* @return true if it can continues handling composition.
|
||||
* Otherwise, e.g., canceled by the web page,
|
||||
* this returns false.
|
||||
*/
|
||||
void OnStartIMEComposition();
|
||||
bool DispatchCompositionStartEvent();
|
||||
|
||||
/**
|
||||
* When a composition is updated, OnUpdateIMEComposition() is called.
|
||||
* DispatchCompositionChangeEvent() dispatches a compositionchange event on
|
||||
* mWidget and modifies the members indicating composition state.
|
||||
*
|
||||
* @param aText User text input.
|
||||
* @param aAttrString An NSAttributedString instance which indicates
|
||||
* current composition string.
|
||||
* @param aSelectedRange Current selected range (or caret position).
|
||||
*
|
||||
* @return true if it can continues handling composition.
|
||||
* Otherwise, e.g., canceled by the web page,
|
||||
* this returns false.
|
||||
*/
|
||||
void OnUpdateIMEComposition(NSString* aIMECompositionString);
|
||||
bool DispatchCompositionChangeEvent(const nsString& aText,
|
||||
NSAttributedString* aAttrString,
|
||||
NSRange& aSelectedRange);
|
||||
|
||||
/**
|
||||
* When a composition is finished, OnEndIMEComposition() is called.
|
||||
* DispatchCompositionCommitEvent() dispatches a compositioncommit event or
|
||||
* compositioncommitasis event. If aCommitString is null, dispatches
|
||||
* compositioncommitasis event. I.e., if aCommitString is null, this
|
||||
* commits the composition with the last data. Otherwise, commits the
|
||||
* composition with aCommitString value.
|
||||
*
|
||||
* @return true if the widget isn't destroyed.
|
||||
* Otherwise, false.
|
||||
*/
|
||||
void OnEndIMEComposition();
|
||||
bool DispatchCompositionCommitEvent(const nsAString* aCommitString = nullptr);
|
||||
|
||||
// The focused IME handler. Please note that the handler might lost the
|
||||
// actual focus by deactivating the application. If we are active, this
|
||||
|
@ -4,12 +4,12 @@
|
||||
* 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/ArrayUtils.h"
|
||||
|
||||
#include "TextInputHandler.h"
|
||||
|
||||
#include "mozilla/Logging.h"
|
||||
|
||||
#include "mozilla/ArrayUtils.h"
|
||||
#include "mozilla/AutoRestore.h"
|
||||
#include "mozilla/MiscEvents.h"
|
||||
#include "mozilla/MouseEvents.h"
|
||||
#include "mozilla/TextEvents.h"
|
||||
@ -2695,23 +2695,66 @@ IMEInputHandler::CreateTextRangeArray(NSAttributedString *aAttrString,
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL;
|
||||
}
|
||||
|
||||
bool
|
||||
IMEInputHandler::DispatchCompositionStartEvent()
|
||||
{
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::DispatchCompositionStartEvent, "
|
||||
"mSelectedRange={ location=%llu, length=%llu }, Destroyed()=%s, "
|
||||
"mView=%p, mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
|
||||
this, SelectedRange().location, mSelectedRange.length,
|
||||
TrueOrFalse(Destroyed()), mView, mWidget,
|
||||
mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
|
||||
|
||||
WidgetCompositionEvent compositionStartEvent(true, eCompositionStart,
|
||||
mWidget);
|
||||
InitCompositionEvent(compositionStartEvent);
|
||||
|
||||
NS_ASSERTION(!mIsIMEComposing, "There is a composition already");
|
||||
mIsIMEComposing = true;
|
||||
|
||||
DispatchEvent(compositionStartEvent);
|
||||
|
||||
if (Destroyed()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::DispatchCompositionStartEvent, "
|
||||
"destroyed by compositionstart event", this));
|
||||
return false;
|
||||
}
|
||||
|
||||
// FYI: compositionstart may cause committing composition by the webapp.
|
||||
if (!mIsIMEComposing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FYI: The selection range might have been modified by a compositionstart
|
||||
// event handler.
|
||||
mIMECompositionStart = SelectedRange().location;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
IMEInputHandler::DispatchCompositionChangeEvent(const nsString& aText,
|
||||
NSAttributedString* aAttrString,
|
||||
NSRange& aSelectedRange)
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
|
||||
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::DispatchCompositionChangeEvent, "
|
||||
"aText=\"%s\", aAttrString=\"%s\", "
|
||||
"aSelectedRange={ location=%llu, length=%llu }, "
|
||||
"Destroyed()=%s",
|
||||
"aSelectedRange={ location=%llu, length=%llu }, Destroyed()=%s, mView=%p, "
|
||||
"mWidget=%p, inputContext=%p, mIsIMEComposing=%s",
|
||||
this, NS_ConvertUTF16toUTF8(aText).get(),
|
||||
GetCharacters([aAttrString string]),
|
||||
aSelectedRange.location, aSelectedRange.length,
|
||||
TrueOrFalse(Destroyed())));
|
||||
TrueOrFalse(Destroyed()), mView, mWidget,
|
||||
mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
|
||||
|
||||
NS_ENSURE_TRUE(!Destroyed(), false);
|
||||
|
||||
NS_ASSERTION(mIsIMEComposing, "We're not in composition");
|
||||
|
||||
nsRefPtr<IMEInputHandler> kungFuDeathGrip(this);
|
||||
|
||||
WidgetCompositionEvent compositionChangeEvent(true, eCompositionChange,
|
||||
@ -2720,33 +2763,90 @@ IMEInputHandler::DispatchCompositionChangeEvent(const nsString& aText,
|
||||
compositionChangeEvent.mData = aText;
|
||||
compositionChangeEvent.mRanges =
|
||||
CreateTextRangeArray(aAttrString, aSelectedRange);
|
||||
return DispatchEvent(compositionChangeEvent);
|
||||
|
||||
mSelectedRange.location = mIMECompositionStart + aSelectedRange.location;
|
||||
mSelectedRange.length = aSelectedRange.length;
|
||||
|
||||
if (mIMECompositionString) {
|
||||
[mIMECompositionString release];
|
||||
}
|
||||
mIMECompositionString = [[aAttrString string] retain];
|
||||
|
||||
DispatchEvent(compositionChangeEvent);
|
||||
|
||||
if (Destroyed()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::DispatchCompositionChangeEvent, "
|
||||
"destroyed by compositionchange event", this));
|
||||
return false;
|
||||
}
|
||||
|
||||
// FYI: compositionstart may cause committing composition by the webapp.
|
||||
return mIsIMEComposing;
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false);
|
||||
}
|
||||
|
||||
bool
|
||||
IMEInputHandler::DispatchCompositionCommitEvent(const nsAString* aCommitString)
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
|
||||
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::DispatchCompositionCommitEvent, "
|
||||
"aCommitString=0x%p (\"%s\"), Destroyed()=%s",
|
||||
"aCommitString=0x%p (\"%s\"), Destroyed()=%s, mView=%p, mWidget=%p, "
|
||||
"inputContext=%p, mIsIMEComposing=%s",
|
||||
this, aCommitString,
|
||||
aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : "",
|
||||
TrueOrFalse(Destroyed())));
|
||||
TrueOrFalse(Destroyed()), mView, mWidget,
|
||||
mView ? [mView inputContext] : nullptr, TrueOrFalse(mIsIMEComposing)));
|
||||
|
||||
if (NS_WARN_IF(Destroyed())) {
|
||||
return false;
|
||||
}
|
||||
NS_ASSERTION(mIsIMEComposing, "We're not in composition");
|
||||
|
||||
nsRefPtr<IMEInputHandler> kungFuDeathGrip(this);
|
||||
|
||||
EventMessage message =
|
||||
aCommitString ? eCompositionCommit : eCompositionCommitAsIs;
|
||||
WidgetCompositionEvent compositionCommitEvent(true, message, mWidget);
|
||||
compositionCommitEvent.time = PR_IntervalNow();
|
||||
if (aCommitString) {
|
||||
compositionCommitEvent.mData = *aCommitString;
|
||||
if (!Destroyed()) {
|
||||
EventMessage message =
|
||||
aCommitString ? eCompositionCommit : eCompositionCommitAsIs;
|
||||
WidgetCompositionEvent compositionCommitEvent(true, message, mWidget);
|
||||
compositionCommitEvent.time = PR_IntervalNow();
|
||||
if (aCommitString) {
|
||||
compositionCommitEvent.mData = *aCommitString;
|
||||
}
|
||||
|
||||
// IME may query selection immediately after this, however, in e10s mode,
|
||||
// OnSelectionChange() will be called asynchronously. Until then, we
|
||||
// should emulate expected selection range if the webapp does nothing.
|
||||
mSelectedRange.location = mIMECompositionStart;
|
||||
if (message == eCompositionCommit) {
|
||||
mSelectedRange.location += compositionCommitEvent.mData.Length();
|
||||
} else if (mIMECompositionString) {
|
||||
nsAutoString commitString;
|
||||
nsCocoaUtils::GetStringForNSString(mIMECompositionString, commitString);
|
||||
mSelectedRange.location += commitString.Length();
|
||||
}
|
||||
mSelectedRange.length = 0;
|
||||
|
||||
DispatchEvent(compositionCommitEvent);
|
||||
}
|
||||
return DispatchEvent(compositionCommitEvent);
|
||||
|
||||
mIsIMEComposing = false;
|
||||
mIMECompositionStart = UINT32_MAX;
|
||||
if (mIMECompositionString) {
|
||||
[mIMECompositionString release];
|
||||
mIMECompositionString = nullptr;
|
||||
}
|
||||
|
||||
if (Destroyed()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::DispatchCompositionCommitEvent, "
|
||||
"destroyed by compositioncommit event", this));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(false);
|
||||
}
|
||||
|
||||
void
|
||||
@ -2787,15 +2887,13 @@ IMEInputHandler::InsertTextAsCommittingComposition(
|
||||
if (IsIMEComposing() && aReplacementRange &&
|
||||
aReplacementRange->location != NSNotFound &&
|
||||
!NSEqualRanges(MarkedRange(), *aReplacementRange)) {
|
||||
DispatchCompositionCommitEvent();
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionCommitEvent()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::InsertTextAsCommittingComposition, "
|
||||
"destroyed by commiting composition for setting replacement range",
|
||||
this));
|
||||
return;
|
||||
}
|
||||
OnEndIMEComposition();
|
||||
}
|
||||
|
||||
nsRefPtr<IMEInputHandler> kungFuDeathGrip(this);
|
||||
@ -2811,32 +2909,21 @@ IMEInputHandler::InsertTextAsCommittingComposition(
|
||||
NS_ENSURE_TRUE_VOID(SetSelection(*aReplacementRange));
|
||||
}
|
||||
|
||||
// XXXmnakano Probably, we shouldn't emulate composition in this case.
|
||||
// I think that we should just fire DOM3 textInput event if we implement it.
|
||||
WidgetCompositionEvent compStart(true, eCompositionStart, mWidget);
|
||||
InitCompositionEvent(compStart);
|
||||
|
||||
DispatchEvent(compStart);
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionStartEvent()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::InsertTextAsCommittingComposition, "
|
||||
"destroyed by compositionstart event", this));
|
||||
"cannot continue handling composition after compositionstart", this));
|
||||
return;
|
||||
}
|
||||
|
||||
OnStartIMEComposition();
|
||||
}
|
||||
|
||||
DispatchCompositionCommitEvent(&str);
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionCommitEvent(&str)) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::InsertTextAsCommittingComposition, "
|
||||
"destroyed by compositioncommit event", this));
|
||||
return;
|
||||
}
|
||||
|
||||
OnEndIMEComposition();
|
||||
|
||||
mMarkedRange = NSMakeRange(NSNotFound, 0);
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK;
|
||||
@ -2874,18 +2961,15 @@ IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString,
|
||||
if (IsIMEComposing() && aReplacementRange &&
|
||||
aReplacementRange->location != NSNotFound &&
|
||||
!NSEqualRanges(MarkedRange(), *aReplacementRange)) {
|
||||
bool ignoreIMECommit = mIgnoreIMECommit;
|
||||
AutoRestore<bool> ignoreIMECommit(mIgnoreIMECommit);
|
||||
mIgnoreIMECommit = false;
|
||||
DispatchCompositionCommitEvent();
|
||||
mIgnoreIMECommit = ignoreIMECommit;
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionCommitEvent()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::SetMarkedText, "
|
||||
"destroyed by commiting composition for setting replacement range",
|
||||
this));
|
||||
return;
|
||||
}
|
||||
OnEndIMEComposition();
|
||||
}
|
||||
|
||||
nsString str;
|
||||
@ -2903,53 +2987,30 @@ IMEInputHandler::SetMarkedText(NSAttributedString* aAttrString,
|
||||
|
||||
mMarkedRange.location = SelectedRange().location;
|
||||
|
||||
WidgetCompositionEvent compStart(true, eCompositionStart, mWidget);
|
||||
InitCompositionEvent(compStart);
|
||||
|
||||
DispatchEvent(compStart);
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionStartEvent()) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::SetMarkedText, "
|
||||
"destroyed by compositionstart event", this));
|
||||
("%p IMEInputHandler::SetMarkedText, cannot continue handling "
|
||||
"composition after dispatching compositionstart", this));
|
||||
return;
|
||||
}
|
||||
|
||||
OnStartIMEComposition();
|
||||
}
|
||||
|
||||
if (!IsIMEComposing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str.IsEmpty()) {
|
||||
OnUpdateIMEComposition([aAttrString string]);
|
||||
|
||||
// Set temprary range for Apple Japanese IME with e10s because
|
||||
// SelectedRange may return invalid range until OnSelectionChange is
|
||||
// called from content process.
|
||||
// This value will be updated by OnSelectionChange soon.
|
||||
mSelectedRange.location = aSelectedRange.location + mMarkedRange.location;
|
||||
mSelectedRange.length = aSelectedRange.length;
|
||||
|
||||
DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange);
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionChangeEvent(str, aAttrString, aSelectedRange)) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::SetMarkedText, "
|
||||
"destroyed by compositionchange event", this));
|
||||
("%p IMEInputHandler::SetMarkedText, cannot continue handling "
|
||||
"composition after dispatching compositionchange", this));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the composition string becomes empty string, we should commit
|
||||
// current composition.
|
||||
DispatchCompositionCommitEvent(&EmptyString());
|
||||
if (Destroyed()) {
|
||||
if (!DispatchCompositionCommitEvent(&EmptyString())) {
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::SetMarkedText, "
|
||||
"destroyed by compositioncommit event", this));
|
||||
return;
|
||||
}
|
||||
OnEndIMEComposition();
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK;
|
||||
}
|
||||
@ -3003,6 +3064,40 @@ IMEInputHandler::GetAttributedSubstringFromRange(NSRange& aRange,
|
||||
|
||||
nsRefPtr<IMEInputHandler> kungFuDeathGrip(this);
|
||||
|
||||
// If we're in composing, the queried range may be in the composition string.
|
||||
// In such case, we should use mIMECompositionString since if the composition
|
||||
// string is handled by a remote process, the content cache may be out of
|
||||
// date.
|
||||
NSUInteger compositionLength =
|
||||
mIMECompositionString ? [mIMECompositionString length] : 0;
|
||||
if (mIMECompositionStart != UINT32_MAX &&
|
||||
mIMECompositionStart >= aRange.location &&
|
||||
mIMECompositionStart + compositionLength <=
|
||||
aRange.location + aRange.length) {
|
||||
NSRange range =
|
||||
NSMakeRange(aRange.location - mIMECompositionStart, aRange.length);
|
||||
NSString* nsstr = [mIMECompositionString substringWithRange:range];
|
||||
NSMutableAttributedString* result =
|
||||
[[[NSMutableAttributedString alloc] initWithString:nsstr
|
||||
attributes:nil] autorelease];
|
||||
// XXX We cannot return font information in this case. However, this
|
||||
// case must occur only when IME tries to confirm if composing string
|
||||
// is handled as expected.
|
||||
if (aActualRange) {
|
||||
*aActualRange = aRange;
|
||||
}
|
||||
|
||||
if (MOZ_LOG_TEST(gLog, LogLevel::Info)) {
|
||||
nsAutoString str;
|
||||
nsCocoaUtils::GetStringForNSString(nsstr, str);
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::GetAttributedSubstringFromRange, "
|
||||
"computed with mIMECompositionString (result string=\"%s\")",
|
||||
this, NS_ConvertUTF16toUTF8(str).get()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
nsAutoString str;
|
||||
WidgetQueryContentEvent textContent(true, eQueryTextContent, mWidget);
|
||||
textContent.InitForQueryTextContent(aRange.location, aRange.length);
|
||||
@ -3307,12 +3402,17 @@ IMEInputHandler::GetValidAttributesForMarkedText()
|
||||
******************************************************************************/
|
||||
|
||||
IMEInputHandler::IMEInputHandler(nsChildView* aWidget,
|
||||
NSView<mozView> *aNativeView) :
|
||||
TextInputHandlerBase(aWidget, aNativeView),
|
||||
mPendingMethods(0), mIMECompositionString(nullptr),
|
||||
mIsIMEComposing(false), mIsIMEEnabled(true),
|
||||
mIsASCIICapableOnly(false), mIgnoreIMECommit(false),
|
||||
mIsInFocusProcessing(false), mIMEHasFocus(false)
|
||||
NSView<mozView> *aNativeView)
|
||||
: TextInputHandlerBase(aWidget, aNativeView)
|
||||
, mPendingMethods(0)
|
||||
, mIMECompositionString(nullptr)
|
||||
, mIMECompositionStart(UINT32_MAX)
|
||||
, mIsIMEComposing(false)
|
||||
, mIsIMEEnabled(true)
|
||||
, mIsASCIICapableOnly(false)
|
||||
, mIgnoreIMECommit(false)
|
||||
, mIsInFocusProcessing(false)
|
||||
, mIMEHasFocus(false)
|
||||
{
|
||||
InitStaticMembers();
|
||||
|
||||
@ -3331,6 +3431,10 @@ IMEInputHandler::~IMEInputHandler()
|
||||
if (sFocusedIMEHandler == this) {
|
||||
sFocusedIMEHandler = nullptr;
|
||||
}
|
||||
if (mIMECompositionString) {
|
||||
[mIMECompositionString release];
|
||||
mIMECompositionString = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@ -3383,7 +3487,6 @@ IMEInputHandler::OnDestroyWidget(nsChildView* aDestroyingWidget)
|
||||
if (IsIMEComposing()) {
|
||||
// If our view is in the composition, we should clean up it.
|
||||
CancelIMEComposition();
|
||||
OnEndIMEComposition();
|
||||
}
|
||||
|
||||
mSelectedRange.location = NSNotFound; // Marking dirty
|
||||
@ -3392,66 +3495,6 @@ IMEInputHandler::OnDestroyWidget(nsChildView* aDestroyingWidget)
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
IMEInputHandler::OnStartIMEComposition()
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
|
||||
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::OnStartIMEComposition, mView=%p, mWidget=%p"
|
||||
"inputContext=%p, mIsIMEComposing=%s",
|
||||
this, mView, mWidget, mView ? [mView inputContext] : nullptr,
|
||||
TrueOrFalse(mIsIMEComposing)));
|
||||
|
||||
NS_ASSERTION(!mIsIMEComposing, "There is a composition already");
|
||||
mIsIMEComposing = true;
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK;
|
||||
}
|
||||
|
||||
void
|
||||
IMEInputHandler::OnUpdateIMEComposition(NSString* aIMECompositionString)
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
|
||||
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::OnUpdateIMEComposition, mView=%p, mWidget=%p, "
|
||||
"inputContext=%p, mIsIMEComposing=%s, aIMECompositionString=\"%s\"",
|
||||
this, mView, mWidget, mView ? [mView inputContext] : nullptr,
|
||||
TrueOrFalse(mIsIMEComposing), GetCharacters(aIMECompositionString)));
|
||||
|
||||
NS_ASSERTION(mIsIMEComposing, "We're not in composition");
|
||||
|
||||
if (mIMECompositionString)
|
||||
[mIMECompositionString release];
|
||||
mIMECompositionString = [aIMECompositionString retain];
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK;
|
||||
}
|
||||
|
||||
void
|
||||
IMEInputHandler::OnEndIMEComposition()
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
|
||||
|
||||
MOZ_LOG(gLog, LogLevel::Info,
|
||||
("%p IMEInputHandler::OnEndIMEComposition, mView=%p, mWidget=%p, "
|
||||
"inputContext=%p, mIsIMEComposing=%s",
|
||||
this, mView, mWidget, mView ? [mView inputContext] : nullptr,
|
||||
TrueOrFalse(mIsIMEComposing)));
|
||||
|
||||
NS_ASSERTION(mIsIMEComposing, "We're not in composition");
|
||||
|
||||
mIsIMEComposing = false;
|
||||
|
||||
if (mIMECompositionString) {
|
||||
[mIMECompositionString release];
|
||||
mIMECompositionString = nullptr;
|
||||
}
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK;
|
||||
}
|
||||
|
||||
void
|
||||
IMEInputHandler::SendCommittedText(NSString *aString)
|
||||
{
|
||||
|
@ -116,7 +116,9 @@ nsAppShell::Init()
|
||||
strcmp(name, "png") &&
|
||||
strcmp(name, "gif") &&
|
||||
strcmp(name, "bmp") &&
|
||||
strcmp(name, "ico")) {
|
||||
strcmp(name, "ico") &&
|
||||
strcmp(name, "xpm") &&
|
||||
strcmp(name, "svg")) {
|
||||
gdk_pixbuf_format_set_disabled(format, TRUE);
|
||||
}
|
||||
g_free(name);
|
||||
|
@ -816,6 +816,16 @@ public:
|
||||
NS_LITERAL_STRING("\x5FAE\x8EDF\x8F38\x5165\x6CD5")));
|
||||
}
|
||||
|
||||
bool IsMSOfficeJapaneseIME2010Active() const
|
||||
{
|
||||
// {54EDCC94-1524-4BB1-9FB7-7BABE4F4CA64}
|
||||
static const GUID kGUID = {
|
||||
0x54EDCC94, 0x1524, 0x4BB1,
|
||||
{ 0x9F, 0xB7, 0x7B, 0xAB, 0xE4, 0xF4, 0xCA, 0x64 }
|
||||
};
|
||||
return mActiveTIPGUID == kGUID;
|
||||
}
|
||||
|
||||
bool IsATOKActive() const
|
||||
{
|
||||
// FYI: Name of ATOK includes the release year like "ATOK 2015".
|
||||
@ -919,6 +929,9 @@ private:
|
||||
// i.e., IMM-IME or just a keyboard layout, this is empty.
|
||||
nsString mActiveTIPKeyboardDescription;
|
||||
|
||||
// Active TIP's GUID
|
||||
GUID mActiveTIPGUID;
|
||||
|
||||
static StaticRefPtr<TSFStaticSink> sInstance;
|
||||
};
|
||||
|
||||
@ -930,6 +943,7 @@ TSFStaticSink::TSFStaticSink()
|
||||
, mLangID(0)
|
||||
, mIsIMM_IME(false)
|
||||
, mOnActivatedCalled(false)
|
||||
, mActiveTIPGUID(GUID_NULL)
|
||||
{
|
||||
}
|
||||
|
||||
@ -1044,6 +1058,7 @@ TSFStaticSink::OnActivated(REFCLSID clsid, REFGUID guidProfile,
|
||||
if (fActivated) {
|
||||
// TODO: We should check if the profile's category is keyboard or not.
|
||||
mOnActivatedCalled = true;
|
||||
mActiveTIPGUID = guidProfile;
|
||||
mIsIMM_IME = IsIMM_IME(::GetKeyboardLayout(0));
|
||||
|
||||
HRESULT hr = mInputProcessorProfiles->GetCurrentLanguage(&mLangID);
|
||||
@ -1086,6 +1101,7 @@ TSFStaticSink::OnActivated(DWORD dwProfileType,
|
||||
(dwProfileType == TF_PROFILETYPE_KEYBOARDLAYOUT ||
|
||||
catid == GUID_TFCAT_TIP_KEYBOARD)) {
|
||||
mOnActivatedCalled = true;
|
||||
mActiveTIPGUID = guidProfile;
|
||||
mLangID = langid;
|
||||
mIsIMM_IME = IsIMM_IME(hkl);
|
||||
GetTIPDescription(rclsid, mLangID, guidProfile,
|
||||
@ -3507,19 +3523,27 @@ TSFTextStore::GetTextExt(TsViewCookie vcView,
|
||||
if (mComposition.IsComposing() && mComposition.mStart < acpEnd &&
|
||||
mLockedContent.IsLayoutChangedAfter(acpEnd)) {
|
||||
const Selection& currentSel = CurrentSelection();
|
||||
if ((sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar ||
|
||||
sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret) &&
|
||||
kSink->IsMSJapaneseIMEActive()) {
|
||||
// The bug of Microsoft Office IME 2010 for Japanese is similar to
|
||||
// MS-IME for Win 8.1 and Win 10. Newer version of MS Office IME is not
|
||||
// released yet. So, we can hack it without prefs because there must be
|
||||
// no developers who want to disable this hack for tests.
|
||||
const bool kIsMSOfficeJapaneseIME2010 =
|
||||
kSink->IsMSOfficeJapaneseIME2010Active();
|
||||
if (kIsMSOfficeJapaneseIME2010 ||
|
||||
((sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar ||
|
||||
sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret) &&
|
||||
kSink->IsMSJapaneseIMEActive())) {
|
||||
// MS IME for Japanese doesn't support asynchronous handling at deciding
|
||||
// its suggest list window position. The feature was implemented
|
||||
// starting from Windows 8.
|
||||
if (IsWin8OrLater()) {
|
||||
if (IsWin8OrLater() || kIsMSOfficeJapaneseIME2010) {
|
||||
// Basically, MS-IME tries to retrieve whole composition string rect
|
||||
// at deciding suggest window immediately after unlocking the document.
|
||||
// However, in e10s mode, the content hasn't updated yet in most cases.
|
||||
// Therefore, if the first character at the retrieving range rect is
|
||||
// available, we should use it as the result.
|
||||
if (sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar &&
|
||||
if ((kIsMSOfficeJapaneseIME2010 ||
|
||||
sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtFirstChar) &&
|
||||
!mLockedContent.IsLayoutChangedAfter(acpStart) &&
|
||||
acpStart < acpEnd) {
|
||||
acpEnd = acpStart;
|
||||
@ -3533,7 +3557,8 @@ TSFTextStore::GetTextExt(TsViewCookie vcView,
|
||||
// caret rect immediately after modifying the composition string but
|
||||
// before unlocking the document. In such case, we should return the
|
||||
// nearest character rect.
|
||||
else if (sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret &&
|
||||
else if ((kIsMSOfficeJapaneseIME2010 ||
|
||||
sDoNotReturnNoLayoutErrorToMSJapaneseIMEAtCaret) &&
|
||||
acpStart == acpEnd &&
|
||||
currentSel.IsCollapsed() && currentSel.EndOffset() == acpEnd) {
|
||||
acpEnd = acpStart = mLockedContent.MinOffsetOfLayoutChanged();
|
||||
|
@ -788,6 +788,7 @@
|
||||
ERROR(NS_ERROR_XPATH_UNBALANCED_CURLY_BRACE, FAILURE(29)),
|
||||
ERROR(NS_ERROR_XSLT_BAD_NODE_NAME, FAILURE(30)),
|
||||
ERROR(NS_ERROR_XSLT_VAR_ALREADY_SET, FAILURE(31)),
|
||||
ERROR(NS_ERROR_XSLT_CALL_TO_KEY_NOT_ALLOWED, FAILURE(32)),
|
||||
|
||||
ERROR(NS_XSLT_GET_NEW_HANDLER, SUCCESS(1)),
|
||||
#undef MODULE
|
||||
|
Loading…
Reference in New Issue
Block a user