Merge m-c to m-i, a=merge

This commit is contained in:
Seth Fowler 2015-09-19 13:28:29 -07:00
commit c383ca71b8
96 changed files with 3207 additions and 1269 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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');

View File

@ -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 },

View File

@ -249,6 +249,7 @@ public:
bool CanBeFormatted();
bool CanBeShared();
bool IsRemovable();
bool LowDiskSpace();
bool Default();
void GetStorageName(nsAString& aStorageName);

View File

@ -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;
}

View File

@ -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;

View File

@ -3374,6 +3374,12 @@ nsDOMDeviceStorage::IsRemovable()
return mIsRemovable;
}
bool
nsDOMDeviceStorage::LowDiskSpace()
{
return DeviceStorageStatics::LowDiskSpace();
}
already_AddRefed<Promise>
nsDOMDeviceStorage::GetRoot(ErrorResult& aRv)
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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'},

View File

@ -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,
};
},
};

View File

@ -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
};

View File

@ -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) {

View File

@ -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;
};

View File

@ -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) {

View File

@ -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 += [

View File

@ -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);
}
};
});

View File

@ -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"

View File

@ -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);
}

View 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>

View File

@ -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
View 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));

View File

@ -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"));
});
}
*/
}

View File

@ -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');
});

View File

@ -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

View File

@ -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();

View File

@ -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;
};

View File

@ -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();
};

View File

@ -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

View File

@ -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 */

View File

@ -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();

View 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>

View File

@ -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

View File

@ -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;
}
};
/*

View File

@ -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;

View File

@ -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,

View File

@ -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);

View File

@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -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>

View File

@ -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
View 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

View File

@ -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

View File

@ -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

View File

@ -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());
}

View 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());
}
}
}

View File

@ -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

View File

@ -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));
}

View File

@ -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*

View 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);

View 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) {}
}

View 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() {})();

View File

@ -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)",

View File

@ -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;
{

View File

@ -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);

View File

@ -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);

View File

@ -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 =

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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());

View File

@ -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);

View File

@ -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()))

View File

@ -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:

View File

@ -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);

View File

@ -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
{

View File

@ -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 {

View File

@ -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;
}

View File

@ -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

View File

@ -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();
}

View File

@ -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;
};
/**

View File

@ -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) {

View File

@ -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 {

View File

@ -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 {
}
}
}

View File

@ -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);

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)
{

View File

@ -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>
{

View File

@ -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[];

View File

@ -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>

View File

@ -19,6 +19,7 @@ EXPORTS += [
'AndroidBridge.h',
'AndroidJavaWrappers.h',
'AndroidJNIWrapper.h',
'GeneratedJNINatives.h',
'GeneratedJNIWrappers.h',
]

View File

@ -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

View File

@ -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)
{

View File

@ -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);

View File

@ -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();

View File

@ -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