author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Sun, 17 Jul 2016 10:08:08 +0200 | |
changeset 305234 | 711963e8daa312ae06409f8ab5c06612cb0b8f7b |
parent 305134 | ef5f932101e5b833b2429407cb0873471b4d764e (current diff) |
parent 305233 | bd1d2c40a07b7b5760ddc81a46d594b9b37d0983 (diff) |
child 305235 | 53900593419c215f37e70c40763ca8b45d86ecb5 |
child 305275 | 0fbdcd21fad76a00328e67875c6f40dc219235f4 |
child 305310 | ce145f8f4c4a7d08b107f6796f1f03deb1d41ff6 |
child 305320 | 9c04250731112aaf2a6774aa79e7781851e8a5b6 |
push id | 30633 |
push user | cbook@mozilla.com |
push date | Sun, 17 Jul 2016 08:11:38 +0000 |
treeherder | autoland@53900593419c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 50.0a1 |
first release with | nightly linux32
711963e8daa3
/
50.0a1
/
20160717030211
/
files
nightly linux64
711963e8daa3
/
50.0a1
/
20160717030211
/
files
nightly mac
711963e8daa3
/
50.0a1
/
20160717030211
/
files
nightly win32
711963e8daa3
/
50.0a1
/
20160717030211
/
files
nightly win64
711963e8daa3
/
50.0a1
/
20160717030211
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
50.0a1
/
20160717030211
/
pushlog to previous
nightly linux64
50.0a1
/
20160717030211
/
pushlog to previous
nightly mac
50.0a1
/
20160717030211
/
pushlog to previous
nightly win32
50.0a1
/
20160717030211
/
pushlog to previous
nightly win64
50.0a1
/
20160717030211
/
pushlog to previous
|
--- a/CLOBBER +++ b/CLOBBER @@ -17,9 +17,9 @@ # # Modifying this file will now automatically clobber the buildbot machines \o/ # # Are you updating CLOBBER because you think it's needed for your WebIDL # changes to stick? As of bug 928195, this shouldn't be necessary! Please # don't change CLOBBER for WebIDL changes any more. -Bug 1267887 - Build skew after updating rust mp4parse +Bug 1286754 - Update the version of rust used on Windows
--- a/accessible/xpcom/xpcAccessibleGeneric.h +++ b/accessible/xpcom/xpcAccessibleGeneric.h @@ -81,20 +81,20 @@ xpcAccessible::Intl() } inline AccessibleOrProxy xpcAccessible::IntlGeneric() { return static_cast<xpcAccessibleGeneric*>(this)->mIntl; } -inline Accessible* +inline AccessibleOrProxy xpcAccessibleHyperLink::Intl() { - return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible(); + return static_cast<xpcAccessibleGeneric*>(this)->mIntl; } inline Accessible* xpcAccessibleSelectable::Intl() { return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible(); }
--- a/accessible/xpcom/xpcAccessibleHyperLink.cpp +++ b/accessible/xpcom/xpcAccessibleHyperLink.cpp @@ -1,94 +1,156 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Accessible-inl.h" #include "xpcAccessibleDocument.h" +#include "nsNetUtil.h" using namespace mozilla::a11y; NS_IMETHODIMP xpcAccessibleHyperLink::GetStartIndex(int32_t* aStartIndex) { NS_ENSURE_ARG_POINTER(aStartIndex); *aStartIndex = 0; - if (!Intl()) + if (Intl().IsNull()) return NS_ERROR_FAILURE; - *aStartIndex = Intl()->StartOffset(); + if (Intl().IsAccessible()) { + *aStartIndex = Intl().AsAccessible()->StartOffset(); + } else { + bool isIndexValid = false; + uint32_t startOffset = Intl().AsProxy()->StartOffset(&isIndexValid); + if (!isIndexValid) + return NS_ERROR_FAILURE; + + *aStartIndex = startOffset; + } + return NS_OK; } NS_IMETHODIMP xpcAccessibleHyperLink::GetEndIndex(int32_t* aEndIndex) { NS_ENSURE_ARG_POINTER(aEndIndex); *aEndIndex = 0; - if (!Intl()) + if (Intl().IsNull()) return NS_ERROR_FAILURE; - *aEndIndex = Intl()->EndOffset(); + if (Intl().IsAccessible()) { + *aEndIndex = Intl().AsAccessible()->EndOffset(); + } else { + bool isIndexValid = false; + uint32_t endOffset = Intl().AsProxy()->EndOffset(&isIndexValid); + if (!isIndexValid) + return NS_ERROR_FAILURE; + + *aEndIndex = endOffset; + } + return NS_OK; } NS_IMETHODIMP xpcAccessibleHyperLink::GetAnchorCount(int32_t* aAnchorCount) { NS_ENSURE_ARG_POINTER(aAnchorCount); *aAnchorCount = 0; - if (!Intl()) + if (Intl().IsNull()) return NS_ERROR_FAILURE; - *aAnchorCount = Intl()->AnchorCount(); + if (Intl().IsAccessible()) { + *aAnchorCount = Intl().AsAccessible()->AnchorCount(); + } else { + bool isCountValid = false; + uint32_t anchorCount = Intl().AsProxy()->AnchorCount(&isCountValid); + if (!isCountValid) + return NS_ERROR_FAILURE; + + *aAnchorCount = anchorCount; + } + return NS_OK; } NS_IMETHODIMP xpcAccessibleHyperLink::GetURI(int32_t aIndex, nsIURI** aURI) { NS_ENSURE_ARG_POINTER(aURI); - if (!Intl()) + if (Intl().IsNull()) return NS_ERROR_FAILURE; - if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount())) + if (aIndex < 0) return NS_ERROR_INVALID_ARG; - RefPtr<nsIURI>(Intl()->AnchorURIAt(aIndex)).forget(aURI); + if (Intl().IsAccessible()) { + if (aIndex >= static_cast<int32_t>(Intl().AsAccessible()->AnchorCount())) + return NS_ERROR_INVALID_ARG; + + RefPtr<nsIURI>(Intl().AsAccessible()->AnchorURIAt(aIndex)).forget(aURI); + } else { + nsCString spec; + bool isURIValid = false; + Intl().AsProxy()->AnchorURIAt(aIndex, spec, &isURIValid); + if (!isURIValid) + return NS_ERROR_FAILURE; + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), spec); + NS_ENSURE_SUCCESS(rv, rv); + + uri.forget(aURI); + } + return NS_OK; } NS_IMETHODIMP xpcAccessibleHyperLink::GetAnchor(int32_t aIndex, nsIAccessible** aAccessible) { NS_ENSURE_ARG_POINTER(aAccessible); *aAccessible = nullptr; - if (!Intl()) + if (Intl().IsNull()) return NS_ERROR_FAILURE; - - if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount())) + + if (aIndex < 0) return NS_ERROR_INVALID_ARG; - NS_IF_ADDREF(*aAccessible = ToXPC(Intl()->AnchorAt(aIndex))); + if (Intl().IsAccessible()) { + if (aIndex >= static_cast<int32_t>(Intl().AsAccessible()->AnchorCount())) + return NS_ERROR_INVALID_ARG; + + NS_IF_ADDREF(*aAccessible = ToXPC(Intl().AsAccessible()->AnchorAt(aIndex))); + } else { + NS_IF_ADDREF(*aAccessible = ToXPC(Intl().AsProxy()->AnchorAt(aIndex))); + } + return NS_OK; } NS_IMETHODIMP xpcAccessibleHyperLink::GetValid(bool* aValid) { NS_ENSURE_ARG_POINTER(aValid); *aValid = false; - if (!Intl()) + if (Intl().IsNull()) return NS_ERROR_FAILURE; - *aValid = Intl()->IsLinkValid(); + if (Intl().IsAccessible()) { + *aValid = Intl().AsAccessible()->IsLinkValid(); + } else { + *aValid = Intl().AsProxy()->IsLinkValid(); + } + return NS_OK; }
--- a/accessible/xpcom/xpcAccessibleHyperLink.h +++ b/accessible/xpcom/xpcAccessibleHyperLink.h @@ -34,15 +34,15 @@ public: protected: xpcAccessibleHyperLink() { } virtual ~xpcAccessibleHyperLink() {} private: xpcAccessibleHyperLink(const xpcAccessibleHyperLink&) = delete; xpcAccessibleHyperLink& operator =(const xpcAccessibleHyperLink&) = delete; - Accessible* Intl(); + AccessibleOrProxy Intl(); }; } // namespace a11y } // namespace mozilla #endif
--- a/addon-sdk/moz.build +++ b/addon-sdk/moz.build @@ -8,17 +8,18 @@ # 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/. # Makefile.in uses a misc target through test_addons_TARGET. HAS_MISC_RULE = True BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] -JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini'] +JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini', + 'source/test/leak/jetpack-package.ini'] JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini'] addons = [ 'addon-manager', 'author-email', 'child_process', 'chrome', 'content-permissions',
--- a/addon-sdk/source/lib/sdk/event/utils.js +++ b/addon-sdk/source/lib/sdk/event/utils.js @@ -3,16 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { "stability": "unstable" }; var { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core"); +const { Cu } = require("chrome"); // This module provides set of high order function for working with event // streams (streams in a NodeJS style that dispatch data, end and error // events). // Function takes a `target` object and returns set of implicit references // (non property references) it keeps. This basically allows defining // references between objects without storing the explicitly. See transform for @@ -21,17 +22,17 @@ var refs = (function() { let refSets = new WeakMap(); return function refs(target) { if (!refSets.has(target)) refSets.set(target, new Set()); return refSets.get(target); }; })(); function transform(input, f) { - let output = {}; + let output = new Output(); // Since event listeners don't prevent `input` to be GC-ed we wanna presrve // it until `output` can be GC-ed. There for we add implicit reference which // is removed once `input` ends. refs(output).add(input); const next = data => receive(output, data); once(output, "start", () => start(input)); @@ -59,17 +60,17 @@ exports.filter = filter; // mapped via given `f` function. const map = (input, f) => transform(input, (data, next) => next(f(data))); exports.map = map; // High order function that takes `input` stream of streams and merges them // into single event stream. Like flatten but time based rather than order // based. function merge(inputs) { - let output = {}; + let output = new Output(); let open = 1; let state = []; output.state = state; refs(output).add(inputs); function end(input) { open = open - 1; refs(output).delete(input); @@ -102,23 +103,28 @@ exports.merge = merge; const expand = (inputs, f) => merge(map(inputs, f)); exports.expand = expand; const pipe = (from, to) => on(from, "*", emit.bind(emit, to)); exports.pipe = pipe; // Shim signal APIs so other modules can be used as is. - const receive = (input, message) => { if (input[receive]) input[receive](input, message); else emit(input, "data", message); + // Ideally our input will extend Input and already provide a weak value + // getter. If not, opportunistically shim the weak value getter on + // other types passed as the input. + if (!("value" in input)) { + Object.defineProperty(input, "value", WeakValueGetterSetter); + } input.value = message; }; receive.toString = () => "@@receive"; exports.receive = receive; exports.send = receive; const end = input => { if (input[end]) @@ -146,17 +152,17 @@ const start = input => { }; start.toString = () => "@@start"; exports.start = start; const lift = (step, ...inputs) => { let args = null; let opened = inputs.length; let started = false; - const output = {}; + const output = new Output(); const init = () => { args = [...inputs.map(input => input.value)]; output.value = step(...args); }; inputs.forEach((input, index) => { on(input, "data", data => { args[index] = data; @@ -177,17 +183,18 @@ const lift = (step, ...inputs) => { init(); return output; }; exports.lift = lift; const merges = inputs => { let opened = inputs.length; - let output = { value: inputs[0].value }; + let output = new Output(); + output.value = inputs[0].value; inputs.forEach((input, index) => { on(input, "data", data => receive(output, data)); on(input, "end", () => { opened = opened - 1; if (opened <= 0) end(output); }); }); @@ -220,22 +227,56 @@ Input.start = input => emit(input, "star Input.prototype.start = Input.start; Input.end = input => { emit(input, "end", input); stop(input); }; Input.prototype[end] = Input.end; +// The event channel system caches the last event seen as input.value. +// Unfortunately, if the last event is a DOM object this is a great way +// leak windows. Mitigate this by storing input.value using a weak +// reference. This allows the system to work for normal event processing +// while also allowing the objects to be reclaimed. It means, however, +// input.value cannot be accessed long after the event was dispatched. +const WeakValueGetterSetter = { + get: function() { + return this._weakValue ? this._weakValue.get() : this._simpleValue + }, + set: function(v) { + if (v && typeof v === "object") { + this._weakValue = Cu.getWeakReference(v) + this._simpleValue = undefined; + return; + } + this._simpleValue = v; + this._weakValue = undefined; + }, +} +Object.defineProperty(Input.prototype, "value", WeakValueGetterSetter); + exports.Input = Input; +// Define an Output type with a weak value getter for the transformation +// functions that produce new channels. +function Output() { } +Object.defineProperty(Output.prototype, "value", WeakValueGetterSetter); +exports.Output = Output; + const $source = "@@source"; const $outputs = "@@outputs"; exports.outputs = $outputs; +// NOTE: Passing DOM objects through a Reactor can cause them to leak +// when they get cached in this.value. We cannot use a weak reference +// in this case because the Reactor design expects to always have both the +// past and present value. If we allow past values to be collected the +// system breaks. + function Reactor(options={}) { const {onStep, onStart, onEnd} = options; if (onStep) this.onStep = onStep; if (onStart) this.onStart = onStart; if (onEnd) this.onEnd = onEnd;
--- a/addon-sdk/source/lib/sdk/window/events.js +++ b/addon-sdk/source/lib/sdk/window/events.js @@ -2,32 +2,50 @@ * 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/. */ "use strict"; module.metadata = { "stability": "unstable" }; -const { Ci } = require("chrome"); +const { Ci, Cu } = require("chrome"); const { observe } = require("../event/chrome"); const { open } = require("../event/dom"); const { windows } = require("../window/utils"); const { filter, merge, map, expand } = require("../event/utils"); +function documentMatches(weakWindow, event) { + let window = weakWindow.get(); + return window && event.target === window.document; +} + +function makeStrictDocumentFilter(window) { + // Note: Do not define a closure within this function. Otherwise + // you may leak the window argument. + let weak = Cu.getWeakReference(window); + return documentMatches.bind(null, weak); +} + +function toEventWithDefaultViewTarget({type, target}) { + return { type: type, target: target.defaultView } +} + // Function registers single shot event listeners for relevant window events // that forward events to exported event stream. function eventsFor(window) { + // NOTE: Do no use pass a closure from this function into a stream + // transform function. You will capture the window in the + // closure and leak the window until the event stream is + // completely closed. let interactive = open(window, "DOMContentLoaded", { capture: true }); let complete = open(window, "load", { capture: true }); let states = merge([interactive, complete]); - let changes = filter(states, ({target}) => target === window.document); - return map(changes, function({type, target}) { - return { type: type, target: target.defaultView } - }); + let changes = filter(states, makeStrictDocumentFilter(window)); + return map(changes, toEventWithDefaultViewTarget); } // In addition to observing windows that are open we also observe windows // that are already already opened in case they're in process of loading. var opened = windows(null, { includePrivate: true }); var currentEvents = merge(opened.map(eventsFor)); // Register system event listeners for top level window open / close.
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/leak/jetpack-package.ini @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = + leak-utils.js + +[test-leak-window-events.js] +[test-leak-event-dom-closed-window.js]
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/leak/leak-utils.js @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Cu, Ci } = require("chrome"); +const { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); +const { SelfSupportBackend } = Cu.import("resource:///modules/SelfSupportBackend.jsm", {}); +const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports; + +// Adapted from the SpecialPowers.exactGC() code. We don't have a +// window to operate on so we cannot use the exact same logic. We +// use 6 GC iterations here as that is what is needed to clean up +// the windows we have tested with. +function gc() { + return new Promise(resolve => { + Cu.forceGC(); + Cu.forceCC(); + let count = 0; + function genGCCallback() { + Cu.forceCC(); + return function() { + if (++count < 5) { + Cu.schedulePreciseGC(genGCCallback()); + } else { + resolve(); + } + } + } + + Cu.schedulePreciseGC(genGCCallback()); + }); +} + +// Execute the given test function and verify that we did not leak windows +// in the process. The test function must return a promise or be a generator. +// If the promise is resolved, or generator completes, with an sdk loader +// object then it will be unloaded after the memory measurements. +exports.asyncWindowLeakTest = function*(assert, asyncTestFunc) { + + // SelfSupportBackend periodically tries to open windows. This can + // mess up our window leak detection below, so turn it off. + SelfSupportBackend.uninit(); + + // Wait for the browser to finish loading. + yield Startup.onceInitialized; + + // Track windows that are opened in an array of weak references. + let weakWindows = []; + function windowObserver(subject, topic) { + let supportsWeak = subject.QueryInterface(Ci.nsISupportsWeakReference); + if (supportsWeak) { + weakWindows.push(Cu.getWeakReference(supportsWeak)); + } + } + Services.obs.addObserver(windowObserver, "domwindowopened", false); + + // Execute the body of the test. + let testLoader = yield asyncTestFunc(assert); + + // Stop tracking new windows and attempt to GC any resources allocated + // by the test body. + Services.obs.removeObserver(windowObserver, "domwindowopened", false); + yield gc(); + + // Check to see if any of the windows we saw survived the GC. We consider + // these leaks. + assert.ok(weakWindows.length > 0, "should see at least one new window"); + for (let i = 0; i < weakWindows.length; ++i) { + assert.equal(weakWindows[i].get(), null, "window " + i + " should be GC'd"); + } + + // Finally, unload the test body's loader if it provided one. We do this + // after our leak detection to avoid free'ing things on unload. Users + // don't tend to unload their addons very often, so we want to find leaks + // that happen while addons are in use. + if (testLoader) { + testLoader.unload(); + } +}
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js @@ -0,0 +1,29 @@ +/* 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/. */ +'use strict'; + +const { asyncWindowLeakTest } = require("./leak-utils"); +const { Loader } = require('sdk/test/loader'); +const openWindow = require("sdk/window/utils").open; + +exports["test sdk/event/dom does not leak when attached to closed window"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise(resolve => { + let loader = Loader(module); + let { open } = loader.require('sdk/event/dom'); + let w = openWindow(); + w.addEventListener("DOMWindowClose", function windowClosed(evt) { + w.removeEventListener("DOMWindowClose", windowClosed); + // The sdk/event/dom module tries to clean itself up when DOMWindowClose + // is fired. Verify that it doesn't leak if its attached to an + // already closed window either. (See bug 1268898.) + open(w.document, "TestEvent1"); + resolve(loader); + }); + w.close(); + }); + }); +} + +require("sdk/test").run(exports);
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/leak/test-leak-window-events.js @@ -0,0 +1,47 @@ +/* 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/. */ +"use strict"; + +// Opening new windows in Fennec causes issues +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + +const { asyncWindowLeakTest } = require("./leak-utils.js"); +const { Loader } = require("sdk/test/loader"); +const { open } = require("sdk/window/utils"); + +exports["test window/events for leaks"] = function*(assert) { + yield asyncWindowLeakTest(assert, _ => { + return new Promise((resolve, reject) => { + let loader = Loader(module); + let { events } = loader.require("sdk/window/events"); + let { on, off } = loader.require("sdk/event/core"); + + on(events, "data", function handler(e) { + try { + if (e.type === "load") { + e.target.close(); + } + else if (e.type === "close") { + off(events, "data", handler); + + // Let asyncWindowLeakTest call loader.unload() after the + // leak check. + resolve(loader); + } + } catch (e) { + reject(e); + } + }); + + // Open a window. This will trigger our data events. + open(); + }); + }); +}; + +require("sdk/test").run(exports);
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -1891,18 +1891,20 @@ BrowserGlue.prototype = { var notification = notifyBox.appendNotification(text, title, null, notifyBox.PRIORITY_CRITICAL_MEDIUM, buttons); notification.persistence = -1; // Until user closes it }, _showSyncStartedDoorhanger: function () { let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties"); + let productName = gBrandBundle.GetStringFromName("brandShortName"); let title = bundle.GetStringFromName("syncStartNotification.title"); - let body = bundle.GetStringFromName("syncStartNotification.body"); + let body = bundle.formatStringFromName("syncStartNotification.body2", + [productName], 1); let clickCallback = (subject, topic, data) => { if (topic != "alertclickcallback") return; this._openPreferences("sync"); } AlertsService.showAlertNotification(null, title, body, true, null, clickCallback); },
--- a/browser/config/tooltool-manifests/win32/releng.manifest +++ b/browser/config/tooltool-manifests/win32/releng.manifest @@ -1,19 +1,19 @@ [ { "size": 266240, "digest": "bb345b0e700ffab4d09436981f14b5de84da55a3f18a7f09ebc4364a4488acdeab8d46f447b12ac70f2da1444a68b8ce8b8675f0dae2ccf845e966d1df0f0869", "algorithm": "sha512", "filename": "mozmake.exe" }, { -"version": "rustc 1.9.0 (e4e8b6668 2016-05-18)", -"size": 82463178, -"digest": "a3c54c6792e75d53ec79caf958db25b651fcf968a37b00949fb327c54a54cad6305a4af302f267082d86d70fcf837ed0f273f85b97706c20b957ff3690889b40", +"version": "rustc 1.10.0 (cfcb716cf 2016-07-03)", +"size": 88820579, +"digest": "3bc772d951bf90b01cdba9dcd0e1d131a98519dff0710bb219784ea43d4d001dbce191071a4b3824933386bb9613f173760c438939eb396b0e0dfdad9a42e4f0", "algorithm": "sha512", "filename": "rustc.tar.bz2", "unpack": true }, { "size": 167175, "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831", "algorithm": "sha512",
--- a/browser/config/tooltool-manifests/win64/releng.manifest +++ b/browser/config/tooltool-manifests/win64/releng.manifest @@ -1,19 +1,19 @@ [ { "size": 266240, "digest": "bb345b0e700ffab4d09436981f14b5de84da55a3f18a7f09ebc4364a4488acdeab8d46f447b12ac70f2da1444a68b8ce8b8675f0dae2ccf845e966d1df0f0869", "algorithm": "sha512", "filename": "mozmake.exe" }, { -"version": "rustc 1.9.0 (e4e8b6668 2016-05-18)", -"size": 88486080, -"digest": "a4fb99cd637b236a9c30e111757ca560bc8df1b143324c1d9ab58c32470b9b9a0598e3e0d220278ee157959dcd88421496388e2ed856e6261d9c81f18e6310e9", +"version": "rustc 1.10.0 (cfcb716cf 2016-07-03)", +"size": 94067220, +"digest": "05cabda2a28ce6674f062aab589b4b3758e0cd4a4af364bb9a2e736254baa10d668936b2b7ed0df530c7f5ba8ea1e7f51ff3affc84a6551c46188b2f67f10e05", "algorithm": "sha512", "visibility": "public", "filename": "rustc.tar.bz2", "unpack": true }, { "size": 167175, "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
--- a/browser/locales/en-US/chrome/browser/accounts.properties +++ b/browser/locales/en-US/chrome/browser/accounts.properties @@ -22,17 +22,18 @@ verificationSentTitle = Verification Sen # LOCALIZATION NOTE (verificationSentBody) - %S = Email address of user's Firefox Account verificationSentBody = A verification link has been sent to %S. verificationNotSentTitle = Unable to Send Verification verificationNotSentBody = We are unable to send a verification mail at this time, please try again later. # LOCALIZATION NOTE (syncStartNotification.title, syncStartNotification.body) # These strings are used in a notification shown after Sync is connected. syncStartNotification.title = Sync enabled -syncStartNotification.body = Firefox will begin syncing momentarily. +# %S is brandShortName +syncStartNotification.body2 = %S will begin syncing momentarily. # LOCALIZATION NOTE (deviceDisconnectedNotification.title, deviceDisconnectedNotification.body) # These strings are used in a notification shown after Sync was disconnected remotely. deviceDisconnectedNotification.title = Sync disconnected deviceDisconnectedNotification.body = This computer has been successfully disconnected from Firefox Sync. # LOCALIZATION NOTE (sendTabToAllDevices.menuitem) # Displayed in the Send Tabs context menu when right clicking a tab, a page or a link.
--- a/build/moz.configure/util.configure +++ b/build/moz.configure/util.configure @@ -132,24 +132,31 @@ def namespace(**kwargs): # such as `set_config`. But those functions do not take immediate values. # The `delayed_getattr` function allows access to attributes from the result # of a @depends function in a non-immediate manner. # @depends('--option') # def option(value) # return namespace(foo=value) # set_config('FOO', delayed_getattr(option, 'foo') @template +@imports('__sandbox__') def delayed_getattr(func, key): - @depends(func) - def result(value): + _, deps = __sandbox__._depends.get(func, (None, ())) + + def result(value, _=None): # The @depends function we're being passed may have returned # None, or an object that simply doesn't have the wanted key. # In that case, just return None. return getattr(value, key, None) - return result + + # Automatically add a dependency on --help when the given @depends + # function itself depends on --help. + if __sandbox__._help_option in deps: + return depends(func, '--help')(result) + return depends(func)(result) # Like @depends, but the decorated function is only called if one of the # arguments it would be called with has a positive value (bool(value) is True) @template def depends_if(*args): def decorator(func): @depends(*args)
--- a/config/check_spidermonkey_style.py +++ b/config/check_spidermonkey_style.py @@ -66,16 +66,18 @@ included_inclnames_to_ignore = set([ 'jsautokw.h', # generated in $OBJDIR 'jscustomallocator.h', # provided by embedders; allowed to be missing 'js-config.h', # generated in $OBJDIR 'fdlibm.h', # fdlibm 'pratom.h', # NSPR 'prcvar.h', # NSPR 'prerror.h', # NSPR 'prinit.h', # NSPR + 'prio.h', # NSPR + 'private/pprio.h', # NSPR 'prlink.h', # NSPR 'prlock.h', # NSPR 'prprf.h', # NSPR 'prthread.h', # NSPR 'prtypes.h', # NSPR 'selfhosted.out.h', # generated in $OBJDIR 'shellmoduleloader.out.h', # generated in $OBJDIR 'unicode/locid.h', # ICU
--- a/devtools/client/responsivedesign/test/browser.ini +++ b/devtools/client/responsivedesign/test/browser.ini @@ -13,8 +13,9 @@ skip-if = e10s && debug # Bug 1252201 - [browser_responsiveruleview.js] skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s [browser_responsiveui.js] [browser_responsiveui_touch.js] [browser_responsiveuiaddcustompreset.js] [browser_responsive_devicewidth.js] [browser_responsiveui_customuseragent.js] [browser_responsiveui_window_close.js] +skip-if = (os == 'linux') && e10s && debug # Bug 1277274
--- a/devtools/server/actors/script.js +++ b/devtools/server/actors/script.js @@ -14,17 +14,16 @@ const { EnvironmentActor } = require("de const { FrameActor } = require("devtools/server/actors/frame"); const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object"); const { SourceActor, getSourceURL } = require("devtools/server/actors/source"); const { DebuggerServer } = require("devtools/server/main"); const { ActorClassWithSpec } = require("devtools/shared/protocol"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { assert, dumpn, update, fetch } = DevToolsUtils; const promise = require("promise"); -const PromiseDebugging = require("PromiseDebugging"); const xpcInspector = require("xpcInspector"); const ScriptStore = require("./utils/ScriptStore"); const { DevToolsWorker } = require("devtools/shared/worker/worker"); const object = require("sdk/util/object"); const { threadSpec } = require("devtools/shared/specs/script"); const { defer, resolve, reject, all } = promise;
--- a/devtools/shared/worker/loader.js +++ b/devtools/shared/worker/loader.js @@ -312,22 +312,16 @@ function WorkerDebuggerLoader(options) { } this.WorkerDebuggerLoader = WorkerDebuggerLoader; // The following APIs rely on the use of Components, and the worker debugger // does not provide alternative definitions for them. Consequently, they are // stubbed out both on the main thread and worker threads. -var PromiseDebugging = { - getState: function () { - throw new Error("PromiseDebugging is not available in workers!"); - } -}; - var chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined, Cm: undefined, Ci: undefined, Cu: undefined, Cr: undefined, @@ -491,17 +485,16 @@ this.worker = new WorkerDebuggerLoader({ "reportError": reportError, "rpc": rpc, "setImmediate": setImmediate, "URL": URL, }, loadSubScript: loadSubScript, modules: { "Debugger": Debugger, - "PromiseDebugging": PromiseDebugging, "Services": Object.create(null), "chrome": chrome, "xpcInspector": xpcInspector }, paths: { // âš DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING âš "": "resource://gre/modules/commonjs/", // âš DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING âš
--- a/docshell/test/browser/browser_timelineMarkers-frame-04.js +++ b/docshell/test/browser/browser_timelineMarkers-frame-04.js @@ -45,17 +45,21 @@ if (Services.prefs.getBoolPref("javascri markers = markers.filter(m => (m.name == "Javascript" && m.causeName == "promise callback")); ok(markers.length > 0, "Found a Javascript marker"); let frame = markers[0].stack; ok(frame.asyncParent !== null, "Parent frame has async parent"); is(frame.asyncParent.asyncCause, "promise callback", "Async parent has correct cause"); - is(frame.asyncParent.functionDisplayName, "do_promise", + let asyncFrame = frame.asyncParent; + // Skip over self-hosted parts of our Promise implementation. + while (asyncFrame.source === 'self-hosted') + asyncFrame = asyncFrame.parent; + is(asyncFrame.functionDisplayName, "do_promise", "Async parent has correct function name"); } }, { desc: "Async stack trace on Javascript marker with script", searchFor: (markers) => { return markers.some(m => (m.name == "Javascript" && m.causeName == "promise callback")); }, @@ -66,15 +70,19 @@ if (Services.prefs.getBoolPref("javascri markers = markers.filter(m => (m.name == "Javascript" && m.causeName == "promise callback")); ok(markers.length > 0, "Found a Javascript marker"); let frame = markers[0].stack; ok(frame.asyncParent !== null, "Parent frame has async parent"); is(frame.asyncParent.asyncCause, "promise callback", "Async parent has correct cause"); - is(frame.asyncParent.functionDisplayName, "do_promise_script", + let asyncFrame = frame.asyncParent; + // Skip over self-hosted parts of our Promise implementation. + while (asyncFrame.source === 'self-hosted') + asyncFrame = asyncFrame.parent; + is(asyncFrame.functionDisplayName, "do_promise_script", "Async parent has correct function name"); } }); } timelineContentTest(TESTS);
--- a/docshell/test/browser/browser_timelineMarkers-frame-05.js +++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js @@ -86,20 +86,32 @@ if (Services.prefs.getBoolPref("javascri searchFor: "ConsoleTime", setup: function(docShell) { let resolver = makePromise(); resolvePromise(resolver); }, check: function(markers) { markers = markers.filter(m => m.name == "ConsoleTime"); ok(markers.length > 0, "Promise marker includes stack"); - + ok(markers[0].stack.functionDisplayName == "testConsoleTime", + "testConsoleTime is on the stack"); let frame = markers[0].endStack; - ok(frame.parent.asyncParent !== null, "Parent frame has async parent"); - is(frame.parent.asyncParent.asyncCause, "promise callback", + ok(frame.functionDisplayName == "testConsoleTimeEnd", + "testConsoleTimeEnd is on the stack"); + + frame = frame.parent; + ok(frame.functionDisplayName == "makePromise/<", + "makePromise/< is on the stack"); + let asyncFrame = frame.asyncParent; + ok(asyncFrame !== null, "Frame has async parent"); + is(asyncFrame.asyncCause, "promise callback", "Async parent has correct cause"); - is(frame.parent.asyncParent.functionDisplayName, "makePromise", + // Skip over self-hosted parts of our Promise implementation. + while (asyncFrame.source === 'self-hosted') { + asyncFrame = asyncFrame.parent; + } + is(asyncFrame.functionDisplayName, "makePromise", "Async parent has correct function name"); } }); } timelineContentTest(TESTS);
--- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -1454,19 +1454,16 @@ nsIDocument::nsIDocument() mGetUserFontSetCalled(false), mPostedFlushUserFontSet(false), mPartID(0), mDidFireDOMContentLoaded(true), mHasScrollLinkedEffect(false), mUserHasInteracted(false) { SetIsDocument(); - if (IsStyledByServo()) { - SetFlags(NODE_IS_DIRTY_FOR_SERVO | NODE_HAS_DIRTY_DESCENDANTS_FOR_SERVO); - } PR_INIT_CLIST(&mDOMMediaQueryLists); } // NOTE! nsDocument::operator new() zeroes out all members, so don't // bother initializing members to 0. nsDocument::nsDocument(const char* aContentType)
--- a/dom/bindings/BindingUtils.cpp +++ b/dom/bindings/BindingUtils.cpp @@ -138,53 +138,64 @@ ThrowNoSetterArg(JSContext* aCx, prototy { nsPrintfCString errorMessage("%s attribute setter", NamesOfInterfacesWithProtos(aProtoId)); return ThrowErrorMessage(aCx, MSG_MISSING_ARGUMENTS, errorMessage.get()); } } // namespace dom -struct ErrorResult::Message { - Message() { MOZ_COUNT_CTOR(ErrorResult::Message); } - ~Message() { MOZ_COUNT_DTOR(ErrorResult::Message); } +namespace binding_danger { + +template<typename CleanupPolicy> +struct TErrorResult<CleanupPolicy>::Message { + Message() { MOZ_COUNT_CTOR(TErrorResult::Message); } + ~Message() { MOZ_COUNT_DTOR(TErrorResult::Message); } nsTArray<nsString> mArgs; dom::ErrNum mErrorNumber; bool HasCorrectNumberOfArguments() { return GetErrorArgCount(mErrorNumber) == mArgs.Length(); } }; +template<typename CleanupPolicy> nsTArray<nsString>& -ErrorResult::CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType) +TErrorResult<CleanupPolicy>::CreateErrorMessageHelper(const dom::ErrNum errorNumber, + nsresult errorType) { + AssertInOwningThread(); mResult = errorType; mMessage = new Message(); mMessage->mErrorNumber = errorNumber; return mMessage->mArgs; } +template<typename CleanupPolicy> void -ErrorResult::SerializeMessage(IPC::Message* aMsg) const +TErrorResult<CleanupPolicy>::SerializeMessage(IPC::Message* aMsg) const { using namespace IPC; + AssertInOwningThread(); MOZ_ASSERT(mUnionState == HasMessage); MOZ_ASSERT(mMessage); WriteParam(aMsg, mMessage->mArgs); WriteParam(aMsg, mMessage->mErrorNumber); } +template<typename CleanupPolicy> bool -ErrorResult::DeserializeMessage(const IPC::Message* aMsg, PickleIterator* aIter) +TErrorResult<CleanupPolicy>::DeserializeMessage(const IPC::Message* aMsg, + PickleIterator* aIter) { using namespace IPC; + AssertInOwningThread(); nsAutoPtr<Message> readMessage(new Message()); if (!ReadParam(aMsg, aIter, &readMessage->mArgs) || !ReadParam(aMsg, aIter, &readMessage->mErrorNumber)) { return false; } if (!readMessage->HasCorrectNumberOfArguments()) { return false; } @@ -192,19 +203,21 @@ ErrorResult::DeserializeMessage(const IP MOZ_ASSERT(mUnionState == HasNothing); mMessage = readMessage.forget(); #ifdef DEBUG mUnionState = HasMessage; #endif // DEBUG return true; } +template<typename CleanupPolicy> void -ErrorResult::SetPendingExceptionWithMessage(JSContext* aCx) +TErrorResult<CleanupPolicy>::SetPendingExceptionWithMessage(JSContext* aCx) { + AssertInOwningThread(); MOZ_ASSERT(mMessage, "SetPendingExceptionWithMessage() can be called only once"); MOZ_ASSERT(mUnionState == HasMessage); Message* message = mMessage; MOZ_RELEASE_ASSERT(message->HasCorrectNumberOfArguments()); const uint32_t argCount = message->mArgs.Length(); const char16_t* args[JS::MaxNumErrorArguments + 1]; for (uint32_t i = 0; i < argCount; ++i) { @@ -215,55 +228,61 @@ ErrorResult::SetPendingExceptionWithMess JS_ReportErrorNumberUCArray(aCx, dom::GetErrorMessage, nullptr, static_cast<const unsigned>(message->mErrorNumber), argCount > 0 ? args : nullptr); ClearMessage(); mResult = NS_OK; } +template<typename CleanupPolicy> void -ErrorResult::ClearMessage() +TErrorResult<CleanupPolicy>::ClearMessage() { + AssertInOwningThread(); MOZ_ASSERT(IsErrorWithMessage()); delete mMessage; mMessage = nullptr; #ifdef DEBUG mUnionState = HasNothing; #endif // DEBUG } +template<typename CleanupPolicy> void -ErrorResult::ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn) +TErrorResult<CleanupPolicy>::ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn) { + AssertInOwningThread(); MOZ_ASSERT(mMightHaveUnreportedJSException, "Why didn't you tell us you planned to throw a JS exception?"); ClearUnionData(); // Make sure mJSException is initialized _before_ we try to root it. But // don't set it to exn yet, because we don't want to do that until after we // root. mJSException.setUndefined(); - if (!js::AddRawValueRoot(cx, &mJSException, "ErrorResult::mJSException")) { + if (!js::AddRawValueRoot(cx, &mJSException, "TErrorResult::mJSException")) { // Don't use NS_ERROR_DOM_JS_EXCEPTION, because that indicates we have // in fact rooted mJSException. mResult = NS_ERROR_OUT_OF_MEMORY; } else { mJSException = exn; mResult = NS_ERROR_DOM_JS_EXCEPTION; #ifdef DEBUG mUnionState = HasJSException; #endif // DEBUG } } +template<typename CleanupPolicy> void -ErrorResult::SetPendingJSException(JSContext* cx) +TErrorResult<CleanupPolicy>::SetPendingJSException(JSContext* cx) { + AssertInOwningThread(); MOZ_ASSERT(!mMightHaveUnreportedJSException, "Why didn't you tell us you planned to handle JS exceptions?"); MOZ_ASSERT(mUnionState == HasJSException); JS::Rooted<JS::Value> exception(cx, mJSException); if (JS_WrapValue(cx, &exception)) { JS_SetPendingException(cx, exception); } @@ -273,140 +292,160 @@ ErrorResult::SetPendingJSException(JSCon js::RemoveRawValueRoot(cx, &mJSException); mResult = NS_OK; #ifdef DEBUG mUnionState = HasNothing; #endif // DEBUG } -struct ErrorResult::DOMExceptionInfo { +template<typename CleanupPolicy> +struct TErrorResult<CleanupPolicy>::DOMExceptionInfo { DOMExceptionInfo(nsresult rv, const nsACString& message) : mMessage(message) , mRv(rv) {} nsCString mMessage; nsresult mRv; }; +template<typename CleanupPolicy> void -ErrorResult::SerializeDOMExceptionInfo(IPC::Message* aMsg) const +TErrorResult<CleanupPolicy>::SerializeDOMExceptionInfo(IPC::Message* aMsg) const { using namespace IPC; + AssertInOwningThread(); MOZ_ASSERT(mDOMExceptionInfo); MOZ_ASSERT(mUnionState == HasDOMExceptionInfo); WriteParam(aMsg, mDOMExceptionInfo->mMessage); WriteParam(aMsg, mDOMExceptionInfo->mRv); } +template<typename CleanupPolicy> bool -ErrorResult::DeserializeDOMExceptionInfo(const IPC::Message* aMsg, PickleIterator* aIter) +TErrorResult<CleanupPolicy>::DeserializeDOMExceptionInfo(const IPC::Message* aMsg, + PickleIterator* aIter) { using namespace IPC; + AssertInOwningThread(); nsCString message; nsresult rv; if (!ReadParam(aMsg, aIter, &message) || !ReadParam(aMsg, aIter, &rv)) { return false; } MOZ_ASSERT(mUnionState == HasNothing); MOZ_ASSERT(IsDOMException()); mDOMExceptionInfo = new DOMExceptionInfo(rv, message); #ifdef DEBUG mUnionState = HasDOMExceptionInfo; #endif // DEBUG return true; } +template<typename CleanupPolicy> void -ErrorResult::ThrowDOMException(nsresult rv, const nsACString& message) +TErrorResult<CleanupPolicy>::ThrowDOMException(nsresult rv, + const nsACString& message) { + AssertInOwningThread(); ClearUnionData(); mResult = NS_ERROR_DOM_DOMEXCEPTION; mDOMExceptionInfo = new DOMExceptionInfo(rv, message); #ifdef DEBUG mUnionState = HasDOMExceptionInfo; #endif } +template<typename CleanupPolicy> void -ErrorResult::SetPendingDOMException(JSContext* cx) +TErrorResult<CleanupPolicy>::SetPendingDOMException(JSContext* cx) { + AssertInOwningThread(); MOZ_ASSERT(mDOMExceptionInfo, "SetPendingDOMException() can be called only once"); MOZ_ASSERT(mUnionState == HasDOMExceptionInfo); dom::Throw(cx, mDOMExceptionInfo->mRv, mDOMExceptionInfo->mMessage); ClearDOMExceptionInfo(); mResult = NS_OK; } +template<typename CleanupPolicy> void -ErrorResult::ClearDOMExceptionInfo() +TErrorResult<CleanupPolicy>::ClearDOMExceptionInfo() { + AssertInOwningThread(); MOZ_ASSERT(IsDOMException()); MOZ_ASSERT(mUnionState == HasDOMExceptionInfo || !mDOMExceptionInfo); delete mDOMExceptionInfo; mDOMExceptionInfo = nullptr; #ifdef DEBUG mUnionState = HasNothing; #endif // DEBUG } +template<typename CleanupPolicy> void -ErrorResult::ClearUnionData() +TErrorResult<CleanupPolicy>::ClearUnionData() { + AssertInOwningThread(); if (IsJSException()) { JSContext* cx = nsContentUtils::RootingCx(); MOZ_ASSERT(cx); mJSException.setUndefined(); js::RemoveRawValueRoot(cx, &mJSException); #ifdef DEBUG mUnionState = HasNothing; #endif // DEBUG } else if (IsErrorWithMessage()) { ClearMessage(); } else if (IsDOMException()) { ClearDOMExceptionInfo(); } } +template<typename CleanupPolicy> void -ErrorResult::SetPendingGenericErrorException(JSContext* cx) +TErrorResult<CleanupPolicy>::SetPendingGenericErrorException(JSContext* cx) { + AssertInOwningThread(); MOZ_ASSERT(!IsErrorWithMessage()); MOZ_ASSERT(!IsJSException()); MOZ_ASSERT(!IsDOMException()); dom::Throw(cx, ErrorCode()); mResult = NS_OK; } -ErrorResult& -ErrorResult::operator=(ErrorResult&& aRHS) +template<typename CleanupPolicy> +TErrorResult<CleanupPolicy>& +TErrorResult<CleanupPolicy>::operator=(TErrorResult<CleanupPolicy>&& aRHS) { + AssertInOwningThread(); + aRHS.AssertInOwningThread(); // Clear out any union members we may have right now, before we // start writing to it. ClearUnionData(); #ifdef DEBUG mMightHaveUnreportedJSException = aRHS.mMightHaveUnreportedJSException; aRHS.mMightHaveUnreportedJSException = false; #endif if (aRHS.IsErrorWithMessage()) { mMessage = aRHS.mMessage; aRHS.mMessage = nullptr; } else if (aRHS.IsJSException()) { JSContext* cx = nsContentUtils::RootingCx(); MOZ_ASSERT(cx); mJSException.setUndefined(); - if (!js::AddRawValueRoot(cx, &mJSException, "ErrorResult::mJSException")) { + if (!js::AddRawValueRoot(cx, &mJSException, "TErrorResult::mJSException")) { MOZ_CRASH("Could not root mJSException, we're about to OOM"); } mJSException = aRHS.mJSException; aRHS.mJSException.setUndefined(); js::RemoveRawValueRoot(cx, &aRHS.mJSException); } else if (aRHS.IsDOMException()) { mDOMExceptionInfo = aRHS.mDOMExceptionInfo; aRHS.mDOMExceptionInfo = nullptr; @@ -422,19 +461,23 @@ ErrorResult::operator=(ErrorResult&& aRH // Note: It's important to do this last, since this affects the condition // checks above! mResult = aRHS.mResult; aRHS.mResult = NS_OK; return *this; } +template<typename CleanupPolicy> void -ErrorResult::CloneTo(ErrorResult& aRv) const +TErrorResult<CleanupPolicy>::CloneTo(TErrorResult& aRv) const { + AssertInOwningThread(); + aRv.AssertInOwningThread(); + aRv.ClearUnionData(); aRv.mResult = mResult; #ifdef DEBUG aRv.mMightHaveUnreportedJSException = mMightHaveUnreportedJSException; #endif if (IsErrorWithMessage()) { #ifdef DEBUG @@ -454,29 +497,33 @@ ErrorResult::CloneTo(ErrorResult& aRv) c aRv.mUnionState = HasJSException; #endif JSContext* cx = nsContentUtils::RootingCx(); JS::Rooted<JS::Value> exception(cx, mJSException); aRv.ThrowJSException(cx, exception); } } +template<typename CleanupPolicy> void -ErrorResult::SuppressException() +TErrorResult<CleanupPolicy>::SuppressException() { + AssertInOwningThread(); WouldReportJSException(); ClearUnionData(); // We don't use AssignErrorCode, because we want to override existing error // states, which AssignErrorCode is not allowed to do. mResult = NS_OK; } +template<typename CleanupPolicy> void -ErrorResult::SetPendingException(JSContext* cx) +TErrorResult<CleanupPolicy>::SetPendingException(JSContext* cx) { + AssertInOwningThread(); if (IsUncatchableException()) { // Nuke any existing exception on cx, to make sure we're uncatchable. JS_ClearPendingException(cx); // Don't do any reporting. Just return, to create an // uncatchable exception. mResult = NS_OK; return; } @@ -496,42 +543,52 @@ ErrorResult::SetPendingException(JSConte } if (IsDOMException()) { SetPendingDOMException(cx); return; } SetPendingGenericErrorException(cx); } +template<typename CleanupPolicy> void -ErrorResult::StealExceptionFromJSContext(JSContext* cx) +TErrorResult<CleanupPolicy>::StealExceptionFromJSContext(JSContext* cx) { + AssertInOwningThread(); MOZ_ASSERT(mMightHaveUnreportedJSException, "Why didn't you tell us you planned to throw a JS exception?"); JS::Rooted<JS::Value> exn(cx); if (!JS_GetPendingException(cx, &exn)) { ThrowUncatchableException(); return; } ThrowJSException(cx, exn); JS_ClearPendingException(cx); } +template<typename CleanupPolicy> void -ErrorResult::NoteJSContextException(JSContext* aCx) +TErrorResult<CleanupPolicy>::NoteJSContextException(JSContext* aCx) { + AssertInOwningThread(); if (JS_IsExceptionPending(aCx)) { mResult = NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT; } else { mResult = NS_ERROR_UNCATCHABLE_EXCEPTION; } } +template class TErrorResult<JustAssertCleanupPolicy>; +template class TErrorResult<AssertAndSuppressCleanupPolicy>; +template class TErrorResult<JustSuppressCleanupPolicy>; + +} // namespace binding_danger + namespace dom { bool DefineConstants(JSContext* cx, JS::Handle<JSObject*> obj, const ConstantSpec* cs) { JS::Rooted<JS::Value> value(cx); for (; cs->name; ++cs) {
--- a/dom/bindings/BindingUtils.h +++ b/dom/bindings/BindingUtils.h @@ -101,17 +101,17 @@ IsNonProxyDOMClass(const js::Class* clas } inline bool IsNonProxyDOMClass(const JSClass* clasp) { return IsNonProxyDOMClass(js::Valueify(clasp)); } -// Returns true if the JSClass is used for DOM interface and interface +// Returns true if the JSClass is used for DOM interface and interface // prototype objects. inline bool IsDOMIfaceAndProtoClass(const JSClass* clasp) { return clasp->flags & JSCLASS_IS_DOMIFACEANDPROTOJSCLASS; } inline bool @@ -2006,16 +2006,22 @@ private: "Offset of mLength should match"); static_assert(offsetof(FakeString, mFlags) == offsetof(StringAsserter, mFlags), "Offset of mFlags should match"); } }; }; +class FastErrorResult : + public mozilla::binding_danger::TErrorResult< + mozilla::binding_danger::JustAssertCleanupPolicy> +{ +}; + } // namespace binding_detail enum StringificationBehavior { eStringify, eEmpty, eNull };
--- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -5207,32 +5207,32 @@ def getJSToNativeConversionInfo(type, de globalObj = js::GetGlobalForObjectCrossCompartment(unwrappedVal); """, sourceDescription=sourceDescription) else: getPromiseGlobal = "" templateBody = fill( """ - { // Scope for our GlobalObject, ErrorResult, JSAutoCompartment, + { // Scope for our GlobalObject, FastErrorResult, JSAutoCompartment, // etc. JS::Rooted<JSObject*> globalObj(cx, JS::CurrentGlobalOrNull(cx)); $*{getPromiseGlobal} JSAutoCompartment ac(cx, globalObj); GlobalObject promiseGlobal(cx, globalObj); if (promiseGlobal.Failed()) { $*{exceptionCode} } JS::Rooted<JS::Value> valueToResolve(cx, $${val}); if (!JS_WrapValue(cx, &valueToResolve)) { $*{exceptionCode} } - ErrorResult promiseRv; + binding_detail::FastErrorResult promiseRv; #ifdef SPIDERMONKEY_PROMISE nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(promiseGlobal.GetAsSupports()); if (!global) { promiseRv.Throw(NS_ERROR_UNEXPECTED); promiseRv.MaybeSetPendingException(cx); $*{exceptionCode} } @@ -6917,17 +6917,17 @@ class CGCallGenerator(CGThing): elif result is not None: assert resultOutParam is None call = CGWrapper(call, pre=resultVar + " = ") call = CGWrapper(call, post=";\n") self.cgRoot.append(call) if isFallible: - self.cgRoot.prepend(CGGeneric("ErrorResult rv;\n")) + self.cgRoot.prepend(CGGeneric("binding_detail::FastErrorResult rv;\n")) self.cgRoot.append(CGGeneric(dedent( """ if (MOZ_UNLIKELY(rv.MaybeSetPendingException(cx))) { return false; } """))) self.cgRoot.append(CGGeneric("MOZ_ASSERT(!JS_IsExceptionPending(cx));\n")) @@ -8432,17 +8432,17 @@ class CGEnumerateHook(CGAbstractBindingM # Our "self" is actually the "obj" argument in this case, not the thisval. CGAbstractBindingMethod.__init__( self, descriptor, ENUMERATE_HOOK_NAME, args, getThisObj="", callArgs="") def generate_code(self): return CGGeneric(dedent(""" AutoTArray<nsString, 8> names; - ErrorResult rv; + binding_detail::FastErrorResult rv; self->GetOwnPropertyNames(cx, names, rv); if (rv.MaybeSetPendingException(cx)) { return false; } bool dummy; for (uint32_t i = 0; i < names.Length(); ++i) { if (!JS_HasUCProperty(cx, obj, names[i].get(), names[i].Length(), &dummy)) { return false; @@ -10525,17 +10525,17 @@ class CGEnumerateOwnPropertiesViaGetOwnP CGAbstractBindingMethod.__init__(self, descriptor, "EnumerateOwnPropertiesViaGetOwnPropertyNames", args, getThisObj="", callArgs="") def generate_code(self): return CGGeneric(dedent(""" AutoTArray<nsString, 8> names; - ErrorResult rv; + binding_detail::FastErrorResult rv; self->GetOwnPropertyNames(cx, names, rv); if (rv.MaybeSetPendingException(cx)) { return false; } // OK to pass null as "proxy" because it's ignored if // shadowPrototypeProperties is true return AppendNamedPropertyIds(cx, nullptr, names, true, props); """))
--- a/dom/bindings/ErrorResult.h +++ b/dom/bindings/ErrorResult.h @@ -1,41 +1,45 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** - * A struct for tracking exceptions that need to be thrown to JS. + * A set of structs for tracking exceptions that need to be thrown to JS: + * ErrorResult and IgnoredErrorResult. * - * Conceptually, an ErrorResult represents either success or an exception in the + * Conceptually, these structs represent either success or an exception in the * process of being thrown. This means that a failing ErrorResult _must_ be * handled in one of the following ways before coming off the stack: * * 1) Suppressed via SuppressException(). * 2) Converted to a pure nsresult return value via StealNSResult(). * 3) Converted to an actual pending exception on a JSContext via * MaybeSetPendingException. * 4) Converted to an exception JS::Value (probably to then reject a Promise * with) via dom::ToJSValue. + * + * An IgnoredErrorResult will automatically do the first of those four things. */ #ifndef mozilla_ErrorResult_h #define mozilla_ErrorResult_h #include <stdarg.h> #include "js/GCAnnotations.h" #include "js/Value.h" #include "nscore.h" #include "nsStringGlue.h" #include "mozilla/Assertions.h" #include "mozilla/Move.h" #include "nsTArray.h" +#include "nsISupportsImpl.h" namespace IPC { class Message; template <typename> struct ParamTraits; } // namespace IPC class PickleIterator; namespace mozilla { @@ -82,82 +86,107 @@ struct StringArrayAppender } aArgs.AppendElement(aFirst); Append(aArgs, aCount - 1, Forward<Ts>(aOtherArgs)...); } }; } // namespace dom -class ErrorResult { +class ErrorResult; + +namespace binding_danger { + +/** + * Templated implementation class for various ErrorResult-like things. The + * instantiations differ only in terms of their cleanup policies (used in the + * destructor), which they can specify via the template argument. Note that + * this means it's safe to reinterpret_cast between the instantiations unless + * you plan to invoke the destructor through such a cast pointer. + * + * A cleanup policy consists of two booleans: whether to assert that we've been + * reported or suppressed, and whether to then go ahead and suppress the + * exception. + */ +template<typename CleanupPolicy> +class TErrorResult { public: - ErrorResult() + TErrorResult() : mResult(NS_OK) #ifdef DEBUG , mMightHaveUnreportedJSException(false) , mUnionState(HasNothing) #endif { } -#ifdef DEBUG - ~ErrorResult() { - // Consumers should have called one of MaybeSetPendingException - // (possibly via ToJSValue), StealNSResult, and SuppressException - MOZ_ASSERT(!Failed()); - MOZ_ASSERT(!mMightHaveUnreportedJSException); - MOZ_ASSERT(mUnionState == HasNothing); + ~TErrorResult() { + AssertInOwningThread(); + + if (CleanupPolicy::assertHandled) { + // Consumers should have called one of MaybeSetPendingException + // (possibly via ToJSValue), StealNSResult, and SuppressException + AssertReportedOrSuppressed(); + } + + if (CleanupPolicy::suppress) { + SuppressException(); + } + + // And now assert that we're in a good final state. + AssertReportedOrSuppressed(); } -#endif // DEBUG - ErrorResult(ErrorResult&& aRHS) + TErrorResult(TErrorResult&& aRHS) // Initialize mResult and whatever else we need to default-initialize, so // the ClearUnionData call in our operator= will do the right thing // (nothing). - : ErrorResult() + : TErrorResult() { *this = Move(aRHS); } - ErrorResult& operator=(ErrorResult&& aRHS); + TErrorResult& operator=(TErrorResult&& aRHS); - explicit ErrorResult(nsresult aRv) - : ErrorResult() + explicit TErrorResult(nsresult aRv) + : TErrorResult() { AssignErrorCode(aRv); } + operator ErrorResult&(); + void Throw(nsresult rv) { MOZ_ASSERT(NS_FAILED(rv), "Please don't try throwing success"); AssignErrorCode(rv); } - // Duplicate our current state on the given ErrorResult object. Any existing - // errors or messages on the target will be suppressed before cloning. Our - // own error state remains unchanged. - void CloneTo(ErrorResult& aRv) const; + // Duplicate our current state on the given TErrorResult object. Any + // existing errors or messages on the target will be suppressed before + // cloning. Our own error state remains unchanged. + void CloneTo(TErrorResult& aRv) const; // Use SuppressException when you want to suppress any exception that might be - // on the ErrorResult. After this call, the ErrorResult will be back a "no + // on the TErrorResult. After this call, the TErrorResult will be back a "no // exception thrown" state. void SuppressException(); - // Use StealNSResult() when you want to safely convert the ErrorResult to an - // nsresult that you will then return to a caller. This will + // Use StealNSResult() when you want to safely convert the TErrorResult to + // an nsresult that you will then return to a caller. This will // SuppressException(), since there will no longer be a way to report it. nsresult StealNSResult() { nsresult rv = ErrorCode(); SuppressException(); return rv; } - // Use MaybeSetPendingException to convert an ErrorResult to a pending + // Use MaybeSetPendingException to convert a TErrorResult to a pending // exception on the given JSContext. This is the normal "throw an exception" // codepath. // - // The return value is false if the ErrorResult represents success, true + // The return value is false if the TErrorResult represents success, true // otherwise. This does mean that in JSAPI method implementations you can't // just use this as |return rv.MaybeSetPendingException(cx)| (though you could // |return !rv.MaybeSetPendingException(cx)|), but in practice pretty much any // consumer would want to do some more work on the success codepath. So // instead the way you use this is: // // if (rv.MaybeSetPendingException(cx)) { // bail out here @@ -168,31 +197,31 @@ public: // want to pay the price of a function call in some of the consumers of this // method in the common case. // // Note that a true return value does NOT mean there is now a pending // exception on aCx, due to uncatchable exceptions. It should still be // considered equivalent to a JSAPI failure in terms of what callers should do // after true is returned. // - // After this call, the ErrorResult will no longer return true from Failed(), + // After this call, the TErrorResult will no longer return true from Failed(), // since the exception will have moved to the JSContext. bool MaybeSetPendingException(JSContext* cx) { WouldReportJSException(); if (!Failed()) { return false; } SetPendingException(cx); return true; } // Use StealExceptionFromJSContext to convert a pending exception on a - // JSContext to an ErrorResult. This function must be called only when a + // JSContext to a TErrorResult. This function must be called only when a // JSAPI operation failed. It assumes that lack of pending exception on the // JSContext means an uncatchable exception was thrown. // // Codepaths that might call this method must call MightThrowJSException even // if the relevant JSAPI calls do not fail. // // When this function returns, JS_IsExceptionPending(cx) will definitely be // false. @@ -210,42 +239,42 @@ public: { ThrowErrorWithMessage<errorNumber>(NS_ERROR_RANGE_ERR, Forward<Ts>(messageArgs)...); } bool IsErrorWithMessage() const { return ErrorCode() == NS_ERROR_TYPE_ERR || ErrorCode() == NS_ERROR_RANGE_ERR; } // Facilities for throwing a preexisting JS exception value via this - // ErrorResult. The contract is that any code which might end up calling + // TErrorResult. The contract is that any code which might end up calling // ThrowJSException() or StealExceptionFromJSContext() must call // MightThrowJSException() even if no exception is being thrown. Code that - // conditionally calls ToJSValue on this ErrorResult only if Failed() must - // first call WouldReportJSException even if this ErrorResult has not failed. + // conditionally calls ToJSValue on this TErrorResult only if Failed() must + // first call WouldReportJSException even if this TErrorResult has not failed. // // The exn argument to ThrowJSException can be in any compartment. It does // not have to be in the compartment of cx. If someone later uses it, they // will wrap it into whatever compartment they're working in, as needed. void ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn); bool IsJSException() const { return ErrorCode() == NS_ERROR_DOM_JS_EXCEPTION; } // Facilities for throwing a DOMException. If an empty message string is // passed to ThrowDOMException, the default message string for the given // nsresult will be used. The passed-in string must be UTF-8. The nsresult // passed in must be one we create DOMExceptions for; otherwise you may get an // XPConnect Exception. void ThrowDOMException(nsresult rv, const nsACString& message = EmptyCString()); bool IsDOMException() const { return ErrorCode() == NS_ERROR_DOM_DOMEXCEPTION; } - // Flag on the ErrorResult that whatever needs throwing has been + // Flag on the TErrorResult that whatever needs throwing has been // thrown on the JSContext already and we should not mess with it. // If nothing was thrown, this becomes an uncatchable exception. void NoteJSContextException(JSContext* aCx); - // Check whether the ErrorResult says to just throw whatever is on + // Check whether the TErrorResult says to just throw whatever is on // the JSContext already. bool IsJSContextException() { return ErrorCode() == NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT; } // Support for uncatchable exceptions. void ThrowUncatchableException() { Throw(NS_ERROR_UNCATCHABLE_EXCEPTION); @@ -301,24 +330,25 @@ private: enum UnionState { HasMessage, HasDOMExceptionInfo, HasJSException, HasNothing }; #endif // DEBUG + friend struct IPC::ParamTraits<TErrorResult>; friend struct IPC::ParamTraits<ErrorResult>; void SerializeMessage(IPC::Message* aMsg) const; bool DeserializeMessage(const IPC::Message* aMsg, PickleIterator* aIter); void SerializeDOMExceptionInfo(IPC::Message* aMsg) const; bool DeserializeDOMExceptionInfo(const IPC::Message* aMsg, PickleIterator* aIter); - // Helper method that creates a new Message for this ErrorResult, + // Helper method that creates a new Message for this TErrorResult, // and returns the arguments array from that Message. nsTArray<nsString>& CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType); template<dom::ErrNum errorNumber, typename... Ts> void ThrowErrorWithMessage(nsresult errorType, Ts&&... messageArgs) { #if defined(DEBUG) && (defined(__clang__) || defined(__GNUC__)) static_assert(dom::ErrorFormatNumArgs[errorNumber] == sizeof...(messageArgs), @@ -331,16 +361,22 @@ private: uint16_t argCount = dom::GetErrorArgCount(errorNumber); dom::StringArrayAppender::Append(messageArgsArray, argCount, Forward<Ts>(messageArgs)...); #ifdef DEBUG mUnionState = HasMessage; #endif // DEBUG } + MOZ_ALWAYS_INLINE void AssertInOwningThread() const { +#ifdef DEBUG + NS_ASSERT_OWNINGTHREAD(TErrorResult); +#endif + } + void AssignErrorCode(nsresult aRv) { MOZ_ASSERT(aRv != NS_ERROR_TYPE_ERR, "Use ThrowTypeError()"); MOZ_ASSERT(aRv != NS_ERROR_RANGE_ERR, "Use ThrowRangeError()"); MOZ_ASSERT(!IsErrorWithMessage(), "Don't overwrite errors with message"); MOZ_ASSERT(aRv != NS_ERROR_DOM_JS_EXCEPTION, "Use ThrowJSException()"); MOZ_ASSERT(!IsJSException(), "Don't overwrite JS exceptions"); MOZ_ASSERT(aRv != NS_ERROR_DOM_DOMEXCEPTION, "Use ThrowDOMException()"); MOZ_ASSERT(!IsDOMException(), "Don't overwrite DOM exceptions"); @@ -366,16 +402,22 @@ private: void SetPendingException(JSContext* cx); // Methods for setting various specific kinds of pending exceptions. void SetPendingExceptionWithMessage(JSContext* cx); void SetPendingJSException(JSContext* cx); void SetPendingDOMException(JSContext* cx); void SetPendingGenericErrorException(JSContext* cx); + MOZ_ALWAYS_INLINE void AssertReportedOrSuppressed() + { + MOZ_ASSERT(!Failed()); + MOZ_ASSERT(!mMightHaveUnreportedJSException); + MOZ_ASSERT(mUnionState == HasNothing); + } // Special values of mResult: // NS_ERROR_TYPE_ERR -- ThrowTypeError() called on us. // NS_ERROR_RANGE_ERR -- ThrowRangeError() called on us. // NS_ERROR_DOM_JS_EXCEPTION -- ThrowJSException() called on us. // NS_ERROR_UNCATCHABLE_EXCEPTION -- ThrowUncatchableException called on us. // NS_ERROR_DOM_DOMEXCEPTION -- ThrowDOMException() called on us. nsresult mResult; @@ -399,32 +441,95 @@ private: // for assertion purposes. bool mMightHaveUnreportedJSException; // Used to keep track of what's stored in our union right now. Note // that this may be set to HasNothing even if our mResult suggests // we should have something, if we have already cleaned up the // something. UnionState mUnionState; + + // The thread that created this TErrorResult + NS_DECL_OWNINGTHREAD; #endif // Not to be implemented, to make sure people always pass this by // reference, not by value. + TErrorResult(const TErrorResult&) = delete; + void operator=(const TErrorResult&) = delete; +}; + +struct JustAssertCleanupPolicy { + static const bool assertHandled = true; + static const bool suppress = false; +}; + +struct AssertAndSuppressCleanupPolicy { + static const bool assertHandled = true; + static const bool suppress = true; +}; + +struct JustSuppressCleanupPolicy { + static const bool assertHandled = false; + static const bool suppress = true; +}; + +} // namespace binding_danger + +// A class people should normally use on the stack when they plan to actually +// do something with the exception. +class ErrorResult : + public binding_danger::TErrorResult<binding_danger::AssertAndSuppressCleanupPolicy> +{ + typedef binding_danger::TErrorResult<binding_danger::AssertAndSuppressCleanupPolicy> BaseErrorResult; + +public: + ErrorResult() + : BaseErrorResult() + {} + + ErrorResult(ErrorResult&& aRHS) + : BaseErrorResult(Move(aRHS)) + {} + + explicit ErrorResult(nsresult aRv) + : BaseErrorResult(aRv) + {} + + void operator=(nsresult rv) + { + BaseErrorResult::operator=(rv); + } + + ErrorResult& operator=(ErrorResult&& aRHS) + { + BaseErrorResult::operator=(Move(aRHS)); + return *this; + } + +private: + // Not to be implemented, to make sure people always pass this by + // reference, not by value. ErrorResult(const ErrorResult&) = delete; void operator=(const ErrorResult&) = delete; }; -// A class for use when an ErrorResult should just automatically be ignored. -class IgnoredErrorResult : public ErrorResult +template<typename CleanupPolicy> +binding_danger::TErrorResult<CleanupPolicy>::operator ErrorResult&() { -public: - ~IgnoredErrorResult() - { - SuppressException(); - } + return *static_cast<ErrorResult*>( + reinterpret_cast<TErrorResult<AssertAndSuppressCleanupPolicy>*>(this)); +} + +// A class for use when an ErrorResult should just automatically be ignored. +// This doesn't inherit from ErrorResult so we don't make two separate calls to +// SuppressException. +class IgnoredErrorResult : + public binding_danger::TErrorResult<binding_danger::JustSuppressCleanupPolicy> +{ }; /****************************************************************************** ** Macros for checking results ******************************************************************************/ #define ENSURE_SUCCESS(res, ret) \ do { \
--- a/dom/bindings/test/test_promise_rejections_from_jsimplemented.html +++ b/dom/bindings/test/test_promise_rejections_from_jsimplemented.html @@ -20,105 +20,114 @@ https://bugzilla.mozilla.org/show_bug.cg is(exn.name, name, "Should have the right exception name in test " + testNumber); is("filename" in exn ? exn.filename : exn.fileName, filename, "Should have the right file name in test " + testNumber); is(exn.message, message, "Should have the right message in test " + testNumber); is(exn.code, code, "Should have the right .code in test " + testNumber); if (message === "") { - is(exn.name, "NS_ERROR_UNEXPECTED", + is(exn.name, "InternalError", "Should have one of our synthetic exceptions in test " + testNumber); } is(exn.stack, stack, "Should have the right stack in test " + testNumber); } function ensurePromiseFail(testNumber, value) { ok(false, "Test " + testNumber + " should not have a fulfilled promise"); } function doTest() { var t = new TestInterfaceJS(); /* Async parent frames from pushPrefEnv don't show up in e10s. */ var isE10S = !SpecialPowers.isMainProcess(); var asyncStack = SpecialPowers.getBoolPref("javascript.options.asyncstack"); var ourFile = location.href; - var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:121:3 + var unwrapError = "Promise rejection value is a non-unwrappable cross-compartment wrapper."; + var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:130:3 ` : ""; Promise.all([ t.testPromiseWithThrowingChromePromiseInit().then( ensurePromiseFail.bind(null, 1), - checkExn.bind(null, 48, "NS_ERROR_UNEXPECTED", "", undefined, - ourFile, 1, - `doTest@${ourFile}:48:7 + checkExn.bind(null, 49, "InternalError", unwrapError, + undefined, ourFile, 1, + `doTest@${ourFile}:49:7 ` + parentFrame)), t.testPromiseWithThrowingContentPromiseInit(function() { thereIsNoSuchContentFunction1(); }).then( ensurePromiseFail.bind(null, 2), - checkExn.bind(null, 56, "ReferenceError", + checkExn.bind(null, 57, "ReferenceError", "thereIsNoSuchContentFunction1 is not defined", undefined, ourFile, 2, - `doTest/<@${ourFile}:56:11 -doTest@${ourFile}:55:7 + `doTest/<@${ourFile}:57:11 +doTest@${ourFile}:56:7 ` + parentFrame)), t.testPromiseWithThrowingChromeThenFunction().then( ensurePromiseFail.bind(null, 3), - checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 3, "")), + checkExn.bind(null, 0, "InternalError", unwrapError, undefined, "", 3, asyncStack ? (`Async*doTest@${ourFile}:67:7 +` + + parentFrame) : "")), t.testPromiseWithThrowingContentThenFunction(function() { thereIsNoSuchContentFunction2(); }).then( ensurePromiseFail.bind(null, 4), - checkExn.bind(null, 70, "ReferenceError", + checkExn.bind(null, 73, "ReferenceError", "thereIsNoSuchContentFunction2 is not defined", undefined, ourFile, 4, - `doTest/<@${ourFile}:70:11 + `doTest/<@${ourFile}:73:11 ` + - (asyncStack ? `Async*doTest@${ourFile}:69:7 + (asyncStack ? `Async*doTest@${ourFile}:72:7 ` : "") + parentFrame)), t.testPromiseWithThrowingChromeThenable().then( ensurePromiseFail.bind(null, 5), - checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 5, "")), + checkExn.bind(null, 0, "InternalError", unwrapError, undefined, "", 5, asyncStack ? (`Async*doTest@${ourFile}:84:7 +` + + parentFrame) : "")), t.testPromiseWithThrowingContentThenable({ then: function() { thereIsNoSuchContentFunction3(); } }).then( ensurePromiseFail.bind(null, 6), - checkExn.bind(null, 85, "ReferenceError", + checkExn.bind(null, 90, "ReferenceError", "thereIsNoSuchContentFunction3 is not defined", undefined, ourFile, 6, - `doTest/<.then@${ourFile}:85:32 -`)), + `doTest/<.then@${ourFile}:90:32 +` + (asyncStack ? `Async*doTest@${ourFile}:89:7\n` + parentFrame : ""))), t.testPromiseWithDOMExceptionThrowingPromiseInit().then( ensurePromiseFail.bind(null, 7), - checkExn.bind(null, 93, "NotFoundError", + checkExn.bind(null, 98, "NotFoundError", "We are a second DOMException", DOMException.NOT_FOUND_ERR, ourFile, 7, - `doTest@${ourFile}:93:7 + `doTest@${ourFile}:98:7 ` + parentFrame)), t.testPromiseWithDOMExceptionThrowingThenFunction().then( ensurePromiseFail.bind(null, 8), - checkExn.bind(null, asyncStack ? 101 : 0, "NetworkError", + checkExn.bind(null, asyncStack ? 106 : 0, "NetworkError", "We are a third DOMException", DOMException.NETWORK_ERR, asyncStack ? ourFile : "", 8, - (asyncStack ? `Async*doTest@${ourFile}:101:7 + (asyncStack ? `Async*doTest@${ourFile}:106:7 ` + parentFrame : ""))), t.testPromiseWithDOMExceptionThrowingThenable().then( ensurePromiseFail.bind(null, 9), - checkExn.bind(null, 0, "TypeMismatchError", + checkExn.bind(null, asyncStack ? 114 : 0, "TypeMismatchError", "We are a fourth DOMException", - DOMException.TYPE_MISMATCH_ERR, "", 9, "")), + DOMException.TYPE_MISMATCH_ERR, + asyncStack ? ourFile : "", 9, + (asyncStack ? `Async*doTest@${ourFile}:114:7 +` + + parentFrame : ""))), ]).then(SimpleTest.finish, - function() { - ok(false, "One of our catch statements totally failed"); + function(err) { + ok(false, "One of our catch statements totally failed with err" + err + ', stack: ' + (err ? err.stack : '')); SimpleTest.finish(); }); } SpecialPowers.pushPrefEnv({set: [['dom.expose_test_interfaces', true]]}, doTest); </script> </head>
--- a/dom/filesystem/compat/tests/test_no_dnd.html +++ b/dom/filesystem/compat/tests/test_no_dnd.html @@ -2,49 +2,50 @@ <html> <head> <title>Test for Blink FileSystem API - no DND == no webkitEntries</title> <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> </head> <body> -<input id="entries" type="file"></input> <script type="application/javascript;version=1.7"> var fileEntry; var directoryEntry; var script; +var entries; function setup_tests() { SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true], ["dom.webkitBlink.filesystem.enabled", true]]}, next); } function populate_entries() { + entries = document.createElement('input'); + entries.setAttribute('type', 'file'); + document.body.appendChild(entries); + var url = SimpleTest.getTestFileURL("script_entries.js"); script = SpecialPowers.loadChromeScript(url); function onOpened(message) { - var entries = document.getElementById('entries'); - for (var i = 0 ; i < message.data.length; ++i) { if (message.data[i] instanceof File) { SpecialPowers.wrap(entries).mozSetFileArray([message.data[i]]); next(); } } } script.addMessageListener("entries.opened", onOpened); script.sendAsyncMessage("entries.open"); } function test_entries() { - var entries = document.getElementById('entries'); ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries"); is(entries.webkitEntries.length, 0, "HTMLInputElement.webkitEntries.length == 0"); is(entries.files.length, 1, "HTMLInputElement.files is still populated"); next(); } function cleanUpTestingFiles() {
--- a/dom/geolocation/nsGeolocation.cpp +++ b/dom/geolocation/nsGeolocation.cpp @@ -558,28 +558,20 @@ nsGeolocationRequest::GetRequester(nsICo return NS_OK; } void nsGeolocationRequest::SetTimeoutTimer() { StopTimeoutTimer(); - int32_t timeout; - if (mOptions && (timeout = mOptions->mTimeout) != 0) { - - if (timeout < 0) { - timeout = 0; - } else if (timeout < 10) { - timeout = 10; - } - + if (mOptions && mOptions->mTimeout != 0 && mOptions->mTimeout != 0x7fffffff) { mTimeoutTimer = do_CreateInstance("@mozilla.org/timer;1"); RefPtr<TimerCallbackHolder> holder = new TimerCallbackHolder(this); - mTimeoutTimer->InitWithCallback(holder, timeout, nsITimer::TYPE_ONE_SHOT); + mTimeoutTimer->InitWithCallback(holder, mOptions->mTimeout, nsITimer::TYPE_ONE_SHOT); } } void nsGeolocationRequest::StopTimeoutTimer() { if (mTimeoutTimer) { mTimeoutTimer->Cancel();
--- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -3407,17 +3407,18 @@ HTMLInputElement::Focus(ErrorResult& aEr } #if defined(XP_WIN) || defined(XP_LINUX) bool HTMLInputElement::IsNodeApzAwareInternal() const { // Tell APZC we may handle mouse wheel event and do preventDefault when input // type is number. - return (mType == NS_FORM_INPUT_NUMBER) || nsINode::IsNodeApzAwareInternal(); + return (mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE) || + nsINode::IsNodeApzAwareInternal(); } #endif bool HTMLInputElement::IsInteractiveHTMLContent(bool aIgnoreTabindex) const { return mType != NS_FORM_INPUT_HIDDEN || nsGenericHTMLFormElementWithState::IsInteractiveHTMLContent(aIgnoreTabindex); @@ -4502,27 +4503,40 @@ HTMLInputElement::PostHandleEvent(EventC StopNumberControlSpinnerSpin(); } } break; } #if defined(XP_WIN) || defined(XP_LINUX) case eWheel: { // Handle wheel events as increasing / decreasing the input element's - // value when it's focused and it's type is number. + // value when it's focused and it's type is number or range. WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent(); if (!aVisitor.mEvent->DefaultPrevented() && aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent && wheelEvent->mDeltaY != 0 && - wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL && - mType == NS_FORM_INPUT_NUMBER) { - nsNumberControlFrame* numberControlFrame = - do_QueryFrame(GetPrimaryFrame()); - if (numberControlFrame && numberControlFrame->IsFocused()) { - StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1); + wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL) { + if (mType == NS_FORM_INPUT_NUMBER) { + nsNumberControlFrame* numberControlFrame = + do_QueryFrame(GetPrimaryFrame()); + if (numberControlFrame && numberControlFrame->IsFocused()) { + StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1); + aVisitor.mEvent->PreventDefault(); + } + } else if (mType == NS_FORM_INPUT_RANGE && + nsContentUtils::IsFocusedContent(this) && + GetMinimum() < GetMaximum()) { + Decimal value = GetValueAsDecimal(); + Decimal step = GetStep(); + if (step == kStepAny) { + step = GetDefaultStep(); + } + MOZ_ASSERT(value.isFinite() && step.isFinite()); + SetValueOfRangeForUserEvent(wheelEvent->mDeltaY < 0 ? + value + step : value - step); aVisitor.mEvent->PreventDefault(); } } break; } #endif default: break; @@ -6009,17 +6023,17 @@ FireEventForAccessibility(nsIDOMHTMLInpu return NS_OK; } #endif void HTMLInputElement::UpdateApzAwareFlag() { #if defined(XP_WIN) || defined(XP_LINUX) - if (mType == NS_FORM_INPUT_NUMBER) { + if ((mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE)) { SetMayBeApzAware(); } #endif } nsresult HTMLInputElement::SetDefaultValueAsValue() {
--- a/dom/html/test/mochitest.ini +++ b/dom/html/test/mochitest.ini @@ -615,10 +615,14 @@ skip-if = buildapp == 'b2g' # bug 112901 [test_bug1230665.html] [test_filepicker_default_directory.html] skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' [test_bug1233598.html] [test_bug1250401.html] [test_bug1260664.html] [test_bug1261673.html] skip-if = (os != 'win' && os != 'linux') +[test_bug1261674-1.html] +skip-if = (os != 'win' && os != 'linux') +[test_bug1261674-2.html] +skip-if = (os != 'win' && os != 'linux') [test_bug1260704.html] [test_allowMedia.html]
new file mode 100644 --- /dev/null +++ b/dom/html/test/test_bug1261674-1.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1261674 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1261674</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_input" type="range" value=5 max=10 min=0> +<script type="text/javascript"> + +/** Test for Bug 1261674 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = window.document.getElementById("test_input"); + + // focus: whether the target input element is focused + // deltaY: deltaY of WheelEvent + // deltaMode: deltaMode of WheelEvent + // valueChanged: expected value changes after input element handled the wheel event + let params = [ + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1}, + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1}, + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0}, + {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}, + {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0} + ]; + + let testIdx = 0; + let result = parseInt(input.value); + + function runNext() { + let p = params[testIdx]; + (p["focus"]) ? input.focus() : input.blur(); + result += parseInt(p["valueChanged"]); + sendWheelAndPaint(input, 1, 1, { deltaY: p["deltaY"], deltaMode: p["deltaMode"] }, () => { + ok(input.value == result, + "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value); + (++testIdx >= params.length) ? SimpleTest.finish() : runNext(); + }); + } + + input.addEventListener("input", () => { + ok(input.value == result, + "Test-" + testIdx + " receive input event, expect " + result + " get " + input.value); + }, false); + + runNext(); +} + +</script> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/html/test/test_bug1261674-2.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1261674 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1261674</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_input" type="range" max=0 min=10> +<script type="text/javascript"> + +/** Test for Bug 1261674 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = window.document.getElementById("test_input"); + + // deltaY: deltaY of WheelEvent + // deltaMode: deltaMode of WheelEvent + let params = [ + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}, + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE}, + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL}, + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE} + ]; + + let testIdx = 0; + let result = parseInt(input.value); + + function runNext() { + let p = params[testIdx]; + (p["focus"]) ? input.focus() : input.blur(); + sendWheelAndPaint(input, 1, 1, { deltaY: p["deltaY"], deltaMode: p["deltaMode"] }, () => { + ok(input.value == result, + "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value); + testIdx++; + (testIdx >= params.length) ? SimpleTest.finish() : runNext(); + }); + } + + input.addEventListener("input", () => { + ok(false, "Wheel event should be no effect to range input element with max < min"); + }, false); + + runNext(); +} +</script> +</body> +</html>
--- a/dom/presentation/PresentationConnection.cpp +++ b/dom/presentation/PresentationConnection.cpp @@ -183,19 +183,18 @@ PresentationConnection::Close(ErrorResul service->CloseSession(mId, mRole, nsIPresentationService::CLOSED_REASON_CLOSED))); } void PresentationConnection::Terminate(ErrorResult& aRv) { - // It only works when the state is CONNECTED or CONNECTING. - if (NS_WARN_IF(mState != PresentationConnectionState::Connected && - mState != PresentationConnectionState::Connecting)) { + // It only works when the state is CONNECTED. + if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) { return; } nsCOMPtr<nsIPresentationService> service = do_GetService(PRESENTATION_SERVICE_CONTRACTID); if(NS_WARN_IF(!service)) { aRv.Throw(NS_ERROR_DOM_OPERATION_ERR); return; @@ -277,31 +276,32 @@ PresentationConnection::ProcessStateChan CopyUTF8toUTF16(message, errorMsg); } NS_WARN_IF(NS_FAILED(DispatchConnectionClosedEvent(reason, errorMsg))); return RemoveFromLoadGroup(); } case PresentationConnectionState::Terminated: { + // Ensure onterminate event is fired. + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false); + NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent())); + nsCOMPtr<nsIPresentationService> service = do_GetService(PRESENTATION_SERVICE_CONTRACTID); if (NS_WARN_IF(!service)) { return NS_ERROR_NOT_AVAILABLE; } nsresult rv = service->UnregisterSessionListener(mId, mRole); if(NS_WARN_IF(NS_FAILED(rv))) { return rv; } - RefPtr<AsyncEventDispatcher> asyncDispatcher = - new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false); - NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent())); - return RemoveFromLoadGroup(); } default: MOZ_CRASH("Unknown presentation session state."); return NS_ERROR_INVALID_ARG; } }
--- a/dom/presentation/PresentationDeviceManager.cpp +++ b/dom/presentation/PresentationDeviceManager.cpp @@ -9,16 +9,17 @@ #include "mozilla/Services.h" #include "MainThreadUtils.h" #include "nsCategoryCache.h" #include "nsCOMPtr.h" #include "nsIMutableArray.h" #include "nsIObserverService.h" #include "nsXULAppAPI.h" #include "PresentationSessionRequest.h" +#include "PresentationTerminateRequest.h" namespace mozilla { namespace dom { NS_IMPL_ISUPPORTS(PresentationDeviceManager, nsIPresentationDeviceManager, nsIPresentationDeviceListener, nsIObserver, @@ -234,16 +235,38 @@ PresentationDeviceManager::OnSessionRequ new PresentationSessionRequest(aDevice, aUrl, aPresentationId, aControlChannel); obs->NotifyObservers(request, PRESENTATION_SESSION_REQUEST_TOPIC, nullptr); return NS_OK; } +NS_IMETHODIMP +PresentationDeviceManager::OnTerminateRequest(nsIPresentationDevice* aDevice, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver) +{ + NS_ENSURE_ARG(aDevice); + NS_ENSURE_ARG(aControlChannel); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); + + RefPtr<PresentationTerminateRequest> request = + new PresentationTerminateRequest(aDevice, aPresentationId, + aControlChannel, aIsFromReceiver); + obs->NotifyObservers(request, + PRESENTATION_TERMINATE_REQUEST_TOPIC, + nullptr); + + return NS_OK; +} + // nsIObserver NS_IMETHODIMP PresentationDeviceManager::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { if (!strcmp(aTopic, "profile-after-change")) { Init();
--- a/dom/presentation/PresentationService.cpp +++ b/dom/presentation/PresentationService.cpp @@ -1,38 +1,66 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "PresentationService.h" + #include "ipc/PresentationIPCService.h" #include "mozilla/Services.h" #include "mozIApplication.h" +#include "nsGlobalWindow.h" #include "nsIAppsService.h" #include "nsIObserverService.h" #include "nsIPresentationControlChannel.h" #include "nsIPresentationDeviceManager.h" #include "nsIPresentationDevicePrompt.h" #include "nsIPresentationListener.h" #include "nsIPresentationRequestUIGlue.h" #include "nsIPresentationSessionRequest.h" +#include "nsIPresentationTerminateRequest.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "PresentationLog.h" -#include "PresentationService.h" using namespace mozilla; using namespace mozilla::dom; namespace mozilla { namespace dom { +static bool +IsSameDevice(nsIPresentationDevice* aDevice, nsIPresentationDevice* aDeviceAnother) { + if (!aDevice || !aDeviceAnother) { + return false; + } + + nsAutoCString deviceId; + aDevice->GetId(deviceId); + nsAutoCString anotherId; + aDeviceAnother->GetId(anotherId); + if (!deviceId.Equals(anotherId)) { + return false; + } + + nsAutoCString deviceType; + aDevice->GetType(deviceType); + nsAutoCString anotherType; + aDeviceAnother->GetType(anotherType); + if (!deviceType.Equals(anotherType)) { + return false; + } + + return true; +} + /* * Implementation of PresentationDeviceRequest */ class PresentationDeviceRequest final : public nsIPresentationDeviceRequest { public: NS_DECL_ISUPPORTS @@ -186,16 +214,20 @@ PresentationService::Init() rv = obs->AddObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC, false); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } rv = obs->AddObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC, false); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } + rv = obs->AddObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } nsCOMPtr<nsIPresentationDeviceManager> deviceManager = do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID); if (NS_WARN_IF(!deviceManager)) { return false; } rv = deviceManager->GetDeviceAvailable(&mIsAvailable); @@ -214,16 +246,23 @@ PresentationService::Observe(nsISupports return HandleDeviceChange(); } else if (!strcmp(aTopic, PRESENTATION_SESSION_REQUEST_TOPIC)) { nsCOMPtr<nsIPresentationSessionRequest> request(do_QueryInterface(aSubject)); if (NS_WARN_IF(!request)) { return NS_ERROR_FAILURE; } return HandleSessionRequest(request); + } else if (!strcmp(aTopic, PRESENTATION_TERMINATE_REQUEST_TOPIC)) { + nsCOMPtr<nsIPresentationTerminateRequest> request(do_QueryInterface(aSubject)); + if (NS_WARN_IF(!request)) { + return NS_ERROR_FAILURE; + } + + return HandleTerminateRequest(request); } else if (!strcmp(aTopic, "profile-after-change")) { // It's expected since we add and entry to |kLayoutCategories| in // |nsLayoutModule.cpp| to launch this service earlier. return NS_OK; } MOZ_ASSERT(false, "Unexpected topic for PresentationService"); return NS_ERROR_UNEXPECTED; @@ -240,16 +279,17 @@ PresentationService::HandleShutdown() mSessionInfoAtController.Clear(); mSessionInfoAtReceiver.Clear(); nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); if (obs) { obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); obs->RemoveObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC); obs->RemoveObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC); + obs->RemoveObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC); } } nsresult PresentationService::HandleDeviceChange() { nsCOMPtr<nsIPresentationDeviceManager> deviceManager = do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID); @@ -355,16 +395,68 @@ PresentationService::HandleSessionReques return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR); } nsCOMPtr<Promise> realPromise = do_QueryInterface(promise); static_cast<PresentationPresentingInfo*>(info.get())->SetPromise(realPromise); return NS_OK; } +nsresult +PresentationService::HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest) +{ + nsCOMPtr<nsIPresentationControlChannel> ctrlChannel; + nsresult rv = aRequest->GetControlChannel(getter_AddRefs(ctrlChannel)); + if (NS_WARN_IF(NS_FAILED(rv) || !ctrlChannel)) { + return rv; + } + + nsAutoString sessionId; + rv = aRequest->GetPresentationId(sessionId); + if (NS_WARN_IF(NS_FAILED(rv))) { + ctrlChannel->Disconnect(rv); + return rv; + } + + nsCOMPtr<nsIPresentationDevice> device; + rv = aRequest->GetDevice(getter_AddRefs(device)); + if (NS_WARN_IF(NS_FAILED(rv))) { + ctrlChannel->Disconnect(rv); + return rv; + } + + bool isFromReceiver; + rv = aRequest->GetIsFromReceiver(&isFromReceiver); + if (NS_WARN_IF(NS_FAILED(rv))) { + ctrlChannel->Disconnect(rv); + return rv; + } + + RefPtr<PresentationSessionInfo> info; + if (!isFromReceiver) { + info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER); + } else { + info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_CONTROLLER); + } + if (NS_WARN_IF(!info)) { + // Cannot terminate non-existed session. + ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR); + return NS_ERROR_DOM_ABORT_ERR; + } + + // Check if terminate request comes from known device. + RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice(); + if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) { + ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR); + return NS_ERROR_DOM_ABORT_ERR; + } + + return info->OnTerminate(ctrlChannel); +} + void PresentationService::NotifyAvailableChange(bool aIsAvailable) { nsTObserverArray<nsCOMPtr<nsIPresentationAvailabilityListener>>::ForwardIterator iter(mAvailabilityListeners); while (iter.HasMore()) { nsCOMPtr<nsIPresentationAvailabilityListener> listener = iter.GetNext(); NS_WARN_IF(NS_FAILED(listener->NotifyAvailableChange(aIsAvailable))); } @@ -520,17 +612,16 @@ PresentationService::CloseSession(const aRole == nsIPresentationService::ROLE_RECEIVER); RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole); if (NS_WARN_IF(!info)) { return NS_ERROR_NOT_AVAILABLE; } if (aClosedReason == nsIPresentationService::CLOSED_REASON_WENTAWAY) { - UntrackSessionInfo(aSessionId, aRole); // Remove nsIPresentationSessionListener since we don't want to dispatch // PresentationConnectionClosedEvent if the page is went away. info->SetListener(nullptr); } return info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED); } @@ -606,18 +697,19 @@ PresentationService::UnregisterSessionLi uint8_t aRole) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER || aRole == nsIPresentationService::ROLE_RECEIVER); RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole); if (info) { - NS_WARN_IF(NS_FAILED(info->Close(NS_OK, nsIPresentationSessionListener::STATE_TERMINATED))); - UntrackSessionInfo(aSessionId, aRole); + // When content side decide not handling this session anymore, simply + // close the connection. Session info is kept for reconnection. + NS_WARN_IF(NS_FAILED(info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED))); return info->SetListener(nullptr); } return NS_OK; } nsresult PresentationService::RegisterTransportBuilder(const nsAString& aSessionId, uint8_t aRole, @@ -724,16 +816,27 @@ PresentationService::UntrackSessionInfo( uint8_t aRole) { MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER || aRole == nsIPresentationService::ROLE_RECEIVER); // Remove the session info. if (nsIPresentationService::ROLE_CONTROLLER == aRole) { mSessionInfoAtController.Remove(aSessionId); } else { + // Terminate receiver page. + uint64_t windowId; + nsresult rv = GetWindowIdBySessionIdInternal(aSessionId, &windowId); + if (NS_SUCCEEDED(rv)) { + NS_DispatchToMainThread(NS_NewRunnableFunction([windowId]() -> void { + if (auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId)) { + window->Close(); + } + })); + } + mSessionInfoAtReceiver.Remove(aSessionId); } // Remove the in-process responding info if there's still any. RemoveRespondingSessionId(aSessionId); return NS_OK; }
--- a/dom/presentation/PresentationService.h +++ b/dom/presentation/PresentationService.h @@ -9,16 +9,17 @@ #include "nsCOMPtr.h" #include "nsIObserver.h" #include "nsTObserverArray.h" #include "PresentationServiceBase.h" #include "PresentationSessionInfo.h" class nsIPresentationSessionRequest; +class nsIPresentationTerminateRequest; class nsIURI; class nsIPresentationSessionTransportBuilder; namespace mozilla { namespace dom { class PresentationDeviceRequest; class PresentationRespondingInfo; @@ -61,16 +62,17 @@ public: private: friend class PresentationDeviceRequest; virtual ~PresentationService(); void HandleShutdown(); nsresult HandleDeviceChange(); nsresult HandleSessionRequest(nsIPresentationSessionRequest* aRequest); + nsresult HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest); void NotifyAvailableChange(bool aIsAvailable); bool IsAppInstalled(nsIURI* aUri); // This is meant to be called by PresentationDeviceRequest. already_AddRefed<PresentationSessionInfo> CreateControllingSessionInfo(const nsAString& aUrl, const nsAString& aSessionId, uint64_t aWindowId);
--- a/dom/presentation/PresentationSessionInfo.cpp +++ b/dom/presentation/PresentationSessionInfo.cpp @@ -231,16 +231,17 @@ PresentationSessionInfo::Shutdown(nsresu // Close the data transport channel if any. if (mTransport) { // |mIsTransportReady| will be unset once |NotifyTransportClosed| is called. NS_WARN_IF(NS_FAILED(mTransport->Close(aReason))); } mIsResponderReady = false; + mIsOnTerminating = false; SetBuilder(nullptr); } nsresult PresentationSessionInfo::SetListener(nsIPresentationSessionListener* aListener) { mListener = aListener; @@ -279,19 +280,52 @@ PresentationSessionInfo::Send(const nsAS nsresult PresentationSessionInfo::Close(nsresult aReason, uint32_t aState) { if (NS_WARN_IF(!IsSessionReady())) { return NS_ERROR_DOM_INVALID_STATE_ERR; } + // Do nothing if session is already terminated. + if (nsIPresentationSessionListener::STATE_TERMINATED == mState) { + return NS_OK; + } + SetStateWithReason(aState, aReason); - Shutdown(aReason); + switch (aState) { + case nsIPresentationSessionListener::STATE_CLOSED: { + Shutdown(aReason); + break; + } + case nsIPresentationSessionListener::STATE_TERMINATED: { + if (!mControlChannel) { + nsCOMPtr<nsIPresentationControlChannel> ctrlChannel; + nsresult rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel)); + if (NS_SUCCEEDED(rv)) { + SetControlChannel(ctrlChannel); + } + return rv; + } + + return mControlChannel->Terminate(mSessionId); + } + } + + return NS_OK; +} + +nsresult +PresentationSessionInfo::OnTerminate(nsIPresentationControlChannel* aControlChannel) +{ + mIsOnTerminating = true; // Mark for terminating transport channel + SetStateWithReason(nsIPresentationSessionListener::STATE_TERMINATED, NS_OK); + SetControlChannel(aControlChannel); + return NS_OK; } nsresult PresentationSessionInfo::ReplySuccess() { SetStateWithReason(nsIPresentationSessionListener::STATE_CONNECTED, NS_OK); return NS_OK; @@ -337,16 +371,28 @@ PresentationSessionInfo::GetWindow() /* virtual */ bool PresentationSessionInfo::IsAccessible(base::ProcessId aProcessId) { // No restriction by default. return true; } +void +PresentationSessionInfo::ContinueTermination() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mControlChannel); + + if (NS_WARN_IF(NS_FAILED(mControlChannel->Terminate(mSessionId))) + || mIsOnTerminating) { + Shutdown(NS_OK); + } +} + // nsIPresentationSessionTransportCallback NS_IMETHODIMP PresentationSessionInfo::NotifyTransportReady() { MOZ_ASSERT(NS_IsMainThread()); mIsTransportReady = true; @@ -687,16 +733,37 @@ PresentationControllingInfo::OnAnswer(ns return NS_OK; } NS_IMETHODIMP PresentationControllingInfo::NotifyConnected() { MOZ_ASSERT(NS_IsMainThread()); + switch (mState) { + case nsIPresentationSessionListener::STATE_CONNECTING: { + Unused << NS_WARN_IF(NS_FAILED(BuildTransport())); + break; + } + case nsIPresentationSessionListener::STATE_TERMINATED: { + ContinueTermination(); + break; + } + default: + break; + } + + return NS_OK; +} + +nsresult +PresentationControllingInfo::BuildTransport() +{ + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = mControlChannel->Launch(GetSessionId(), GetUrl()); if (NS_FAILED(rv)) { return rv; } if (!Preferences::GetBool("dom.presentation.session_transport.data_channel.enable")) { // Build TCP session transport return GetAddress(); @@ -1134,17 +1201,20 @@ PresentationPresentingInfo::OnIceCandida builder = do_QueryInterface(mBuilder); return builder->OnIceCandidate(aCandidate); } NS_IMETHODIMP PresentationPresentingInfo::NotifyConnected() { - // Do nothing. + if (nsIPresentationSessionListener::STATE_TERMINATED == mState) { + ContinueTermination(); + } + return NS_OK; } NS_IMETHODIMP PresentationPresentingInfo::NotifyDisconnected(nsresult aReason) { MOZ_ASSERT(NS_IsMainThread());
--- a/dom/presentation/PresentationSessionInfo.h +++ b/dom/presentation/PresentationSessionInfo.h @@ -100,16 +100,18 @@ public: } } nsresult Send(const nsAString& aData); nsresult Close(nsresult aReason, uint32_t aState); + nsresult OnTerminate(nsIPresentationControlChannel* aControlChannel); + nsresult ReplyError(nsresult aReason); virtual bool IsAccessible(base::ProcessId aProcessId); protected: virtual ~PresentationSessionInfo() { Shutdown(NS_OK); @@ -137,28 +139,31 @@ protected: // Notify session state change. if (mListener) { nsresult rv = mListener->NotifyStateChange(mSessionId, mState, aReason); NS_WARN_IF(NS_FAILED(rv)); } } + void ContinueTermination(); + // Should be nsIPresentationChannelDescription::TYPE_TCP/TYPE_DATACHANNEL uint8_t mTransportType = 0; nsPIDOMWindowInner* GetWindow(); nsString mUrl; nsString mSessionId; // mRole should be nsIPresentationService::ROLE_CONTROLLER // or nsIPresentationService::ROLE_RECEIVER. uint8_t mRole; bool mIsResponderReady; bool mIsTransportReady; + bool mIsOnTerminating = false; uint32_t mState; // CONNECTED, CLOSED, TERMINATED nsresult mReason; nsCOMPtr<nsIPresentationSessionListener> mListener; nsCOMPtr<nsIPresentationDevice> mDevice; nsCOMPtr<nsIPresentationSessionTransport> mTransport; nsCOMPtr<nsIPresentationControlChannel> mControlChannel; nsCOMPtr<nsIPresentationSessionTransportBuilder> mBuilder; }; @@ -188,16 +193,18 @@ private: } void Shutdown(nsresult aReason) override; nsresult GetAddress(); nsresult OnGetAddress(const nsACString& aAddress); + nsresult BuildTransport(); + nsCOMPtr<nsIServerSocket> mServerSocket; }; // Session info with presenting browsing context (receiver side) behaviors. class PresentationPresentingInfo final : public PresentationSessionInfo , public PromiseNativeHandler , public nsITimerCallback {
new file mode 100644 --- /dev/null +++ b/dom/presentation/PresentationTerminateRequest.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PresentationTerminateRequest.h" +#include "nsIPresentationControlChannel.h" +#include "nsIPresentationDevice.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(PresentationTerminateRequest, nsIPresentationTerminateRequest) + +PresentationTerminateRequest::PresentationTerminateRequest( + nsIPresentationDevice* aDevice, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver) + : mPresentationId(aPresentationId) + , mDevice(aDevice) + , mControlChannel(aControlChannel) + , mIsFromReceiver(aIsFromReceiver) +{ +} + +PresentationTerminateRequest::~PresentationTerminateRequest() +{ +} + +// nsIPresentationTerminateRequest +NS_IMETHODIMP +PresentationTerminateRequest::GetDevice(nsIPresentationDevice** aRetVal) +{ + NS_ENSURE_ARG_POINTER(aRetVal); + + nsCOMPtr<nsIPresentationDevice> device = mDevice; + device.forget(aRetVal); + + return NS_OK; +} + +NS_IMETHODIMP +PresentationTerminateRequest::GetPresentationId(nsAString& aRetVal) +{ + aRetVal = mPresentationId; + + return NS_OK; +} + +NS_IMETHODIMP +PresentationTerminateRequest::GetControlChannel( + nsIPresentationControlChannel** aRetVal) +{ + NS_ENSURE_ARG_POINTER(aRetVal); + + nsCOMPtr<nsIPresentationControlChannel> controlChannel = mControlChannel; + controlChannel.forget(aRetVal); + + return NS_OK; +} + +NS_IMETHODIMP +PresentationTerminateRequest::GetIsFromReceiver(bool* aRetVal) +{ + *aRetVal = mIsFromReceiver; + + return NS_OK; +} + +} // namespace dom +} // namespace mozilla
new file mode 100644 --- /dev/null +++ b/dom/presentation/PresentationTerminateRequest.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_PresentationTerminateRequest_h__ +#define mozilla_dom_PresentationTerminateRequest_h__ + +#include "nsIPresentationTerminateRequest.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +namespace mozilla { +namespace dom { + +class PresentationTerminateRequest final : public nsIPresentationTerminateRequest +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPRESENTATIONTERMINATEREQUEST + + PresentationTerminateRequest(nsIPresentationDevice* aDevice, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver); + +private: + virtual ~PresentationTerminateRequest(); + + nsString mPresentationId; + nsCOMPtr<nsIPresentationDevice> mDevice; + nsCOMPtr<nsIPresentationControlChannel> mControlChannel; + bool mIsFromReceiver; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_PresentationTerminateRequest_h__ */ +
--- a/dom/presentation/interfaces/moz.build +++ b/dom/presentation/interfaces/moz.build @@ -13,16 +13,17 @@ XPIDL_SOURCES += [ 'nsIPresentationDeviceProvider.idl', 'nsIPresentationListener.idl', 'nsIPresentationLocalDevice.idl', 'nsIPresentationRequestUIGlue.idl', 'nsIPresentationService.idl', 'nsIPresentationSessionRequest.idl', 'nsIPresentationSessionTransport.idl', 'nsIPresentationSessionTransportBuilder.idl', + 'nsIPresentationTerminateRequest.idl', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android': XPIDL_SOURCES += [ 'nsIPresentationNetworkHelper.idl', ] XPIDL_MODULE = 'dom_presentation'
--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl +++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl @@ -106,13 +106,20 @@ interface nsIPresentationControlChannel: * Launch a presentation on remote endpoint. * @param presentationId The Id for representing this session. * @param url The URL requested to open by remote device. * @throws NS_ERROR_FAILURE on failure */ void launch(in DOMString presentationId, in DOMString url); /* + * Terminate a presentation on remote endpoint. + * @param presentationId The Id for representing this session. + * @throws NS_ERROR_FAILURE on failure + */ + void terminate(in DOMString presentationId); + + /* * Disconnect the control channel. * @param reason The reason of disconnecting channel; NS_OK represents normal. */ void disconnect(in nsresult reason); };
--- a/dom/presentation/interfaces/nsIPresentationControlService.idl +++ b/dom/presentation/interfaces/nsIPresentationControlService.idl @@ -40,16 +40,28 @@ interface nsIPresentationControlServerLi * @param aUrl The URL requested to open by remote device. * @param aPresentationId The Id for representing this session. * @param aControlChannel The control channel for this session. */ void onSessionRequest(in nsITCPDeviceInfo aDeviceInfo, in DOMString aUrl, in DOMString aPresentationId, in nsIPresentationControlChannel aControlChannel); + + /** + * Callback while the remote host is requesting to terminate a presentation session. + * @param aDeviceInfo The device information related to the remote host. + * @param aPresentationId The Id for representing this session. + * @param aControlChannel The control channel for this session. + * @param aIsFromReceiver true if termination is initiated by receiver. + */ + void onTerminateRequest(in nsITCPDeviceInfo aDeviceInfo, + in DOMString aPresentationId, + in nsIPresentationControlChannel aControlChannel, + in boolean aIsFromReceiver); }; /** * Presentation control service which can be used for both presentation * control client and server. */ [scriptable, uuid(55d6b605-2389-4aae-a8fe-60d4440540ea)] interface nsIPresentationControlService: nsISupports
--- a/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl +++ b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl @@ -27,16 +27,28 @@ interface nsIPresentationDeviceListener: * @param url The URL requested to open by remote device. * @param presentationId The Id for representing this session. * @param controlChannel The control channel for this session. */ void onSessionRequest(in nsIPresentationDevice device, in DOMString url, in DOMString presentationId, in nsIPresentationControlChannel controlChannel); + + /* + * Callback while the remote device is requesting to terminate a presentation session. + * @param device The remote device that sent session request. + * @param presentationId The Id for representing this session. + * @param controlChannel The control channel for this session. + * @param aIsFromReceiver true if termination is initiated by receiver. + */ + void onTerminateRequest(in nsIPresentationDevice device, + in DOMString presentationId, + in nsIPresentationControlChannel controlChannel, + in boolean aIsFromReceiver); }; /* * Device provider for any device protocol, can be registered as default * providers by adding its contractID to category "presentation-device-provider". */ [scriptable, uuid(3db2578a-0f50-44ad-b01b-28427b71b7bf)] interface nsIPresentationDeviceProvider: nsISupports
new file mode 100644 --- /dev/null +++ b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl @@ -0,0 +1,33 @@ +/* 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 "nsISupports.idl" + +interface nsIPresentationDevice; +interface nsIPresentationControlChannel; + +%{C++ +#define PRESENTATION_TERMINATE_REQUEST_TOPIC "presentation-terminate-request" +%} + +/* + * The event of a device requesting for terminating a presentation session. User can + * monitor the terminate request on every device by observing "presentation-terminate-request". + */ +[scriptable, uuid(3ddbf3a4-53ee-4b70-9bbc-58ac90dce6b5)] +interface nsIPresentationTerminateRequest: nsISupports +{ + // The device which requesting to terminate presentation session. + readonly attribute nsIPresentationDevice device; + + // The Id for representing this session. + readonly attribute DOMString presentationId; + + // The control channel for this session. + // Should only use this channel to complete session termination. + readonly attribute nsIPresentationControlChannel controlChannel; + + // True if termination is initiated by receiver. + readonly attribute boolean isFromReceiver; +};
--- a/dom/presentation/ipc/PresentationIPCService.cpp +++ b/dom/presentation/ipc/PresentationIPCService.cpp @@ -3,16 +3,17 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/PPresentation.h" #include "mozilla/ipc/InputStreamUtils.h" #include "mozilla/ipc/URIUtils.h" +#include "nsGlobalWindow.h" #include "nsIPresentationListener.h" #include "PresentationCallbacks.h" #include "PresentationChild.h" #include "PresentationContentSessionInfo.h" #include "PresentationIPCService.h" using namespace mozilla; using namespace mozilla::dom; @@ -336,16 +337,28 @@ PresentationIPCService::NotifyReceiverRe mCallback = nullptr; return NS_OK; } NS_IMETHODIMP PresentationIPCService::UntrackSessionInfo(const nsAString& aSessionId, uint8_t aRole) { + if (nsIPresentationService::ROLE_RECEIVER == aRole) { + // Terminate receiver page. + uint64_t windowId; + if (NS_SUCCEEDED(GetWindowIdBySessionIdInternal(aSessionId, &windowId))) { + NS_DispatchToMainThread(NS_NewRunnableFunction([windowId]() -> void { + if (auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId)) { + window->Close(); + } + })); + } + } + // Remove the OOP responding info (if it has never been used). RemoveRespondingSessionId(aSessionId); if (mSessionInfos.Contains(aSessionId)) { mSessionInfos.Remove(aSessionId); } return NS_OK; }
--- a/dom/presentation/moz.build +++ b/dom/presentation/moz.build @@ -47,16 +47,17 @@ UNIFIED_SOURCES += [ 'PresentationDeviceManager.cpp', 'PresentationReceiver.cpp', 'PresentationRequest.cpp', 'PresentationService.cpp', 'PresentationServiceBase.cpp', 'PresentationSessionInfo.cpp', 'PresentationSessionRequest.cpp', 'PresentationTCPSessionTransport.cpp', + 'PresentationTerminateRequest.cpp', ] EXTRA_COMPONENTS += [ 'PresentationDataChannelSessionTransport.js', 'PresentationDataChannelSessionTransport.manifest', 'PresentationDeviceInfoManager.js', 'PresentationDeviceInfoManager.manifest', ]
--- a/dom/presentation/provider/ControllerStateMachine.jsm +++ b/dom/presentation/provider/ControllerStateMachine.jsm @@ -45,16 +45,22 @@ var handlers = [ switch (command.type) { case CommandType.DISCONNECT: stateMachine.state = State.CLOSED; stateMachine._notifyDisconnected(command.reason); break; case CommandType.LAUNCH_ACK: stateMachine._notifyLaunch(command.presentationId); break; + case CommandType.TERMINATE: + stateMachine._notifyTerminate(command.presentationId); + break; + case CommandType.TERMINATE_ACK: + stateMachine._notifyTerminate(command.presentationId); + break; case CommandType.ANSWER: case CommandType.ICE_CANDIDATE: stateMachine._notifyChannelDescriptor(command); break; default: debug("unexpected command: " + JSON.stringify(command)); // ignore unexpected command. break; @@ -82,16 +88,34 @@ ControllerStateMachine.prototype = { this._sendCommand({ type: CommandType.LAUNCH, presentationId: presentationId, url: url, }); } }, + terminate: function _terminate(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE, + presentationId: presentationId, + }); + } + }, + + terminateAck: function _terminateAck(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE_ACK, + presentationId: presentationId, + }); + } + }, + sendOffer: function _sendOffer(offer) { if (this.state === State.CONNECTED) { this._sendCommand({ type: CommandType.OFFER, offer: offer, }); } }, @@ -167,16 +191,20 @@ ControllerStateMachine.prototype = { _notifyDisconnected: function _notifyDisconnected(reason) { this._channel.notifyDisconnected(reason); }, _notifyLaunch: function _notifyLaunch(presentationId) { this._channel.notifyLaunch(presentationId); }, + _notifyTerminate: function _notifyTerminate(presentationId) { + this._channel.notifyTerminate(presentationId); + }, + _notifyChannelDescriptor: function _notifyChannelDescriptor(command) { switch (command.type) { case CommandType.ANSWER: this._channel.notifyAnswer(command.answer); break; case CommandType.ICE_CANDIDATE: this._channel.notifyIceCandidate(command.candidate); break;
--- a/dom/presentation/provider/DisplayDeviceProvider.cpp +++ b/dom/presentation/provider/DisplayDeviceProvider.cpp @@ -400,16 +400,47 @@ DisplayDeviceProvider::OnSessionRequest( aControlChannel); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } +NS_IMETHODIMP +DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aDeviceInfo); + MOZ_ASSERT(aControlChannel); + + nsresult rv; + + nsCOMPtr<nsIPresentationDeviceListener> listener; + rv = GetListener(getter_AddRefs(listener)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(!listener); + + rv = listener->OnTerminateRequest(mDevice, + aPresentationId, + aControlChannel, + aIsFromReceiver); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + // nsIObserver NS_IMETHODIMP DisplayDeviceProvider::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) { nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject);
--- a/dom/presentation/provider/LegacyPresentationControlService.js +++ b/dom/presentation/provider/LegacyPresentationControlService.js @@ -226,16 +226,22 @@ LegacyTCPControlChannel.prototype = { launch: function(aPresentationId, aUrl) { this._presentationId = aPresentationId; this._url = aUrl; this._sendInit(); }, + terminate: function() { + // Legacy protocol doesn't support extra terminate protocol. + // Trigger error handling for browser to shutdown all the resource locally. + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + sendOffer: function(aOffer) { let msg = { type: "requestSession:Offer", presentationId: this._presentationId, offer: discriptionAsJson(aOffer), }; this._sendMessage(msg); },
--- a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp +++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp @@ -912,16 +912,60 @@ MulticastDNSDeviceProvider::OnSessionReq if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { Unused << listener->OnSessionRequest(device, aUrl, aPresentationId, aControlChannel); } return NS_OK; } +NS_IMETHODIMP +MulticastDNSDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo, + const nsAString& aPresentationId, + nsIPresentationControlChannel* aControlChannel, + bool aIsFromReceiver) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString address; + Unused << aDeviceInfo->GetAddress(address); + + LOG_I("OnTerminateRequest: %s", address.get()); + + RefPtr<Device> device; + uint32_t index; + if (FindDeviceByAddress(address, index)) { + device = mDevices[index]; + } else { + // Create a one-time device object for non-discoverable controller. + // This device will not be in the list of available devices and cannot + // be used for requesting session. + nsAutoCString id; + Unused << aDeviceInfo->GetId(id); + uint16_t port; + Unused << aDeviceInfo->GetPort(&port); + + device = new Device(id, + /* aName = */ id, + /* aType = */ EmptyCString(), + address, + port, + DeviceState::eActive, + /* aProvider = */ nullptr); + } + + nsCOMPtr<nsIPresentationDeviceListener> listener; + if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) { + Unused << listener->OnTerminateRequest(device, aPresentationId, + aControlChannel, aIsFromReceiver); + } + + return NS_OK; +} + // nsIObserver NS_IMETHODIMP MulticastDNSDeviceProvider::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { MOZ_ASSERT(NS_IsMainThread());
--- a/dom/presentation/provider/PresentationControlService.js +++ b/dom/presentation/provider/PresentationControlService.js @@ -161,23 +161,43 @@ PresentationControlService.prototype = { aDeviceInfo, "receiver"); }, // Triggered by TCPControlChannel onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) { DEBUG && log("PresentationControlService - onSessionRequest: " + aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line + if (!this.listener) { + this.releaseControlChannel(aControlChannel); + return; + } + this.listener.onSessionRequest(aDeviceInfo, aUrl, aPresentationId, aControlChannel); this.releaseControlChannel(aControlChannel); }, + onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel, aIsFromReceiver) { + DEBUG && log("TCPPresentationServer - onSessionTerminate: " + + aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line + if (!this.listener) { + this.releaseControlChannel(aControlChannel); + return; + } + + this.listener.onTerminateRequest(aDeviceInfo, + aPresentationId, + aControlChannel, + aIsFromReceiver); + this.releaseControlChannel(aControlChannel); + }, + // nsIServerSocketListener (Triggered by nsIServerSocket.init) onSocketAccepted: function(aServerSocket, aClientSocket) { DEBUG && log("PresentationControlService - onSocketAccepted: " + aClientSocket.host + ":" + aClientSocket.port); // jshint ignore:line let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port); this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket)); }, @@ -385,16 +405,26 @@ TCPControlChannel.prototype = { sendIceCandidate: function(aCandidate) { this._stateMachine.updateIceCandidate(aCandidate); }, launch: function(aPresentationId, aUrl) { this._stateMachine.launch(aPresentationId, aUrl); }, + terminate: function(aPresentationId) { + if (!this._terminatingId) { + this._terminatingId = aPresentationId; + this._stateMachine.terminate(aPresentationId); + } else { + this._stateMachine.terminateAck(aPresentationId); + delete this._terminatingId; + } + }, + // may throw an exception _send: function(aMsg) { DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); // jshint ignore:line /** * XXX In TCP streaming, it is possible that more than one message in one * TCP packet. We use line delimited JSON to identify where one JSON encoded * object ends and the next begins. Therefore, we do not allow newline @@ -645,16 +675,36 @@ TCPControlChannel.prototype = { this._presentationService.onSessionRequest(this._deviceInfo, url, presentationId, this); break; } }, + notifyTerminate: function(presentationId) { + if (!this._terminatingId) { + this._terminatingId = presentationId; + this._presentationService.onSessionTerminate(this._deviceInfo, + presentationId, + this, + this._direction === "sender"); + return; + } + + if (this._terminatingId !== presentationId) { + // Requested presentation Id doesn't matched with the one in ACK. + // Disconnect the control channel with error. + DEBUG && log("TCPControlChannel - unmatched terminatingId: " + presentationId); // jshint ignore:line + this.disconnect(Cr.NS_ERROR_FAILURE); + } + + delete this._terminatingId; + }, + notifyOffer: function(offer) { this._onOffer(offer); }, notifyAnswer: function(answer) { this._onAnswer(answer); },
--- a/dom/presentation/provider/ReceiverStateMachine.jsm +++ b/dom/presentation/provider/ReceiverStateMachine.jsm @@ -53,16 +53,22 @@ var handlers = [ case CommandType.LAUNCH: stateMachine._notifyLaunch(command.presentationId, command.url); stateMachine._sendCommand({ type: CommandType.LAUNCH_ACK, presentationId: command.presentationId }); break; + case CommandType.TERMINATE: + stateMachine._notifyTerminate(command.presentationId); + break; + case CommandType.TERMINATE_ACK: + stateMachine._notifyTerminate(command.presentationId); + break; case CommandType.OFFER: case CommandType.ICE_CANDIDATE: stateMachine._notifyChannelDescriptor(command); break; default: debug("unexpected command: " + JSON.stringify(command)); // ignore unexpected command break; @@ -84,16 +90,34 @@ function ReceiverStateMachine(channel) { } ReceiverStateMachine.prototype = { launch: function _launch() { // presentation session can only be launched by controlling UA. debug("receiver shouldn't trigger launch"); }, + terminate: function _terminate(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE, + presentationId: presentationId, + }); + } + }, + + terminateAck: function _terminateAck(presentationId) { + if (this.state === State.CONNECTED) { + this._sendCommand({ + type: CommandType.TERMINATE_ACK, + presentationId: presentationId, + }); + } + }, + sendOffer: function _sendOffer() { // offer can only be sent by controlling UA. debug("receiver shouldn't generate offer"); }, sendAnswer: function _sendAnswer(answer) { if (this.state === State.CONNECTED) { this._sendCommand({ @@ -166,16 +190,20 @@ ReceiverStateMachine.prototype = { _notifyDisconnected: function _notifyDisconnected(reason) { this._channel.notifyDisconnected(reason); }, _notifyLaunch: function _notifyLaunch(presentationId, url) { this._channel.notifyLaunch(presentationId, url); }, + _notifyTerminate: function _notifyTerminate(presentationId) { + this._channel.notifyTerminate(presentationId); + }, + _notifyChannelDescriptor: function _notifyChannelDescriptor(command) { switch (command.type) { case CommandType.OFFER: this._channel.notifyOffer(command.offer); break; case CommandType.ICE_CANDIDATE: this._channel.notifyIceCandidate(command.candidate); break;
--- a/dom/presentation/provider/StateMachineHelper.jsm +++ b/dom/presentation/provider/StateMachineHelper.jsm @@ -20,16 +20,18 @@ const State = Object.freeze({ const CommandType = Object.freeze({ // control channel life cycle CONNECT: "connect", // { deviceId: <string> } CONNECT_ACK: "connect-ack", // { presentationId: <string> } DISCONNECT: "disconnect", // { reason: <int> } // presentation session life cycle LAUNCH: "launch", // { presentationId: <string>, url: <string> } LAUNCH_ACK: "launch-ack", // { presentationId: <string> } + TERMINATE: "terminate", // { presentationId: <string> } + TERMINATE_ACK: "terminate-ack", // { presentationId: <string> } // session transport establishment OFFER: "offer", // { offer: <json> } ANSWER: "answer", // { answer: <json> } ICE_CANDIDATE: "ice-candidate", // { candidate: <string> } }); this.State = State; // jshint ignore:line this.CommandType = CommandType; // jshint ignore:line
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js +++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js @@ -40,17 +40,17 @@ function registerMockedFactory(contractI function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) { if (originalFactory) { var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); registrar.unregisterFactory(mockedClassId, mockedFactory); registrar.registerFactory(originalClassId, "", contractId, originalFactory); } } -const sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString(); +var sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString(); const address = Cc["@mozilla.org/supports-cstring;1"] .createInstance(Ci.nsISupportsCString); address.data = "127.0.0.1"; const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); addresses.appendElement(address, false); const mockedChannelDescription = { @@ -137,16 +137,20 @@ const mockedControlChannel = { isValid = false; } } else if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL) { isValid = (aSDP.dataChannelSDP == "test-sdp"); } return isValid; }, launch: function(presentationId, url) { + sessionId = presentationId; + }, + terminate: function(presentationId) { + sendAsyncMessage('sender-terminate', presentationId); }, disconnect: function(reason) { sendAsyncMessage('control-channel-closed', reason); this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyDisconnected(reason); }, simulateReceiverReady: function() { this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady(); }, @@ -387,16 +391,23 @@ addMessageListener('trigger-device-promp addMessageListener('trigger-incoming-session-request', function(url) { var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] .getService(Ci.nsIPresentationDeviceManager); deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) .onSessionRequest(mockedDevice, url, sessionId, mockedControlChannel); }); +addMessageListener('trigger-incoming-terminate-request', function() { + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onTerminateRequest(mockedDevice, sessionId, mockedControlChannel, true); +}); + addMessageListener('trigger-incoming-offer', function() { mockedControlChannel.simulateOnOffer(); }); addMessageListener('trigger-incoming-answer', function() { mockedControlChannel.simulateOnAnswer(); });
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js +++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js @@ -12,17 +12,17 @@ Cu.import('resource://gre/modules/XPCOMU const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] .getService(Ci.nsIUUIDGenerator); function debug(str) { // dump('DEBUG -*- PresentationSessionChromeScript1UA -*-: ' + str + '\n'); } const originalFactoryData = []; -const sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString(); +var sessionId; // Store the uuid generated by PresentationRequest. const address = Cc["@mozilla.org/supports-cstring;1"] .createInstance(Ci.nsISupportsCString); address.data = "127.0.0.1"; const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); addresses.appendElement(address, false); function mockChannelDescription(role) { this.QueryInterface = XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]); @@ -179,24 +179,31 @@ const mockControlChannelOfSender = { sendAsyncMessage('offer-sent'); }, onAnswer: function(answer) { this._listener .QueryInterface(Ci.nsIPresentationControlChannelListener) .onAnswer(answer); }, launch: function(presentationId, url) { + sessionId = presentationId; sendAsyncMessage('sender-launch', url); }, disconnect: function(reason) { + if (!this._listener) { + return; + } this._listener .QueryInterface(Ci.nsIPresentationControlChannelListener) .notifyDisconnected(reason); mockControlChannelOfReceiver.disconnect(); - } + }, + terminate: function(presentationId) { + sendAsyncMessage('sender-terminate'); + }, }; // control channel of receiver const mockControlChannelOfReceiver = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]), set listener(listener) { // PresentationPresentingInfo::SetControlChannel if (listener) { @@ -231,20 +238,26 @@ const mockControlChannelOfReceiver = { }, sendAnswer: function(answer) { this._listener .QueryInterface(Ci.nsIPresentationSessionTransportCallback) .notifyTransportReady(); sendAsyncMessage('answer-sent'); }, disconnect: function(reason) { + if (!this._listener) { + return; + } + this._listener .QueryInterface(Ci.nsIPresentationControlChannelListener) .notifyDisconnected(reason); sendAsyncMessage('control-channel-receiver-closed', reason); + }, + terminate: function(presentaionId) { } }; const mockDevice = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), id: 'id', name: 'name', type: 'type', @@ -362,16 +375,27 @@ function initMockAndListener() { .getService(Ci.nsIPresentationDeviceManager); deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) .onSessionRequest(mockDevice, url, sessionId, mockControlChannelOfReceiver); }); + addMessageListener('trigger-on-terminate-request', function() { + debug('Got message: trigger-on-terminate-request'); + var deviceManager = Cc['@mozilla.org/presentation-device/manager;1'] + .getService(Ci.nsIPresentationDeviceManager); + deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onTerminateRequest(mockDevice, + sessionId, + mockControlChannelOfReceiver, + false); + }); + addMessageListener('trigger-control-channel-open', function(reason) { debug('Got message: trigger-control-channel-open'); mockControlChannelOfSender.notifyConnected(); mockControlChannelOfReceiver.notifyConnected(); }); addMessageListener('trigger-on-offer', function() { debug('Got message: trigger-on-offer');
--- a/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html +++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html @@ -106,28 +106,28 @@ function testSendMessage() { } function testConnectionClosed() { return new Promise(function(aResolve, aReject) { info('Receiver: --- testConnectionClosed ---'); connection.onclose = function() { connection.onclose = null; is(connection.state, "closed", "Receiver: Connection should be closed."); + command('forward-command', JSON.stringify({ name: 'receiver-closed' })); aResolve(); }; command('forward-command', JSON.stringify({ name: 'ready-to-close' })); }); } function runTests() { testConnectionAvailable() .then(testConnectionReady) .then(testIncomingMessage) .then(testSendMessage) - .then(testConnectionClosed) - .then(finish); + .then(testConnectionClosed); } runTests(); </script> </body> </html>
new file mode 100644 --- /dev/null +++ b/dom/presentation/tests/mochitest/file_presentation_terminate.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: --> +<html> + <head> + <meta charset='utf-8'> + <title>Test for B2G PresentationReceiver at receiver side</title> + </head> + <body> + <div id='content'></div> +<script type='application/javascript;version=1.7'> + +'use strict'; + +function is(a, b, msg) { + if (a === b) { + alert('OK ' + msg); + } else { + alert('KO ' + msg + ' | reason: ' + a + ' != ' + b); + } +} + +function ok(a, msg) { + alert((a ? 'OK ' : 'KO ') + msg); +} + +function info(msg) { + alert('INFO ' + msg); +} + +function command(name, data) { + alert('COMMAND ' + JSON.stringify({name: name, data: data})); +} + +function finish() { + alert('DONE'); +} + +var connection; + +function testConnectionAvailable() { + return new Promise(function(aResolve, aReject) { + info('Receiver: --- testConnectionAvailable ---'); + ok(navigator.presentation, 'Receiver: navigator.presentation should be available.'); + ok(navigator.presentation.receiver, 'Receiver: navigator.presentation.receiver should be available.'); + + navigator.presentation.receiver.connectionList + .then((aList) => { + is(aList.connections.length, 1, 'Should get one conncetion.'); + connection = aList.connections[0]; + ok(connection.id, 'Connection ID should be set: ' + connection.id); + is(connection.state, 'connected', 'Connection state at receiver side should be connected.'); + aResolve(); + }) + .catch((aError) => { + ok(false, 'Receiver: Error occurred when getting the connection: ' + aError); + finish(); + aReject(); + }); + }); +} + +function testConnectionReady() { + return new Promise(function(aResolve, aReject) { + info('Receiver: --- testConnectionReady ---'); + connection.onconnect = function() { + connection.onconnect = null; + ok(false, 'Should not get |onconnect| event.') + aReject(); + }; + if (connection.state === 'connected') { + connection.onconnect = null; + is(connection.state, 'connected', 'Receiver: Connection state should become connected.'); + aResolve(); + } + }); +} + +function testConnectionTerminate() { + return new Promise(function(aResolve, aReject) { + info('Receiver: --- testConnectionTerminate ---'); + connection.onterminate = function() { + connection.onterminate = null; + // Using window.alert at this stage will cause window.close() fail. + // Only trigger it if verdict fail. + if (connection.state !== 'terminated') { + is(connection.state, 'terminated', 'Receiver: Connection should be terminated.'); + } + aResolve(); + }; + command('forward-command', JSON.stringify({ name: 'ready-to-terminate' })); + }); +} + +function runTests() { + testConnectionAvailable() + .then(testConnectionReady) + .then(testConnectionTerminate) +} + +runTests(); + +</script> + </body> +</html>
--- a/dom/presentation/tests/mochitest/mochitest.ini +++ b/dom/presentation/tests/mochitest/mochitest.ini @@ -10,16 +10,18 @@ support-files = file_presentation_receiver.html file_presentation_receiver_establish_connection_error.html file_presentation_receiver_inner_iframe.html file_presentation_1ua_wentaway.html test_presentation_1ua_connection_wentaway.js file_presentation_receiver_auxiliary_navigation.html test_presentation_receiver_auxiliary_navigation.js file_presentation_sandboxed_presentation.html + file_presentation_terminate.html + test_presentation_terminate.js [test_presentation_dc_sender.html] [test_presentation_dc_receiver.html] skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785 [test_presentation_dc_receiver_oop.html] skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785 [test_presentation_1ua_sender_and_receiver.html] skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785 @@ -41,9 +43,15 @@ skip-if = (e10s || toolkit == 'gonk' || [test_presentation_tcp_receiver.html] skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785 [test_presentation_tcp_receiver_oop.html] skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785 [test_presentation_receiver_auxiliary_navigation_inproc.html] skip-if = (e10s || toolkit == 'gonk') [test_presentation_receiver_auxiliary_navigation_oop.html] skip-if = (e10s || toolkit == 'gonk') +[test_presentation_terminate_inproc.html] +skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') +[test_presentation_terminate_oop.html] +skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') +[test_presentation_sender_on_terminate_request.html] +skip-if = toolkit == 'android' [test_presentation_sandboxed_presentation.html]
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js @@ -1,11 +1,12 @@ 'use strict'; SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event'); function debug(str) { // info(str); } var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js')); var receiverUrl = SimpleTest.getTestFileURL('file_presentation_1ua_wentaway.html'); var request; @@ -139,17 +140,21 @@ function testStartConnection() { } function testConnectionWentaway() { return new Promise(function(aResolve, aReject) { info('Sender: --- testConnectionWentaway ---'); connection.onclose = function() { connection.onclose = null; is(connection.state, "closed", "Sender: Connection should be closed."); - aResolve(); + receiverIframe.addEventListener('mozbrowserclose', function closeHandler() { + ok(false, 'wentaway should not trigger receiver close'); + aResolve(); + }); + setTimeout(aResolve, 3000); }; gScript.addMessageListener('ready-to-remove-receiverFrame', function onReadyToRemove() { gScript.removeMessageListener('ready-to-remove-receiverFrame', onReadyToRemove); receiverIframe.src = "http://example.com"; }); }); }
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html @@ -65,17 +65,16 @@ function setup() { } else if (/^INFO /.exec(message)) { info(message.replace(/^INFO /, "")); } else if (/^COMMAND /.exec(message)) { var command = JSON.parse(message.replace(/^COMMAND /, "")); gScript.sendAsyncMessage(command.name, command.data); } else if (/^DONE$/.exec(message)) { receiverIframe.removeEventListener("mozbrowsershowmodalprompt", receiverListener); - teardown(); } }, false); var promise = new Promise(function(aResolve, aReject) { document.body.appendChild(receiverIframe); aResolve(receiverIframe); }); @@ -173,28 +172,65 @@ function testIncomingMessage() { postMessageToIframe('message-from-receiver-received'); aResolve(); }); postMessageToIframe('trigger-message-from-receiver'); }); } function testCloseConnection() { - return new Promise(function(aResolve, aReject) { - info('Sender: --- testCloseConnection ---'); - connection.onclose = function() { - connection.onclose = null; - is(connection.state, "closed", "Sender: Connection should be closed."); - aResolve(); - }; - gScript.addMessageListener('ready-to-close', function onReadyToClose() { - gScript.removeMessageListener('ready-to-close', onReadyToClose); - connection.close(); + info('Sender: --- testCloseConnection ---'); + gScript.addMessageListener('ready-to-close', function onReadyToClose() { + gScript.removeMessageListener('ready-to-close', onReadyToClose); + connection.close(); + + // Test terminate immediate after close. + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + ok(false, "terminate after close should do nothing"); }); + connection.terminate(); }); + + return Promise.all([ + new Promise(function(aResolve, aReject) { + connection.onclose = function() { + connection.onclose = null; + is(connection.state, 'closed', 'Sender: Connection should be closed.'); + aResolve(); + }; + }), + new Promise(function(aResolve, aReject) { + gScript.addMessageListener('receiver-closed', function onReceiverClosed() { + gScript.removeMessageListener('receiver-closed', onReceiverClosed); + aResolve(); + }); + }), + ]); +} + +function testTerminateAfterClose() { + info('Sender: --- testTerminateAfterClose ---'); + return Promise.race([ + new Promise(function(aResolve, aReject) { + connection.onterminate = function() { + connection.onterminate = null; + ok(false, 'terminate after close should do nothing'); + aResolve(); + }; + connection.terminate(); + }), + new Promise(function(aResolve, aReject) { + setTimeout(function() { + is(connection.state, 'closed', 'Sender: Connection should be closed.'); + aResolve(); + }, 3000); + }), + ]); } function teardown() { gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() { debug('Got message: teardown-complete'); gScript.removeMessageListener('teardown-complete', teardownCompleteHandler); gScript.destroy(); SimpleTest.finish(); @@ -203,20 +239,23 @@ function teardown() { gScript.sendAsyncMessage('teardown'); } function runTests() { setup().then(testCreateRequest) .then(testStartConnection) .then(testSendMessage) .then(testIncomingMessage) - .then(testCloseConnection); + .then(testCloseConnection) + .then(testTerminateAfterClose) + .then(teardown); } SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event'); SpecialPowers.pushPermissions([ {type: 'presentation-device-manage', allow: false, context: document}, {type: 'presentation', allow: true, context: document}, {type: "browser", allow: true, context: document}, ], () => { SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true], /* Mocked TCP session transport builder in the test */ ["dom.presentation.session_transport.data_channel.enable", false],
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html +++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html @@ -69,17 +69,16 @@ function setup() { } else if (/^INFO /.exec(message)) { info(message.replace(/^INFO /, "")); } else if (/^COMMAND /.exec(message)) { var command = JSON.parse(message.replace(/^COMMAND /, "")); gScript.sendAsyncMessage(command.name, command.data); } else if (/^DONE$/.exec(message)) { receiverIframe.removeEventListener("mozbrowsershowmodalprompt", receiverListener); - teardown(); } }, false); var promise = new Promise(function(aResolve, aReject) { document.body.appendChild(receiverIframe); aResolve(receiverIframe); }); @@ -180,28 +179,65 @@ function testIncomingMessage() { postMessageToIframe('message-from-receiver-received'); aResolve(); }); postMessageToIframe('trigger-message-from-receiver'); }); } function testCloseConnection() { - return new Promise(function(aResolve, aReject) { - info('Sender: --- testCloseConnection ---'); - connection.onclose = function() { - connection.onclose = null; - is(connection.state, "closed", "Sender: Connection should be closed."); - aResolve(); - }; - gScript.addMessageListener('ready-to-close', function onReadyToClose() { - gScript.removeMessageListener('ready-to-close', onReadyToClose); - connection.close(); + info('Sender: --- testCloseConnection ---'); + gScript.addMessageListener('ready-to-close', function onReadyToClose() { + gScript.removeMessageListener('ready-to-close', onReadyToClose); + connection.close(); + + // Test terminate immediate after close. + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + ok(false, 'terminate after close should do nothing'); }); + connection.terminate(); }); + + return Promise.all([ + new Promise(function(aResolve, aReject) { + connection.onclose = function() { + connection.onclose = null; + is(connection.state, 'closed', 'Sender: Connection should be closed.'); + aResolve(); + }; + }), + new Promise(function(aResolve, aReject) { + gScript.addMessageListener('receiver-closed', function onReceiverClosed() { + gScript.removeMessageListener('receiver-closed', onReceiverClosed); + aResolve(); + }); + }), + ]); +} + +function testTerminateAfterClose() { + info('Sender: --- testTerminateAfterClose ---'); + return Promise.race([ + new Promise(function(aResolve, aReject) { + connection.onterminate = function() { + connection.onterminate = null; + ok(false, 'terminate at closed state should do nothing'); + aResolve(); + }; + connection.terminate(); + }), + new Promise(function(aResolve, aReject) { + setTimeout(function() { + is(connection.state, 'closed', 'Sender: Connection should be closed.'); + aResolve(); + }, 3000); + }), + ]); } function teardown() { gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() { gScript.removeMessageListener('teardown-complete', teardownCompleteHandler); gScript.destroy(); SimpleTest.finish(); }); @@ -210,20 +246,23 @@ function teardown() { } function runTests() { setup() .then(testCreateRequest) .then(testStartConnection) .then(testSendMessage) .then(testIncomingMessage) - .then(testCloseConnection); + .then(testCloseConnection) + .then(testTerminateAfterClose) + .then(teardown); } SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event'); SpecialPowers.pushPermissions([ {type: 'presentation-device-manage', allow: false, context: document}, {type: 'presentation', allow: true, context: document}, {type: "browser", allow: true, context: document}, ], () => { SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true], /* Mocked TCP session transport builder in the test */ ["dom.presentation.session_transport.data_channel.enable", false],
new file mode 100644 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html @@ -0,0 +1,185 @@ +<!DOCTYPE HTML> +<html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> + <meta charset="utf-8"> + <title>Test onTerminateRequest at sender side</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276378">Test onTerminateRequest at sender side</a> +<script type="application/javascript;version=1.8"> + +'use strict'; + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js')); +var request; +var connection; + +function testSetup() { + return new Promise(function(aResolve, aReject) { + request = new PresentationRequest("http://example.com"); + + request.getAvailability().then( + function(aAvailability) { + aAvailability.onchange = function() { + aAvailability.onchange = null; + ok(aAvailability.value, "Device should be available."); + aResolve(); + } + }, + function(aError) { + ok(false, "Error occurred when getting availability: " + aError); + teardown(); + aReject(); + } + ); + + gScript.sendAsyncMessage('trigger-device-add'); + }); +} + +function testStartConnection() { + return new Promise(function(aResolve, aReject) { + gScript.addMessageListener('device-prompt', function devicePromptHandler() { + gScript.removeMessageListener('device-prompt', devicePromptHandler); + info("Device prompt is triggered."); + gScript.sendAsyncMessage('trigger-device-prompt-select'); + }); + + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', controlChannelEstablishedHandler); + info("A control channel is established."); + gScript.sendAsyncMessage('trigger-control-channel-open'); + }); + + gScript.addMessageListener('control-channel-opened', function controlChannelOpenedHandler(aReason) { + gScript.removeMessageListener('control-channel-opened', controlChannelOpenedHandler); + info("The control channel is opened."); + }); + + gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) { + gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler); + info("The control channel is closed. " + aReason); + }); + + gScript.addMessageListener('offer-sent', function offerSentHandler(aIsValid) { + gScript.removeMessageListener('offer-sent', offerSentHandler); + ok(aIsValid, "A valid offer is sent out."); + gScript.sendAsyncMessage('trigger-incoming-transport'); + }); + + gScript.addMessageListener('answer-received', function answerReceivedHandler() { + gScript.removeMessageListener('answer-received', answerReceivedHandler); + info("An answer is received."); + }); + + gScript.addMessageListener('data-transport-initialized', function dataTransportInitializedHandler() { + gScript.removeMessageListener('data-transport-initialized', dataTransportInitializedHandler); + info("Data transport channel is initialized."); + gScript.sendAsyncMessage('trigger-incoming-answer'); + }); + + gScript.addMessageListener('data-transport-notification-enabled', function dataTransportNotificationEnabledHandler() { + gScript.removeMessageListener('data-transport-notification-enabled', dataTransportNotificationEnabledHandler); + info("Data notification is enabled for data transport channel."); + }); + + var connectionFromEvent; + request.onconnectionavailable = function(aEvent) { + request.onconnectionavailable = null; + connectionFromEvent = aEvent.connection; + ok(connectionFromEvent, "|connectionavailable| event is fired with a connection."); + + if (connection) { + is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same."); + } + }; + + request.start().then( + function(aConnection) { + connection = aConnection; + ok(connection, "Connection should be available."); + ok(connection.id, "Connection ID should be set."); + is(connection.state, "connecting", "The initial state should be connecting."); + + if (connectionFromEvent) { + is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same."); + } + connection.onconnect = function() { + connection.onconnect = null; + is(connection.state, "connected", "Connection should be connected."); + aResolve(); + }; + }, + function(aError) { + ok(false, "Error occurred when establishing a connection: " + aError); + teardown(); + aReject(); + } + ); + }); +} + +function testOnTerminateRequest() { + return new Promise(function(aResolve, aReject) { + gScript.addMessageListener('control-channel-opened', function controlChannelOpenedHandler(aReason) { + gScript.removeMessageListener('control-channel-opened', controlChannelOpenedHandler); + info("The control channel is opened."); + }); + + gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) { + gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler); + info("The control channel is closed. " + aReason); + }); + + gScript.addMessageListener('data-transport-closed', function dataTransportClosedHandler(aReason) { + gScript.removeMessageListener('data-transport-closed', dataTransportClosedHandler); + info("The data transport is closed. " + aReason); + }); + + connection.onterminate = function() { + connection.onterminate = null; + is(connection.state, "terminated", "Connection should be closed."); + aResolve(); + }; + + gScript.sendAsyncMessage('trigger-incoming-terminate-request'); + gScript.sendAsyncMessage('trigger-control-channel-open'); + }); +} + +function teardown() { + gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() { + gScript.removeMessageListener('teardown-complete', teardownCompleteHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + + gScript.sendAsyncMessage('teardown'); +} + +function runTests() { + ok(window.PresentationRequest, "PresentationRequest should be available."); + + testSetup(). + then(testStartConnection). + then(testOnTerminateRequest). + then(teardown); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPermissions([ + {type: 'presentation-device-manage', allow: false, context: document}, + {type: 'presentation', allow: true, context: document}, +], function() { + SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true], + ["dom.presentation.session_transport.data_channel.enable", false]]}, + runTests); +}); + +</script> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js @@ -0,0 +1,241 @@ +'use strict'; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event'); + +function debug(str) { + // info(str); +} + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js')); +var receiverUrl = SimpleTest.getTestFileURL('file_presentation_terminate.html'); +var request; +var connection; +var receiverIframe; + +function setup() { + gScript.addMessageListener('device-prompt', function devicePromptHandler() { + debug('Got message: device-prompt'); + gScript.removeMessageListener('device-prompt', devicePromptHandler); + gScript.sendAsyncMessage('trigger-device-prompt-select'); + }); + + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + gScript.sendAsyncMessage('trigger-control-channel-open'); + }); + + gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) { + debug('Got message: sender-launch'); + gScript.removeMessageListener('sender-launch', senderLaunchHandler); + is(url, receiverUrl, 'Receiver: should receive the same url'); + receiverIframe = document.createElement('iframe'); + receiverIframe.setAttribute('mozbrowser', 'true'); + receiverIframe.setAttribute('mozpresentation', receiverUrl); + var oop = location.pathname.indexOf('_inproc') == -1; + receiverIframe.setAttribute('remote', oop); + + receiverIframe.setAttribute('src', receiverUrl); + receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() { + receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander); + info('Receiver loaded.'); + }); + + // This event is triggered when the iframe calls 'alert'. + receiverIframe.addEventListener('mozbrowsershowmodalprompt', function receiverListener(evt) { + var message = evt.detail.message; + if (/^OK /.exec(message)) { + ok(true, message.replace(/^OK /, '')); + } else if (/^KO /.exec(message)) { + ok(false, message.replace(/^KO /, '')); + } else if (/^INFO /.exec(message)) { + info(message.replace(/^INFO /, '')); + } else if (/^COMMAND /.exec(message)) { + var command = JSON.parse(message.replace(/^COMMAND /, '')); + gScript.sendAsyncMessage(command.name, command.data); + } else if (/^DONE$/.exec(message)) { + ok(true, 'Messaging from iframe complete.'); + receiverIframe.removeEventListener('mozbrowsershowmodalprompt', + receiverListener); + } + }, false); + + var promise = new Promise(function(aResolve, aReject) { + document.body.appendChild(receiverIframe); + aResolve(receiverIframe); + }); + + var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1'] + .getService(SpecialPowers.Ci.nsIObserverService); + obs.notifyObservers(promise, 'setup-request-promise', null); + }); + + gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() { + debug('Got message: promise-setup-ready'); + gScript.removeMessageListener('promise-setup-ready', + promiseSetupReadyHandler); + gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl); + }); + + gScript.addMessageListener('offer-sent', function offerSentHandler() { + debug('Got message: offer-sent'); + gScript.removeMessageListener('offer-sent', offerSentHandler); + gScript.sendAsyncMessage('trigger-on-offer'); + }); + + gScript.addMessageListener('answer-sent', function answerSentHandler() { + debug('Got message: answer-sent'); + gScript.removeMessageListener('answer-sent', answerSentHandler); + gScript.sendAsyncMessage('trigger-on-answer'); + }); + + return Promise.resolve(); +} + +function testCreateRequest() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testCreateRequest ---'); + request = new PresentationRequest(receiverUrl); + request.getAvailability().then((aAvailability) => { + aAvailability.onchange = function() { + aAvailability.onchange = null; + ok(aAvailability.value, 'Sender: Device should be available.'); + aResolve(); + } + }).catch((aError) => { + ok(false, 'Sender: Error occurred when getting availability: ' + aError); + teardown(); + aReject(); + }); + + gScript.sendAsyncMessage('trigger-device-add'); + }); +} + +function testStartConnection() { + return new Promise(function(aResolve, aReject) { + request.start().then((aConnection) => { + connection = aConnection; + ok(connection, 'Sender: Connection should be available.'); + ok(connection.id, 'Sender: Connection ID should be set.'); + is(connection.state, 'connecting', 'Sender: The initial state should be connecting.'); + connection.onconnect = function() { + connection.onconnect = null; + is(connection.state, 'connected', 'Connection should be connected.'); + aResolve(); + }; + + info('Sender: test terminate at connecting state'); + connection.onterminate = function() { + connection.onterminate = null; + ok(false, 'Should not be able to terminate at connecting state'); + aReject(); + } + connection.terminate(); + }).catch((aError) => { + ok(false, 'Sender: Error occurred when establishing a connection: ' + aError); + teardown(); + aReject(); + }); + }); +} + +function testConnectionTerminate() { + return new Promise(function(aResolve, aReject) { + info('Sender: --- testConnectionTerminate---'); + connection.onterminate = function() { + connection.onterminate = null; + is(connection.state, 'terminated', 'Sender: Connection should be terminated.'); + }; + gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() { + gScript.removeMessageListener('control-channel-established', + controlChannelEstablishedHandler); + gScript.sendAsyncMessage('trigger-control-channel-open'); + }); + gScript.addMessageListener('sender-terminate', function senderTerminateHandler() { + gScript.removeMessageListener('sender-terminate', + senderTerminateHandler); + + receiverIframe.addEventListener('mozbrowserclose', function() { + ok(true, 'observe receiver page closing'); + aResolve(); + }); + + gScript.sendAsyncMessage('trigger-on-terminate-request'); + }); + gScript.addMessageListener('ready-to-terminate', function onReadyToTerminate() { + gScript.removeMessageListener('ready-to-terminate', onReadyToTerminate); + connection.terminate(); + + // test unexpected close right after terminate + connection.onclose = function() { + ok(false, 'close after terminate should do nothing'); + }; + connection.close(); + }); + }); +} + +function testSendAfterTerminate() { + return new Promise(function(aResolve, aReject) { + try { + connection.send('something'); + ok(false, 'PresentationConnection.send should be failed'); + } catch (e) { + is(e.name, 'InvalidStateError', 'Must throw InvalidStateError'); + } + aResolve(); + }); +} + +function testCloseAfterTerminate() { + return Promise.race([ + new Promise(function(aResolve, aReject) { + connection.onclose = function() { + connection.onclose = null; + ok(false, 'close at terminated state should do nothing'); + aResolve(); + }; + connection.close(); + }), + new Promise(function(aResolve, aReject) { + setTimeout(function() { + is(connection.state, 'terminated', 'Sender: Connection should be terminated.'); + aResolve(); + }, 3000); + }), + ]); +} + +function teardown() { + gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() { + debug('Got message: teardown-complete'); + gScript.removeMessageListener('teardown-complete', teardownCompleteHandler); + gScript.destroy(); + SimpleTest.finish(); + }); + gScript.sendAsyncMessage('teardown'); +} + +function runTests() { + setup().then(testCreateRequest) + .then(testStartConnection) + .then(testConnectionTerminate) + .then(testSendAfterTerminate) + .then(testCloseAfterTerminate) + .then(teardown); +} + +SpecialPowers.pushPermissions([ + {type: 'presentation-device-manage', allow: false, context: document}, + {type: 'presentation', allow: true, context: document}, + {type: 'browser', allow: true, context: document}, +], () => { + SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true], + ['dom.presentation.test.enabled', true], + ['dom.mozBrowserFramesEnabled', true], + ['dom.ipc.tabs.disabled', false], + ['dom.presentation.test.stage', 0]]}, + runTests); +});
new file mode 100644 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: --> +<html> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <head> + <meta charset='utf-8'> + <title>Test for PresentationConnection.terminate()</title> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script> + </head> + <body> + <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'> + Test for PresentationConnection.terminate()</a> + <script type='application/javascript;version=1.8' src='test_presentation_terminate.js'> + </script> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: --> +<html> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <head> + <meta charset='utf-8'> + <title>Test for PresentationConnection.terminate()</title> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script> + </head> + <body> + <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'> + Test for PresentationConnection.terminate()</a> + <script type='application/javascript;version=1.8' src='test_presentation_terminate.js'> + </script> + </body> +</html>
--- a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js +++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js @@ -18,16 +18,17 @@ function TestPresentationDevice() {} function TestPresentationControlChannel() {} TestPresentationControlChannel.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]), sendOffer: function(offer) {}, sendAnswer: function(answer) {}, disconnect: function() {}, launch: function() {}, + terminate: function() {}, set listener(listener) {}, get listener() {}, }; var testProvider = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]), forceDiscovery: function() { @@ -134,16 +135,37 @@ function sessionRequest() { Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id'); run_next_test(); }, 'presentation-session-request', false); manager.QueryInterface(Ci.nsIPresentationDeviceListener) .onSessionRequest(testDevice, testUrl, testPresentationId, testControlChannel); } +function terminateRequest() { + let testUrl = 'http://www.example.org/'; + let testPresentationId = 'test-presentation-id'; + let testControlChannel = new TestPresentationControlChannel(); + let testIsFromReceiver = true; + Services.obs.addObserver(function observer(subject, topic, data) { + Services.obs.removeObserver(observer, topic); + + let request = subject.QueryInterface(Ci.nsIPresentationTerminateRequest); + + Assert.equal(request.device.id, testDevice.id, 'expected device'); + Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id'); + Assert.equal(request.isFromReceiver, testIsFromReceiver, 'expected isFromReceiver'); + + run_next_test(); + }, 'presentation-terminate-request', false); + manager.QueryInterface(Ci.nsIPresentationDeviceListener) + .onTerminateRequest(testDevice, testPresentationId, + testControlChannel, testIsFromReceiver); +} + function removeDevice() { Services.obs.addObserver(function observer(subject, topic, data) { Services.obs.removeObserver(observer, topic); let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice); Assert.equal(updatedDevice.id, testDevice.id, 'expected device id'); Assert.equal(updatedDevice.name, testDevice.name, 'expected device name'); Assert.equal(updatedDevice.type, testDevice.type, 'expected device type'); @@ -171,14 +193,15 @@ function removeProvider() { manager.removeDeviceProvider(testProvider); } add_test(addProvider); add_test(forceDiscovery); add_test(addDevice); add_test(updateDevice); add_test(sessionRequest); +add_test(terminateRequest); add_test(removeDevice); add_test(removeProvider); function run_test() { run_next_test(); }
--- a/dom/presentation/tests/xpcshell/test_presentation_state_machine.js +++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js @@ -73,16 +73,55 @@ function launch() { Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state'); Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack'); run_next_test(); }; }; } +function terminateByController() { + Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state'); + Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state'); + + controllerState.terminate(testPresentationId); + mockReceiverChannel.notifyTerminate = function(presentationId) { + Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state'); + Assert.equal(presentationId, testPresentationId, 'expected presentationId received'); + + mockControllerChannel.notifyTerminate = function(presentationId) { + Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state'); + Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack'); + + run_next_test(); + }; + + receiverState.terminateAck(presentationId); + }; +} + +function terminateByReceiver() { + Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state'); + Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state'); + + receiverState.terminate(testPresentationId); + mockControllerChannel.notifyTerminate = function(presentationId) { + Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state'); + Assert.equal(presentationId, testPresentationId, 'expected presentationId received'); + + mockReceiverChannel.notifyTerminate = function(presentationId) { + Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state'); + Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack'); + run_next_test(); + }; + + controllerState.terminateAck(presentationId); + }; +} + function exchangeSDP() { Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state'); Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state'); const testOffer = 'test-offer'; const testAnswer = 'test-answer'; const testIceCandidate = 'test-ice-candidate'; controllerState.sendOffer(testOffer); @@ -180,16 +219,18 @@ function abnormalDisconnect() { run_next_test(); }; controllerState.onChannelClosed(Cr.NS_OK, true); }; } add_test(connect); add_test(launch); +add_test(terminateByController); +add_test(terminateByReceiver); add_test(exchangeSDP); add_test(disconnect); add_test(receiverDisconnect); add_test(abnormalDisconnect); function run_test() { // jshint ignore:line run_next_test(); }
--- a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js +++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js @@ -176,16 +176,185 @@ function testPresentationServer() { this.status = 'closed'; Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify closed'); yayFuncs.presenterControlChannelClose(); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), }; } +function terminateRequest() { + let yayFuncs = makeJointSuccess(['controllerControlChannelConnected', + 'controllerControlChannelDisconnected', + 'presenterControlChannelDisconnected']); + let controllerControlChannel; + + pcs.listener = { + onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiverj) { + controllerControlChannel = controlChannel; + Assert.equal(deviceInfo.id, pcs.id, 'expected device id'); + Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address'); + Assert.equal(presentationId, 'testPresentationId', 'expected presentation id'); + Assert.equal(isFromReceiver, false, 'expected request from controller'); + + controllerControlChannel.listener = { + notifyConnected: function() { + Assert.ok(true, 'control channel notify connected'); + yayFuncs.controllerControlChannelConnected(); + }, + notifyDisconnected: function(aReason) { + Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted'); + yayFuncs.controllerControlChannelDisconnected(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), + }; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]), + }; + + let presenterDeviceInfo = { + id: 'presentatorID', + address: '127.0.0.1', + port: PRESENTER_CONTROL_CHANNEL_PORT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]), + }; + + let presenterControlChannel = pcs.connect(presenterDeviceInfo); + + presenterControlChannel.listener = { + notifyConnected: function() { + presenterControlChannel.terminate('testPresentationId', 'http://example.com'); + presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON); + }, + notifyDisconnected: function(aReason) { + Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected'); + yayFuncs.presenterControlChannelDisconnected(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), + }; +} + +function terminateRequest() { + let yayFuncs = makeJointSuccess(['controllerControlChannelConnected', + 'controllerControlChannelDisconnected', + 'presenterControlChannelDisconnected', + 'terminatedByController', + 'terminatedByReceiver']); + let controllerControlChannel; + let terminatePhase = 'controller'; + + pcs.listener = { + onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) { + Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address'); + Assert.equal(presentationId, 'testPresentationId', 'expected presentation id'); + controlChannel.terminate(presentationId); // Reply terminate ack. + + if (terminatePhase === 'controller') { + controllerControlChannel = controlChannel; + Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id'); + Assert.equal(isFromReceiver, false, 'expected request from controller'); + yayFuncs.terminatedByController(); + + controllerControlChannel.listener = { + notifyConnected: function() { + Assert.ok(true, 'control channel notify connected'); + yayFuncs.controllerControlChannelConnected(); + + terminatePhase = 'receiver'; + controllerControlChannel.terminate('testPresentationId'); + }, + notifyDisconnected: function(aReason) { + Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted'); + yayFuncs.controllerControlChannelDisconnected(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), + }; + } else { + Assert.equal(deviceInfo.id, presenterDeviceInfo.id, 'expected presenter device id'); + Assert.equal(isFromReceiver, true, 'expected request from receiver'); + yayFuncs.terminatedByReceiver(); + presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON); + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]), + }; + + let presenterDeviceInfo = { + id: 'presentatorID', + address: '127.0.0.1', + port: PRESENTER_CONTROL_CHANNEL_PORT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]), + }; + + let presenterControlChannel = pcs.connect(presenterDeviceInfo); + + presenterControlChannel.listener = { + notifyConnected: function() { + presenterControlChannel.terminate('testPresentationId'); + }, + notifyDisconnected: function(aReason) { + Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected'); + yayFuncs.presenterControlChannelDisconnected(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), + }; +} + +function terminateRequestAbnormal() { + let yayFuncs = makeJointSuccess(['controllerControlChannelConnected', + 'controllerControlChannelDisconnected', + 'presenterControlChannelDisconnected']); + let controllerControlChannel; + + pcs.listener = { + onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) { + Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id'); + Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address'); + Assert.equal(presentationId, 'testPresentationId', 'expected presentation id'); + Assert.equal(isFromReceiver, false, 'expected request from controller'); + controlChannel.terminate('unmatched-presentationId'); // Reply abnormal terminate ack. + + controllerControlChannel = controlChannel; + + controllerControlChannel.listener = { + notifyConnected: function() { + Assert.ok(true, 'control channel notify connected'); + yayFuncs.controllerControlChannelConnected(); + }, + notifyDisconnected: function(aReason) { + Assert.equal(aReason, Cr.NS_ERROR_FAILURE, 'controllerControlChannel notify disconncted with error'); + yayFuncs.controllerControlChannelDisconnected(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), + }; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]), + }; + + let presenterDeviceInfo = { + id: 'presentatorID', + address: '127.0.0.1', + port: PRESENTER_CONTROL_CHANNEL_PORT, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]), + }; + + let presenterControlChannel = pcs.connect(presenterDeviceInfo); + + presenterControlChannel.listener = { + notifyConnected: function() { + presenterControlChannel.terminate('testPresentationId'); + }, + notifyDisconnected: function(aReason) { + Assert.equal(aReason, Cr.NS_ERROR_FAILURE, '4. presenterControlChannel notify disconnected with error'); + yayFuncs.presenterControlChannelDisconnected(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]), + }; +} + function setOffline() { pcs.listener = { onPortChange: function(aPort) { Assert.notEqual(aPort, 0, 'TCPPresentationServer port changed and the port should be valid'); pcs.close(); run_next_test(); }, }; @@ -220,16 +389,18 @@ function shutdown() // Test manually close control channel with NS_ERROR_FAILURE function changeCloseReason() { CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_ERROR_FAILURE; run_next_test(); } add_test(loopOfferAnser); +add_test(terminateRequest); +add_test(terminateRequestAbnormal); add_test(setOffline); add_test(changeCloseReason); add_test(oneMoreLoop); add_test(shutdown); function run_test() { Services.prefs.setBoolPref("dom.presentation.tcp_server.debug", true);
--- a/dom/tests/mochitest/general/test_interfaces.html +++ b/dom/tests/mochitest/general/test_interfaces.html @@ -67,16 +67,17 @@ var ecmaGlobals = {name: "Intl", android: false}, "Iterator", "JSON", "Map", "Math", {name: "NaN", xbl: false}, "Number", "Object", + "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", {name: "SharedArrayBuffer", nightly: true}, {name: "SIMD", nightly: true}, @@ -957,18 +958,16 @@ var interfaceNamesInGlobalScope = {name: "PresentationSession", disabled: true, permission: ["presentation"]}, // IMPORTANT: Do not change this list without review from a DOM peer! {name: "PresentationSessionConnectEvent", disabled: true, permission: ["presentation"]}, // IMPORTANT: Do not change this list without review from a DOM peer! "ProcessingInstruction", // IMPORTANT: Do not change this list without review from a DOM peer! "ProgressEvent", // IMPORTANT: Do not change this list without review from a DOM peer! - "Promise", -// IMPORTANT: Do not change this list without review from a DOM peer! {name: "PushManager", b2g: false}, // IMPORTANT: Do not change this list without review from a DOM peer! {name: "PushSubscription", b2g: false}, // IMPORTANT: Do not change this list without review from a DOM peer! {name: "PushSubscriptionOptions", b2g: false}, // IMPORTANT: Do not change this list without review from a DOM peer! "RadioNodeList", // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/url/URL.cpp +++ b/dom/url/URL.cpp @@ -917,75 +917,79 @@ public: // This class creates a URL object on the main thread. class ConstructorRunnable : public WorkerMainThreadRunnable { private: const nsString mURL; nsString mBase; // IsVoid() if we have no base URI string. RefPtr<URLProxy> mBaseProxy; - ErrorResult& mRv; RefPtr<URLProxy> mRetval; public: ConstructorRunnable(WorkerPrivate* aWorkerPrivate, - const nsAString& aURL, const Optional<nsAString>& aBase, - ErrorResult& aRv) + const nsAString& aURL, const Optional<nsAString>& aBase) : WorkerMainThreadRunnable(aWorkerPrivate, NS_LITERAL_CSTRING("URL :: Constructor")) , mURL(aURL) - , mRv(aRv) { if (aBase.WasPassed()) { mBase = aBase.Value(); } else { mBase.SetIsVoid(true); } mWorkerPrivate->AssertIsOnWorkerThread(); } ConstructorRunnable(WorkerPrivate* aWorkerPrivate, - const nsAString& aURL, URLProxy* aBaseProxy, - ErrorResult& aRv) + const nsAString& aURL, URLProxy* aBaseProxy) : WorkerMainThreadRunnable(aWorkerPrivate, NS_LITERAL_CSTRING("URL :: Constructor with BaseURL")) , mURL(aURL) , mBaseProxy(aBaseProxy) - , mRv(aRv) { mBase.SetIsVoid(true); mWorkerPrivate->AssertIsOnWorkerThread(); } bool MainThreadRun() { AssertIsOnMainThread(); + ErrorResult rv; RefPtr<URLMainThread> url; if (mBaseProxy) { - url = URLMainThread::Constructor(nullptr, mURL, mBaseProxy->URI(), mRv); + url = URLMainThread::Constructor(nullptr, mURL, mBaseProxy->URI(), rv); } else if (!mBase.IsVoid()) { - url = URLMainThread::Constructor(nullptr, mURL, mBase, mRv); + url = URLMainThread::Constructor(nullptr, mURL, mBase, rv); } else { - url = URLMainThread::Constructor(nullptr, mURL, nullptr, mRv); + url = URLMainThread::Constructor(nullptr, mURL, nullptr, rv); } - if (mRv.Failed()) { + if (rv.Failed()) { + rv.SuppressException(); return true; } mRetval = new URLProxy(url.forget()); return true; } URLProxy* - GetURLProxy() + GetURLProxy(ErrorResult& aRv) const { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mRetval) { + aRv.ThrowTypeError<MSG_INVALID_URL>(mURL); + } + return mRetval; } }; class TeardownURLRunnable : public Runnable { public: explicit TeardownURLRunnable(URLProxy* aURLProxy) @@ -1206,19 +1210,18 @@ already_AddRefed<URLWorker> FinishConstructor(JSContext* aCx, WorkerPrivate* aPrivate, ConstructorRunnable* aRunnable, ErrorResult& aRv) { aRunnable->Dispatch(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } - RefPtr<URLProxy> proxy = aRunnable->GetURLProxy(); - if (NS_WARN_IF(!proxy)) { - aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + RefPtr<URLProxy> proxy = aRunnable->GetURLProxy(aRv); + if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr<URLWorker> url = new URLWorker(aPrivate, proxy); return url.forget(); } /* static */ already_AddRefed<URLWorker> @@ -1227,46 +1230,46 @@ URLWorker::Constructor(const GlobalObjec { MOZ_ASSERT(!NS_IsMainThread()); JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); URLWorker& base = static_cast<URLWorker&>(aBase); RefPtr<ConstructorRunnable> runnable = - new ConstructorRunnable(workerPrivate, aURL, base.GetURLProxy(), aRv); + new ConstructorRunnable(workerPrivate, aURL, base.GetURLProxy()); return FinishConstructor(cx, workerPrivate, runnable, aRv); } /* static */ already_AddRefed<URLWorker> URLWorker::Constructor(const GlobalObject& aGlobal, const nsAString& aURL, const Optional<nsAString>& aBase, ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); RefPtr<ConstructorRunnable> runnable = - new ConstructorRunnable(workerPrivate, aURL, aBase, aRv); + new ConstructorRunnable(workerPrivate, aURL, aBase); return FinishConstructor(cx, workerPrivate, runnable, aRv); } /* static */ already_AddRefed<URLWorker> URLWorker::Constructor(const GlobalObject& aGlobal, const nsAString& aURL, const nsAString& aBase, ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); Optional<nsAString> base; base = &aBase; RefPtr<ConstructorRunnable> runnable = - new ConstructorRunnable(workerPrivate, aURL, base, aRv); + new ConstructorRunnable(workerPrivate, aURL, base); return FinishConstructor(cx, workerPrivate, runnable, aRv); } /* static */ void URLWorker::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, const mozilla::dom::objectURLOptions& aOptions, nsAString& aResult, mozilla::ErrorResult& aRv)
--- a/dom/webidl/Geolocation.webidl +++ b/dom/webidl/Geolocation.webidl @@ -7,18 +7,18 @@ * http://www.w3.org/TR/geolocation-API * * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C * liability, trademark and document use rules apply. */ dictionary PositionOptions { boolean enableHighAccuracy = false; - long timeout = 0x7fffffff; - long maximumAge = 0; + [Clamp] unsigned long timeout = 0x7fffffff; + [Clamp] unsigned long maximumAge = 0; }; [NoInterfaceObject] interface Geolocation { [Throws] void getCurrentPosition(PositionCallback successCallback, optional PositionErrorCallback? errorCallback = null, optional PositionOptions options);
--- a/dom/workers/ServiceWorkerRegistration.cpp +++ b/dom/workers/ServiceWorkerRegistration.cpp @@ -1,22 +1,24 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ServiceWorkerRegistration.h" +#include "ipc/ErrorIPCUtils.h" #include "mozilla/dom/Notification.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseWorkerProxy.h" #include "mozilla/dom/ServiceWorkerRegistrationBinding.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" +#include "mozilla/unused.h" #include "nsCycleCollectionParticipant.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" #include "ServiceWorker.h" #include "ServiceWorkerManager.h" #include "nsIDocument.h" #include "nsIServiceWorkerManager.h" @@ -382,49 +384,51 @@ public: { mPromise->MaybeReject(aStatus); } }; class UpdateResultRunnable final : public WorkerRunnable { RefPtr<PromiseWorkerProxy> mPromiseProxy; - ErrorResult mStatus; + IPC::Message mSerializedErrorResult; ~UpdateResultRunnable() {} public: UpdateResultRunnable(PromiseWorkerProxy* aPromiseProxy, ErrorResult& aStatus) : WorkerRunnable(aPromiseProxy->GetWorkerPrivate()) , mPromiseProxy(aPromiseProxy) - , mStatus(Move(aStatus)) - { } + { + // ErrorResult is not thread safe. Serialize it for transfer across + // threads. + IPC::WriteParam(&mSerializedErrorResult, aStatus); + aStatus.SuppressException(); + } bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + // Deserialize the ErrorResult now that we are back in the worker + // thread. + ErrorResult status; + PickleIterator iter = PickleIterator(mSerializedErrorResult); + Unused << IPC::ReadParam(&mSerializedErrorResult, &iter, &status); + Promise* promise = mPromiseProxy->WorkerPromise(); - if (mStatus.Failed()) { - promise->MaybeReject(mStatus); + if (status.Failed()) { + promise->MaybeReject(status); } else { promise->MaybeResolve(JS::UndefinedHandleValue); } - mStatus.SuppressException(); + status.SuppressException(); mPromiseProxy->CleanUp(); return true; } - - void - PostDispatch(WorkerPrivate* aWorkerPrivate, bool aSuccess) override - { - if (!aSuccess) { - mStatus.SuppressException(); - } - } }; class WorkerThreadUpdateCallback final : public ServiceWorkerUpdateFinishCallback { RefPtr<PromiseWorkerProxy> mPromiseProxy; ~WorkerThreadUpdateCallback() {
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js +++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js @@ -42,16 +42,17 @@ var ecmaGlobals = {name: "Intl", android: false}, "Iterator", "JSON", "Map", "Math", "NaN", "Number", "Object", + "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", {name: "SharedArrayBuffer", nightly: true}, {name: "SIMD", nightly: true}, @@ -170,18 +171,16 @@ var interfaceNamesInGlobalScope = "PerformanceMark", // IMPORTANT: Do not change this list without review from a DOM peer! "PerformanceMeasure", // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceObserver", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceObserverEntryList", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! - "Promise", -// IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushEvent", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushManager", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushMessageData", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushSubscription", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/workers/test/test_worker_interfaces.js +++ b/dom/workers/test/test_worker_interfaces.js @@ -42,16 +42,17 @@ var ecmaGlobals = {name: "Intl", android: false}, "Iterator", "JSON", "Map", "Math", "NaN", "Number", "Object", + "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", {name: "SharedArrayBuffer", nightly: true}, {name: "SIMD", nightly: true}, @@ -162,18 +163,16 @@ var interfaceNamesInGlobalScope = "PerformanceMark", // IMPORTANT: Do not change this list without review from a DOM peer! "PerformanceMeasure", // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceObserver", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PerformanceObserverEntryList", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! - "Promise", -// IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushManager", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushSubscription", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "PushSubscriptionOptions", b2g: false }, // IMPORTANT: Do not change this list without review from a DOM peer! "Request", // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/xbl/nsXBLBinding.cpp +++ b/dom/xbl/nsXBLBinding.cpp @@ -235,17 +235,17 @@ nsXBLBinding::InstallAnonymousContent(ns // an element is added to a XUL document), we need to notify the // XUL document using its special API. nsCOMPtr<nsIXULDocument> xuldoc(do_QueryInterface(doc)); if (xuldoc) xuldoc->AddSubtreeToDocument(child); #endif if (servoStyleSet) { - servoStyleSet->RestyleSubtree(child); + servoStyleSet->RestyleSubtree(child, /* aForce = */ true); } } } void nsXBLBinding::UninstallAnonymousContent(nsIDocument* aDocument, nsIContent* aAnonParent) {
--- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -1133,16 +1133,20 @@ XMLHttpRequestMainThread::GetAllResponse aResponseHeaders.Truncate(); // If the state is UNSENT or OPENED, // return the empty string and terminate these steps. if (mState == State::unsent || mState == State::opened) { return; } + if (mErrorLoad) { + return; + } + if (nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel()) { RefPtr<nsHeaderVisitor> visitor = new nsHeaderVisitor(*this, WrapNotNull(httpChannel)); if (NS_SUCCEEDED(httpChannel->VisitResponseHeaders(visitor))) { aResponseHeaders = visitor->Headers(); } return; }
--- a/extensions/auth/nsHttpNegotiateAuth.cpp +++ b/extensions/auth/nsHttpNegotiateAuth.cpp @@ -35,27 +35,32 @@ #include "prprf.h" #include "mozilla/Logging.h" #include "prmem.h" #include "prnetdb.h" #include "mozilla/Likely.h" #include "mozilla/Snprintf.h" #include "nsIChannel.h" #include "nsNetUtil.h" +#include "nsThreadUtils.h" +#include "nsIHttpAuthenticatorCallback.h" +#include "mozilla/Mutex.h" +#include "nsICancelable.h" //----------------------------------------------------------------------------- static const char kNegotiate[] = "Negotiate"; static const char kNegotiateAuthTrustedURIs[] = "network.negotiate-auth.trusted-uris"; static const char kNegotiateAuthDelegationURIs[] = "network.negotiate-auth.delegation-uris"; static const char kNegotiateAuthAllowProxies[] = "network.negotiate-auth.allow-proxies"; static const char kNegotiateAuthAllowNonFqdn[] = "network.negotiate-auth.allow-non-fqdn"; static const char kNegotiateAuthSSPI[] = "network.auth.use-sspi"; #define kNegotiateLen (sizeof(kNegotiate)-1) +#define DEFAULT_THREAD_TIMEOUT_MS 30000 //----------------------------------------------------------------------------- // Return false when the channel comes from a Private browsing window. static bool TestNotInPBMode(nsIHttpAuthenticableChannel *authChannel) { nsCOMPtr<nsIChannel> bareChannel = do_QueryInterface(authChannel); @@ -179,17 +184,270 @@ nsHttpNegotiateAuth::ChallengeReceived(n return rv; } *continuationState = module; return NS_OK; } NS_IMPL_ISUPPORTS(nsHttpNegotiateAuth, nsIHttpAuthenticator) - + +namespace { + +// +// GetNextTokenCompleteEvent +// +// This event is fired on main thread when async call of +// nsHttpNegotiateAuth::GenerateCredentials is finished. During the Run() +// method the nsIHttpAuthenticatorCallback::OnCredsAvailable is called with +// obtained credentials, flags and NS_OK when successful, otherwise +// NS_ERROR_FAILURE is returned as a result of failed operation. +// +class GetNextTokenCompleteEvent final : public nsIRunnable, + public nsICancelable +{ + virtual ~GetNextTokenCompleteEvent() + { + if (mCreds) { + free(mCreds); + } + }; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit GetNextTokenCompleteEvent(nsIHttpAuthenticatorCallback* aCallback) + : mCallback(aCallback) + , mCreds(nullptr) + , mCancelled(false) + { + } + + NS_IMETHODIMP DispatchSuccess(char *aCreds, + uint32_t aFlags, + already_AddRefed<nsISupports> aSessionState, + already_AddRefed<nsISupports> aContinuationState) + { + // Called from worker thread + MOZ_ASSERT(!NS_IsMainThread()); + + mCreds = aCreds; + mFlags = aFlags; + mResult = NS_OK; + mSessionState = aSessionState; + mContinuationState = aContinuationState; + return NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL); + } + + NS_IMETHODIMP DispatchError(already_AddRefed<nsISupports> aSessionState, + already_AddRefed<nsISupports> aContinuationState) + { + // Called from worker thread + MOZ_ASSERT(!NS_IsMainThread()); + + mResult = NS_ERROR_FAILURE; + mSessionState = aSessionState; + mContinuationState = aContinuationState; + return NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL); + } + + NS_IMETHODIMP Run() override + { + // Runs on main thread + MOZ_ASSERT(NS_IsMainThread()); + + if (!mCancelled) { + nsCOMPtr<nsIHttpAuthenticatorCallback> callback; + callback.swap(mCallback); + callback->OnCredsGenerated(mCreds, mFlags, mResult, mSessionState, mContinuationState); + } + return NS_OK; + } + + NS_IMETHODIMP Cancel(nsresult aReason) override + { + // Supposed to be called from main thread + MOZ_ASSERT(NS_IsMainThread()); + + mCancelled = true; + return NS_OK; + } + +private: + nsCOMPtr<nsIHttpAuthenticatorCallback> mCallback; + char *mCreds; // This class owns it, freed in destructor + uint32_t mFlags; + nsresult mResult; + bool mCancelled; + nsCOMPtr<nsISupports> mSessionState; + nsCOMPtr<nsISupports> mContinuationState; +}; + +NS_IMPL_ISUPPORTS(GetNextTokenCompleteEvent, nsIRunnable, nsICancelable) + +// +// GetNextTokenRunnable +// +// This runnable is created by GenerateCredentialsAsync and it runs +// in nsHttpNegotiateAuth::mNegotiateThread and calling GenerateCredentials. +// +class GetNextTokenRunnable final : public mozilla::Runnable +{ + virtual ~GetNextTokenRunnable() {} + public: + GetNextTokenRunnable(nsIHttpAuthenticableChannel *authChannel, + const char *challenge, + bool isProxyAuth, + const char16_t *domain, + const char16_t *username, + const char16_t *password, + nsISupports *sessionState, + nsISupports *continuationState, + GetNextTokenCompleteEvent *aCompleteEvent + ) + : mAuthChannel(authChannel) + , mChallenge(challenge) + , mIsProxyAuth(isProxyAuth) + , mDomain(domain) + , mUsername(username) + , mPassword(password) + , mSessionState(sessionState) + , mContinuationState(continuationState) + , mCompleteEvent(aCompleteEvent) + { + } + + NS_IMETHODIMP Run() override + { + // Runs on worker thread + MOZ_ASSERT(!NS_IsMainThread()); + + char *creds; + uint32_t flags; + nsresult rv = ObtainCredentialsAndFlags(&creds, &flags); + + // Passing session and continuation state this way to not touch + // referencing of the object that may not be thread safe. + // Not having a thread safe referencing doesn't mean the object + // cannot be used on multiple threads (one example is nsAuthSSPI.) + // This ensures state objects will be destroyed on the main thread + // when not changed by GenerateCredentials. + if (NS_FAILED(rv)) { + return mCompleteEvent->DispatchError(mSessionState.forget(), + mContinuationState.forget()); + } + + return mCompleteEvent->DispatchSuccess(creds, flags, + mSessionState.forget(), + mContinuationState.forget()); + } + + NS_IMETHODIMP ObtainCredentialsAndFlags(char **aCreds, uint32_t *aFlags) + { + nsresult rv; + + // Use negotiate service to call GenerateCredentials outside of main thread + nsAutoCString contractId; + contractId.Assign(NS_HTTP_AUTHENTICATOR_CONTRACTID_PREFIX); + contractId.Append("negotiate"); + nsCOMPtr<nsIHttpAuthenticator> authenticator = + do_GetService(contractId.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsISupports *sessionState = mSessionState; + nsISupports *continuationState = mContinuationState; + // The continuationState is for the sake of completeness propagated + // to the caller (despite it is not changed in any GenerateCredentials + // implementation). + // + // The only implementation that use sessionState is the + // nsHttpDigestAuth::GenerateCredentials. Since there's no reason + // to implement nsHttpDigestAuth::GenerateCredentialsAsync + // because digest auth does not block the main thread, we won't + // propagate changes to sessionState to the caller because of + // the change is too complicated on the caller side. + // + // Should any of the session or continuation states change inside + // this method, they must be threadsafe. + rv = authenticator->GenerateCredentials(mAuthChannel, + mChallenge.get(), + mIsProxyAuth, + mDomain.get(), + mUsername.get(), + mPassword.get(), + &sessionState, + &continuationState, + aFlags, + aCreds); + if (mSessionState != sessionState) { + mSessionState = sessionState; + } + if (mContinuationState != continuationState) { + mContinuationState = continuationState; + } + return rv; + } + private: + nsCOMPtr<nsIHttpAuthenticableChannel> mAuthChannel; + nsCString mChallenge; + bool mIsProxyAuth; + nsString mDomain; + nsString mUsername; + nsString mPassword; + nsCOMPtr<nsISupports> mSessionState; + nsCOMPtr<nsISupports> mContinuationState; + RefPtr<GetNextTokenCompleteEvent> mCompleteEvent; +}; + +} // anonymous namespace + +NS_IMETHODIMP +nsHttpNegotiateAuth::GenerateCredentialsAsync(nsIHttpAuthenticableChannel *authChannel, + nsIHttpAuthenticatorCallback* aCallback, + const char *challenge, + bool isProxyAuth, + const char16_t *domain, + const char16_t *username, + const char16_t *password, + nsISupports *sessionState, + nsISupports *continuationState, + nsICancelable **aCancelable) +{ + NS_ENSURE_ARG(aCallback); + NS_ENSURE_ARG_POINTER(aCancelable); + + RefPtr<GetNextTokenCompleteEvent> cancelEvent = + new GetNextTokenCompleteEvent(aCallback); + + + nsCOMPtr<nsIRunnable> getNextTokenRunnable = + new GetNextTokenRunnable(authChannel, + challenge, + isProxyAuth, + domain, + username, + password, + sessionState, + continuationState, + cancelEvent); + cancelEvent.forget(aCancelable); + + nsresult rv; + if (!mNegotiateThread) { + mNegotiateThread = + new mozilla::LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, + NS_LITERAL_CSTRING("NegotiateAuth")); + NS_ENSURE_TRUE(mNegotiateThread, NS_ERROR_OUT_OF_MEMORY); + } + rv = mNegotiateThread->Dispatch(getNextTokenRunnable, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + // // GenerateCredentials // // This routine is responsible for creating the correct authentication // blob to pass to the server that requested "Negotiate" authentication. // NS_IMETHODIMP nsHttpNegotiateAuth::GenerateCredentials(nsIHttpAuthenticableChannel *authChannel,
--- a/extensions/auth/nsHttpNegotiateAuth.h +++ b/extensions/auth/nsHttpNegotiateAuth.h @@ -5,24 +5,25 @@ #ifndef nsHttpNegotiateAuth_h__ #define nsHttpNegotiateAuth_h__ #include "nsIHttpAuthenticator.h" #include "nsIURI.h" #include "nsSubstring.h" #include "mozilla/Attributes.h" +#include "mozilla/LazyIdleThread.h" // The nsHttpNegotiateAuth class provides responses for the GSS-API Negotiate method // as specified by Microsoft in draft-brezak-spnego-http-04.txt class nsHttpNegotiateAuth final : public nsIHttpAuthenticator { public: - NS_DECL_ISUPPORTS + NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIHTTPAUTHENTICATOR private: ~nsHttpNegotiateAuth() {} // returns the value of the given boolean pref bool TestBoolPref(const char *pref); @@ -32,10 +33,12 @@ private: // returns true if URI is accepted by the list of hosts in the pref bool TestPref(nsIURI *, const char *pref); bool MatchesBaseURI(const nsCSubstring &scheme, const nsCSubstring &host, int32_t port, const char *baseStart, const char *baseEnd); + // Thread for GenerateCredentialsAsync + RefPtr<mozilla::LazyIdleThread> mNegotiateThread; }; #endif /* nsHttpNegotiateAuth_h__ */
--- a/image/Decoder.cpp +++ b/image/Decoder.cpp @@ -20,28 +20,23 @@ using mozilla::gfx::IntSize; using mozilla::gfx::SurfaceFormat; namespace mozilla { namespace image { class MOZ_STACK_CLASS AutoRecordDecoderTelemetry final { public: - AutoRecordDecoderTelemetry(Decoder* aDecoder, uint32_t aByteCount) + explicit AutoRecordDecoderTelemetry(Decoder* aDecoder) : mDecoder(aDecoder) { MOZ_ASSERT(mDecoder); // Begin recording telemetry data. mStartTime = TimeStamp::Now(); - mDecoder->mChunkCount++; - - // Keep track of the total number of bytes written. - mDecoder->mBytesDecoded += aByteCount; - } ~AutoRecordDecoderTelemetry() { // Finish telemetry. mDecoder->mDecodeTime += (TimeStamp::Now() - mStartTime); } @@ -53,27 +48,24 @@ private: Decoder::Decoder(RasterImage* aImage) : mImageData(nullptr) , mImageDataLength(0) , mColormap(nullptr) , mColormapSize(0) , mImage(aImage) , mProgress(NoProgress) , mFrameCount(0) - , mFailCode(NS_OK) - , mChunkCount(0) , mDecoderFlags(DefaultDecoderFlags()) , mSurfaceFlags(DefaultSurfaceFlags()) - , mBytesDecoded(0) , mInitialized(false) , mMetadataDecode(false) , mInFrame(false) - , mDataDone(false) + , mReachedTerminalState(false) , mDecodeDone(false) - , mDataError(false) + , mError(false) , mDecodeAborted(false) , mShouldReportError(false) { } Decoder::~Decoder() { MOZ_ASSERT(mProgress == NoProgress || !mImage, "Destroying Decoder without taking all its progress changes"); @@ -87,123 +79,122 @@ Decoder::~Decoder() NS_ReleaseOnMainThread(mImage.forget()); } } /* * Common implementation of the decoder interface. */ -void +nsresult Decoder::Init() { // No re-initializing MOZ_ASSERT(!mInitialized, "Can't re-initialize a decoder!"); // All decoders must have a SourceBufferIterator. MOZ_ASSERT(mIterator); // It doesn't make sense to decode anything but the first frame if we can't // store anything in the SurfaceCache, since only the last frame we decode // will be retrievable. MOZ_ASSERT(ShouldUseSurfaceCache() || IsFirstFrameDecode()); - // Implementation-specific initialization - InitInternal(); + // Implementation-specific initialization. + nsresult rv = InitInternal(); mInitialized = true; + + return rv; } nsresult -Decoder::Decode(NotNull<IResumable*> aOnResume) +Decoder::Decode(IResumable* aOnResume /* = nullptr */) { MOZ_ASSERT(mInitialized, "Should be initialized here"); MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); - // We keep decoding chunks until the decode completes or there are no more - // chunks available. - while (!GetDecodeDone() && !HasError()) { - auto newState = mIterator->AdvanceOrScheduleResume(aOnResume.get()); - - if (newState == SourceBufferIterator::WAITING) { - // We can't continue because the rest of the data hasn't arrived from the - // network yet. We don't have to do anything special; the - // SourceBufferIterator will ensure that Decode() gets called again on a - // DecodePool thread when more data is available. - return NS_OK; - } - - if (newState == SourceBufferIterator::COMPLETE) { - mDataDone = true; + // If we're already done, don't attempt to keep decoding. + if (GetDecodeDone()) { + return HasError() ? NS_ERROR_FAILURE : NS_OK; + } - nsresult finalStatus = mIterator->CompletionStatus(); - if (NS_FAILED(finalStatus)) { - PostDataError(); - } - - CompleteDecode(); - return finalStatus; - } - - MOZ_ASSERT(newState == SourceBufferIterator::READY); + Maybe<TerminalState> terminalState; + { + PROFILER_LABEL("ImageDecoder", "Decode", js::ProfileEntry::Category::GRAPHICS); + AutoRecordDecoderTelemetry telemetry(this); - { - PROFILER_LABEL("ImageDecoder", "Write", - js::ProfileEntry::Category::GRAPHICS); - - AutoRecordDecoderTelemetry telemetry(this, mIterator->Length()); - - // Pass the data along to the implementation. - Maybe<TerminalState> terminalState = DoDecode(*mIterator); - - if (terminalState == Some(TerminalState::FAILURE)) { - PostDataError(); - } - } + terminalState = DoDecode(*mIterator, aOnResume); } + if (!terminalState) { + // We need more data to continue. If @aOnResume was non-null, the + // SourceBufferIterator will automatically reschedule us. Otherwise, it's up + // to the caller. + return NS_OK; + } + + // We reached a terminal state; we're now done decoding. + mReachedTerminalState = true; + + // If decoding failed, record that fact. + if (terminalState == Some(TerminalState::FAILURE)) { + PostError(); + } + + // Perform final cleanup. CompleteDecode(); + return HasError() ? NS_ERROR_FAILURE : NS_OK; } bool Decoder::ShouldSyncDecode(size_t aByteLimit) { MOZ_ASSERT(aByteLimit > 0); MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator"); return mIterator->RemainingBytesIsNoMoreThan(aByteLimit); } void Decoder::CompleteDecode() { - // Implementation-specific finalization - BeforeFinishInternal(); - if (!HasError()) { - FinishInternal(); - } else { - FinishWithErrorInternal(); + // Implementation-specific finalization. + nsresult rv = BeforeFinishInternal(); + if (NS_FAILED(rv)) { + PostError(); + } + + rv = HasError() ? FinishWithErrorInternal() + : FinishInternal(); + if (NS_FAILED(rv)) { + PostError(); + } + + // If this was a metadata decode and we never got a size, the decode failed. + if (IsMetadataDecode() && !HasSize()) { + PostError(); } // If the implementation left us mid-frame, finish that up. if (mInFrame && !HasError()) { PostFrameStop(); } // If PostDecodeDone() has not been called, and this decoder wasn't aborted // early because of low-memory conditions or losing a race with another // decoder, we need to send teardown notifications (and report an error to the // console later). if (!IsMetadataDecode() && !mDecodeDone && !WasAborted()) { mShouldReportError = true; - // If we only have a data error, we're usable if we have at least one - // complete frame. - if (!HasDecoderError() && GetCompleteFrameCount() > 0) { + // Even if we encountered an error, we're still usable if we have at least + // one complete frame. + if (GetCompleteFrameCount() > 0) { // We're usable, so do exactly what we should have when the decoder // completed. // Not writing to the entire frame may have left us transparent. PostHasTransparency(); if (mInFrame) { PostFrameStop(); @@ -271,32 +262,30 @@ Decoder::AllocateFrame(uint32_t aFrameNu if (aFrameNum + 1 == mFrameCount) { // If we're past the first frame, PostIsAnimated() should've been called. MOZ_ASSERT_IF(mFrameCount > 1, HasAnimation()); // Update our state to reflect the new frame MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!"); mInFrame = true; } - } else { - PostDataError(); } return mCurrentFrame ? NS_OK : NS_ERROR_FAILURE; } RawAccessFrameRef Decoder::AllocateFrameInternal(uint32_t aFrameNum, const nsIntSize& aTargetSize, const nsIntRect& aFrameRect, SurfaceFormat aFormat, uint8_t aPaletteDepth, imgFrame* aPreviousFrame) { - if (mDataError || NS_FAILED(mFailCode)) { + if (HasError()) { return RawAccessFrameRef(); } if (aFrameNum != mFrameCount) { MOZ_ASSERT_UNREACHABLE("Allocating frames out of order"); return RawAccessFrameRef(); } @@ -383,20 +372,20 @@ Decoder::AllocateFrameInternal(uint32_t return ref; } /* * Hook stubs. Override these as necessary in decoder implementations. */ -void Decoder::InitInternal() { } -void Decoder::BeforeFinishInternal() { } -void Decoder::FinishInternal() { } -void Decoder::FinishWithErrorInternal() { } +nsresult Decoder::InitInternal() { return NS_OK; } +nsresult Decoder::BeforeFinishInternal() { return NS_OK; } +nsresult Decoder::FinishInternal() { return NS_OK; } +nsresult Decoder::FinishWithErrorInternal() { return NS_OK; } /* * Progress Notifications */ void Decoder::PostSize(int32_t aWidth, int32_t aHeight, @@ -489,35 +478,19 @@ Decoder::PostDecodeDone(int32_t aLoopCou mDecodeDone = true; mImageMetadata.SetLoopCount(aLoopCount); mProgress |= FLAG_DECODE_COMPLETE; } void -Decoder::PostDataError() +Decoder::PostError() { - mDataError = true; - - if (mInFrame && mCurrentFrame) { - mCurrentFrame->Abort(); - } -} - -void -Decoder::PostDecoderError(nsresult aFailureCode) -{ - MOZ_ASSERT(NS_FAILED(aFailureCode), "Not a failure code!"); - - mFailCode = aFailureCode; - - // XXXbholley - we should report the image URI here, but imgContainer - // needs to know its URI first - NS_WARNING("Image decoding error - This is probably a bug!"); + mError = true; if (mInFrame && mCurrentFrame) { mCurrentFrame->Abort(); } } Telemetry::ID Decoder::SpeedHistogram()
--- a/image/Decoder.h +++ b/image/Decoder.h @@ -32,28 +32,30 @@ class Decoder { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Decoder) explicit Decoder(RasterImage* aImage); /** * Initialize an image decoder. Decoders may not be re-initialized. + * + * @return NS_OK if the decoder could be initialized successfully. */ - void Init(); + nsresult Init(); /** * Decodes, reading all data currently available in the SourceBuffer. * - * If more data is needed, Decode() will schedule @aOnResume to be called when - * more data is available. + * If more data is needed and @aOnResume is non-null, Decode() will schedule + * @aOnResume to be called when more data is available. * * Any errors are reported by setting the appropriate state on the decoder. */ - nsresult Decode(NotNull<IResumable*> aOnResume); + nsresult Decode(IResumable* aOnResume = nullptr); /** * Given a maximum number of bytes we're willing to decode, @aByteLimit, * returns true if we should attempt to run this decoder synchronously. */ bool ShouldSyncDecode(size_t aByteLimit); /** @@ -157,49 +159,54 @@ public: /** * Should we stop decoding after the first frame? */ bool IsFirstFrameDecode() const { return bool(mDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY); } - size_t BytesDecoded() const { return mBytesDecoded; } + size_t BytesDecoded() const + { + MOZ_ASSERT(mIterator); + return mIterator->ByteCount(); + } // The amount of time we've spent inside DoDecode() so far for this decoder. TimeDuration DecodeTime() const { return mDecodeTime; } // The number of chunks this decoder's input was divided into. - uint32_t ChunkCount() const { return mChunkCount; } + uint32_t ChunkCount() const + { + MOZ_ASSERT(mIterator); + return mIterator->ChunkCount(); + } // The number of frames we have, including anything in-progress. Thus, this // is only 0 if we haven't begun any frames. uint32_t GetFrameCount() { return mFrameCount; } // The number of complete frames we have (ie, not including anything // in-progress). uint32_t GetCompleteFrameCount() { return mInFrame ? mFrameCount - 1 : mFrameCount; } // Did we discover that the image we're decoding is animated? bool HasAnimation() const { return mImageMetadata.HasAnimation(); } // Error tracking - bool HasError() const { return HasDataError() || HasDecoderError(); } - bool HasDataError() const { return mDataError; } - bool HasDecoderError() const { return NS_FAILED(mFailCode); } + bool HasError() const { return mError; } bool ShouldReportError() const { return mShouldReportError; } - nsresult GetDecoderError() const { return mFailCode; } /// Did we finish decoding enough that calling Decode() again would be useless? bool GetDecodeDone() const { - return mDecodeDone || (mMetadataDecode && HasSize()) || - HasError() || mDataDone; + return mReachedTerminalState || mDecodeDone || + (mMetadataDecode && HasSize()) || HasError(); } /// Are we in the middle of a frame right now? Used for assertions only. bool InFrame() const { return mInFrame; } /// Should we store surfaces created by this decoder in the SurfaceCache? bool ShouldUseSurfaceCache() const { return bool(mImage); } @@ -282,23 +289,24 @@ protected: virtual ~Decoder(); /* * Internal hooks. Decoder implementations may override these and * only these methods. * * BeforeFinishInternal() can be used to detect if decoding is in an * incomplete state, e.g. due to file truncation, in which case it should - * call PostDataError(). + * return a failing nsresult. */ - virtual void InitInternal(); - virtual Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) = 0; - virtual void BeforeFinishInternal(); - virtual void FinishInternal(); - virtual void FinishWithErrorInternal(); + virtual nsresult InitInternal(); + virtual Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) = 0; + virtual nsresult BeforeFinishInternal(); + virtual nsresult FinishInternal(); + virtual nsresult FinishWithErrorInternal(); /* * Progress notifications. */ // Called by decoders when they determine the size of the image. Informs // the image of its size and sends notifications. void PostSize(int32_t aWidth, @@ -353,27 +361,16 @@ protected: // the stream, or by us calling FinishInternal(). // // May not be called mid-frame. // // For animated images, specify the loop count. -1 means loop forever, 0 // means a single iteration, stopping on the last frame. void PostDecodeDone(int32_t aLoopCount = 0); - // Data errors are the fault of the source data, decoder errors are our fault - void PostDataError(); - void PostDecoderError(nsresult aFailCode); - - /** - * CompleteDecode() finishes up the decoding process after Decode() determines - * that we're finished. It records final progress and does all the cleanup - * that's possible off-main-thread. - */ - void CompleteDecode(); - /** * Allocates a new frame, making it our current frame if successful. * * The @aFrameNum parameter only exists as a sanity check; it's illegal to * create a new frame anywhere but immediately after the existing frames. * * If a non-paletted frame is desired, pass 0 for aPaletteDepth. */ @@ -385,16 +382,27 @@ protected: /// Helper method for decoders which only have 'basic' frame allocation needs. nsresult AllocateBasicFrame() { nsIntSize size = GetSize(); return AllocateFrame(0, size, nsIntRect(nsIntPoint(), size), gfx::SurfaceFormat::B8G8R8A8); } +private: + /// Report that an error was encountered while decoding. + void PostError(); + + /** + * CompleteDecode() finishes up the decoding process after Decode() determines + * that we're finished. It records final progress and does all the cleanup + * that's possible off-main-thread. + */ + void CompleteDecode(); + RawAccessFrameRef AllocateFrameInternal(uint32_t aFrameNum, const nsIntSize& aTargetSize, const nsIntRect& aFrameRect, gfx::SurfaceFormat aFormat, uint8_t aPaletteDepth, imgFrame* aPreviousFrame); protected: @@ -410,32 +418,28 @@ private: Maybe<SourceBufferIterator> mIterator; RawAccessFrameRef mCurrentFrame; ImageMetadata mImageMetadata; nsIntRect mInvalidRect; // Tracks an invalidation region in the current frame. Progress mProgress; uint32_t mFrameCount; // Number of frames, including anything in-progress - nsresult mFailCode; - // Telemetry data for this decoder. TimeDuration mDecodeTime; - uint32_t mChunkCount; DecoderFlags mDecoderFlags; SurfaceFlags mSurfaceFlags; - size_t mBytesDecoded; bool mInitialized : 1; bool mMetadataDecode : 1; bool mInFrame : 1; - bool mDataDone : 1; + bool mReachedTerminalState : 1; bool mDecodeDone : 1; - bool mDataError : 1; + bool mError : 1; bool mDecodeAborted : 1; bool mShouldReportError : 1; }; } // namespace image } // namespace mozilla #endif // mozilla_image_Decoder_h
--- a/image/DecoderFactory.cpp +++ b/image/DecoderFactory.cpp @@ -130,18 +130,17 @@ DecoderFactory::CreateDecoder(DecoderTyp decoder->SetSampleSize(aSampleSize); // Set a target size for downscale-during-decode if applicable. if (aTargetSize) { DebugOnly<nsresult> rv = decoder->SetTargetSize(*aTargetSize); MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?"); } - decoder->Init(); - if (NS_FAILED(decoder->GetDecoderError())) { + if (NS_FAILED(decoder->Init())) { return nullptr; } // Add a placeholder to the SurfaceCache so we won't trigger any more decoders // with the same parameters. IntSize surfaceSize = aTargetSize.valueOr(aIntrinsicSize); SurfaceKey surfaceKey = RasterSurfaceKey(surfaceSize, aSurfaceFlags, /* aFrameNum = */ 0); @@ -175,18 +174,17 @@ DecoderFactory::CreateAnimationDecoder(D MOZ_ASSERT(decoder, "Should have a decoder now"); // Initialize the decoder. decoder->SetMetadataDecode(false); decoder->SetIterator(aSourceBuffer->Iterator()); decoder->SetDecoderFlags(aDecoderFlags | DecoderFlags::IS_REDECODE); decoder->SetSurfaceFlags(aSurfaceFlags); - decoder->Init(); - if (NS_FAILED(decoder->GetDecoderError())) { + if (NS_FAILED(decoder->Init())) { return nullptr; } // Add a placeholder for the first frame to the SurfaceCache so we won't // trigger any more decoders with the same parameters. SurfaceKey surfaceKey = RasterSurfaceKey(aIntrinsicSize, aSurfaceFlags, /* aFrameNum = */ 0); InsertOutcome outcome = @@ -213,18 +211,17 @@ DecoderFactory::CreateMetadataDecoder(De GetDecoder(aType, aImage, /* aIsRedecode = */ false); MOZ_ASSERT(decoder, "Should have a decoder now"); // Initialize the decoder. decoder->SetMetadataDecode(true); decoder->SetIterator(aSourceBuffer->Iterator()); decoder->SetSampleSize(aSampleSize); - decoder->Init(); - if (NS_FAILED(decoder->GetDecoderError())) { + if (NS_FAILED(decoder->Init())) { return nullptr; } RefPtr<IDecodingTask> task = new MetadataDecodingTask(WrapNotNull(decoder)); return task.forget(); } /* static */ already_AddRefed<Decoder> @@ -262,18 +259,17 @@ DecoderFactory::CreateDecoderForICOResou // Set a target size for downscale-during-decode if applicable. const Maybe<IntSize> targetSize = aICODecoder->GetTargetSize(); if (targetSize) { DebugOnly<nsresult> rv = decoder->SetTargetSize(*targetSize); MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?"); } - decoder->Init(); - if (NS_FAILED(decoder->GetDecoderError())) { + if (NS_FAILED(decoder->Init())) { return nullptr; } return decoder.forget(); } /* static */ already_AddRefed<Decoder> DecoderFactory::CreateAnonymousDecoder(DecoderType aType, @@ -308,18 +304,17 @@ DecoderFactory::CreateAnonymousDecoder(D decoder->SetSurfaceFlags(aSurfaceFlags); // Set a target size for downscale-during-decode if applicable. if (aTargetSize) { DebugOnly<nsresult> rv = decoder->SetTargetSize(*aTargetSize); MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?"); } - decoder->Init(); - if (NS_FAILED(decoder->GetDecoderError())) { + if (NS_FAILED(decoder->Init())) { return nullptr; } return decoder.forget(); } /* static */ already_AddRefed<Decoder> DecoderFactory::CreateAnonymousMetadataDecoder(DecoderType aType, @@ -333,18 +328,17 @@ DecoderFactory::CreateAnonymousMetadataD GetDecoder(aType, /* aImage = */ nullptr, /* aIsRedecode = */ false); MOZ_ASSERT(decoder, "Should have a decoder now"); // Initialize the decoder. decoder->SetMetadataDecode(true); decoder->SetIterator(aSourceBuffer->Iterator()); decoder->SetDecoderFlags(DecoderFlags::FIRST_FRAME_ONLY); - decoder->Init(); - if (NS_FAILED(decoder->GetDecoderError())) { + if (NS_FAILED(decoder->Init())) { return nullptr; } return decoder.forget(); } } // namespace image } // namespace mozilla
--- a/image/RasterImage.cpp +++ b/image/RasterImage.cpp @@ -1777,17 +1777,17 @@ RasterImage::FinalizeDecoder(Decoder* aD void RasterImage::ReportDecoderError(Decoder* aDecoder) { nsCOMPtr<nsIConsoleService> consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID); nsCOMPtr<nsIScriptError> errorObject = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); - if (consoleService && errorObject && !aDecoder->HasDecoderError()) { + if (consoleService && errorObject) { nsAutoString msg(NS_LITERAL_STRING("Image corrupt or truncated.")); nsAutoString src; if (GetURI()) { nsCString uri; if (GetURI()->GetSpecTruncatedTo1k(uri) == ImageURL::TruncatedTo1k) { msg += NS_LITERAL_STRING(" URI in this note truncated due to length."); } src = NS_ConvertUTF8toUTF16(uri);
--- a/image/SourceBuffer.cpp +++ b/image/SourceBuffer.cpp @@ -25,35 +25,93 @@ namespace image { SourceBufferIterator::~SourceBufferIterator() { if (mOwner) { mOwner->OnIteratorRelease(); } } +SourceBufferIterator& +SourceBufferIterator::operator=(SourceBufferIterator&& aOther) +{ + if (mOwner) { + mOwner->OnIteratorRelease(); + } + + mOwner = Move(aOther.mOwner); + mState = aOther.mState; + mData = aOther.mData; + mChunkCount = aOther.mChunkCount; + mByteCount = aOther.mByteCount; + + return *this; +} + SourceBufferIterator::State -SourceBufferIterator::AdvanceOrScheduleResume(IResumable* aConsumer) +SourceBufferIterator::AdvanceOrScheduleResume(size_t aRequestedBytes, + IResumable* aConsumer) { MOZ_ASSERT(mOwner); - return mOwner->AdvanceIteratorOrScheduleResume(*this, aConsumer); + + if (MOZ_UNLIKELY(!HasMore())) { + MOZ_ASSERT_UNREACHABLE("Should not advance a completed iterator"); + return COMPLETE; + } + + // The range of data [mOffset, mOffset + mNextReadLength) has just been read + // by the caller (or at least they don't have any interest in it), so consume + // that data. + MOZ_ASSERT(mData.mIterating.mNextReadLength <= mData.mIterating.mAvailableLength); + mData.mIterating.mOffset += mData.mIterating.mNextReadLength; + mData.mIterating.mAvailableLength -= mData.mIterating.mNextReadLength; + mData.mIterating.mNextReadLength = 0; + + if (MOZ_LIKELY(mState == READY)) { + // If the caller wants zero bytes of data, that's easy enough; we just + // configured ourselves for a zero-byte read above! In theory we could do + // this even in the START state, but it's not important for performance and + // breaking the ability of callers to assert that the pointer returned by + // Data() is non-null doesn't seem worth it. + if (aRequestedBytes == 0) { + MOZ_ASSERT(mData.mIterating.mNextReadLength == 0); + return READY; + } + + // Try to satisfy the request out of our local buffer. This is potentially + // much faster than requesting data from our owning SourceBuffer because we + // don't have to take the lock. Note that if we have anything at all in our + // local buffer, we use it to satisfy the request; @aRequestedBytes is just + // the *maximum* number of bytes we can return. + if (mData.mIterating.mAvailableLength > 0) { + return AdvanceFromLocalBuffer(aRequestedBytes); + } + } + + // Our local buffer is empty, so we'll have to request data from our owning + // SourceBuffer. + return mOwner->AdvanceIteratorOrScheduleResume(*this, + aRequestedBytes, + aConsumer); } bool SourceBufferIterator::RemainingBytesIsNoMoreThan(size_t aBytes) const { MOZ_ASSERT(mOwner); return mOwner->RemainingBytesIsNoMoreThan(*this, aBytes); } ////////////////////////////////////////////////////////////////////////////// // SourceBuffer implementation. ////////////////////////////////////////////////////////////////////////////// +const size_t SourceBuffer::MIN_CHUNK_CAPACITY; + SourceBuffer::SourceBuffer() : mMutex("image::SourceBuffer") , mConsumerCount(0) { } SourceBuffer::~SourceBuffer() { MOZ_ASSERT(mConsumerCount == 0, @@ -216,17 +274,19 @@ SourceBuffer::FibonacciCapacityWithMinim void SourceBuffer::AddWaitingConsumer(IResumable* aConsumer) { mMutex.AssertCurrentThreadOwns(); MOZ_ASSERT(!mStatus, "Waiting when we're complete?"); - mWaitingConsumers.AppendElement(aConsumer); + if (aConsumer) { + mWaitingConsumers.AppendElement(aConsumer); + } } void SourceBuffer::ResumeWaitingConsumers() { mMutex.AssertCurrentThreadOwns(); if (mWaitingConsumers.Length() == 0) { @@ -499,17 +559,17 @@ SourceBuffer::RemainingBytesIsNoMoreThan // If the iterator's at the end, the answer is trivial. if (!aIterator.HasMore()) { return true; } uint32_t iteratorChunk = aIterator.mData.mIterating.mChunk; size_t iteratorOffset = aIterator.mData.mIterating.mOffset; - size_t iteratorLength = aIterator.mData.mIterating.mLength; + size_t iteratorLength = aIterator.mData.mIterating.mAvailableLength; // Include the bytes the iterator is currently pointing to in the limit, so // that the current chunk doesn't have to be a special case. size_t bytes = aBytes + iteratorOffset + iteratorLength; // Count the length over all of our chunks, starting with the one that the // iterator is currently pointing to. (This is O(N), but N is expected to be // ~1, so it doesn't seem worth caching the length separately.) @@ -521,24 +581,23 @@ SourceBuffer::RemainingBytesIsNoMoreThan } } return true; } SourceBufferIterator::State SourceBuffer::AdvanceIteratorOrScheduleResume(SourceBufferIterator& aIterator, + size_t aRequestedBytes, IResumable* aConsumer) { MutexAutoLock lock(mMutex); - if (MOZ_UNLIKELY(!aIterator.HasMore())) { - MOZ_ASSERT_UNREACHABLE("Should not advance a completed iterator"); - return SourceBufferIterator::COMPLETE; - } + MOZ_ASSERT(aIterator.HasMore(), "Advancing a completed iterator and " + "AdvanceOrScheduleResume didn't catch it"); if (MOZ_UNLIKELY(mStatus && NS_FAILED(*mStatus))) { // This SourceBuffer is complete due to an error; all reads fail. return aIterator.SetComplete(*mStatus); } if (MOZ_UNLIKELY(mChunks.Length() == 0)) { // We haven't gotten an initial chunk yet. @@ -546,32 +605,33 @@ SourceBuffer::AdvanceIteratorOrScheduleR return aIterator.SetWaiting(); } uint32_t iteratorChunkIdx = aIterator.mData.mIterating.mChunk; MOZ_ASSERT(iteratorChunkIdx < mChunks.Length()); const Chunk& currentChunk = mChunks[iteratorChunkIdx]; size_t iteratorEnd = aIterator.mData.mIterating.mOffset + - aIterator.mData.mIterating.mLength; + aIterator.mData.mIterating.mAvailableLength; MOZ_ASSERT(iteratorEnd <= currentChunk.Length()); MOZ_ASSERT(iteratorEnd <= currentChunk.Capacity()); if (iteratorEnd < currentChunk.Length()) { // There's more data in the current chunk. return aIterator.SetReady(iteratorChunkIdx, currentChunk.Data(), - iteratorEnd, currentChunk.Length() - iteratorEnd); + iteratorEnd, currentChunk.Length() - iteratorEnd, + aRequestedBytes); } if (iteratorEnd == currentChunk.Capacity() && !IsLastChunk(iteratorChunkIdx)) { // Advance to the next chunk. const Chunk& nextChunk = mChunks[iteratorChunkIdx + 1]; return aIterator.SetReady(iteratorChunkIdx + 1, nextChunk.Data(), 0, - nextChunk.Length()); + nextChunk.Length(), aRequestedBytes); } MOZ_ASSERT(IsLastChunk(iteratorChunkIdx), "Should've advanced"); if (mStatus) { // There's no more data and this SourceBuffer completed successfully. MOZ_ASSERT(NS_SUCCEEDED(*mStatus), "Handled failures earlier"); return aIterator.SetComplete(*mStatus);
--- a/image/SourceBuffer.h +++ b/image/SourceBuffer.h @@ -75,52 +75,85 @@ public: READY, // The iterator is pointing to new data. WAITING, // The iterator is blocked and the caller must yield. COMPLETE // The iterator is pointing to the end of the buffer. }; explicit SourceBufferIterator(SourceBuffer* aOwner) : mOwner(aOwner) , mState(START) + , mChunkCount(0) + , mByteCount(0) { MOZ_ASSERT(aOwner); mData.mIterating.mChunk = 0; mData.mIterating.mData = nullptr; mData.mIterating.mOffset = 0; - mData.mIterating.mLength = 0; + mData.mIterating.mAvailableLength = 0; + mData.mIterating.mNextReadLength = 0; } SourceBufferIterator(SourceBufferIterator&& aOther) : mOwner(Move(aOther.mOwner)) , mState(aOther.mState) , mData(aOther.mData) + , mChunkCount(aOther.mChunkCount) + , mByteCount(aOther.mByteCount) { } ~SourceBufferIterator(); - SourceBufferIterator& operator=(SourceBufferIterator&& aOther) - { - mOwner = Move(aOther.mOwner); - mState = aOther.mState; - mData = aOther.mData; - return *this; - } + SourceBufferIterator& operator=(SourceBufferIterator&& aOther); /** * Returns true if there are no more than @aBytes remaining in the * SourceBuffer. If the SourceBuffer is not yet complete, returns false. */ bool RemainingBytesIsNoMoreThan(size_t aBytes) const; /** - * Advances the iterator through the SourceBuffer if possible. If not, + * Advances the iterator through the SourceBuffer if possible. Advances no + * more than @aRequestedBytes bytes. (Use SIZE_MAX to advance as much as + * possible.) + * + * This is a wrapper around AdvanceOrScheduleResume() that makes it clearer at + * the callsite when the no resuming is intended. + * + * @return State::READY if the iterator was successfully advanced. + * State::WAITING if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer, but the SourceBuffer + * may still receive additional data. + * State::COMPLETE if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer and the SourceBuffer is + * marked complete (i.e., it will never receive any additional + * data). + */ + State Advance(size_t aRequestedBytes) + { + return AdvanceOrScheduleResume(aRequestedBytes, nullptr); + } + + /** + * Advances the iterator through the SourceBuffer if possible. Advances no + * more than @aRequestedBytes bytes. (Use SIZE_MAX to advance as much as + * possible.) If advancing is not possible and @aConsumer is not null, * arranges to call the @aConsumer's Resume() method when more data is * available. + * + * @return State::READY if the iterator was successfully advanced. + * State::WAITING if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer, but the SourceBuffer + * may still receive additional data. @aConsumer's Resume() method + * will be called when additional data is available. + * State::COMPLETE if the iterator could not be advanced because it's + * at the end of the underlying SourceBuffer and the SourceBuffer is + * marked complete (i.e., it will never receive any additional + * data). */ - State AdvanceOrScheduleResume(IResumable* aConsumer); + State AdvanceOrScheduleResume(size_t aRequestedBytes, IResumable* aConsumer); /// If at the end, returns the status passed to SourceBuffer::Complete(). nsresult CompletionStatus() const { MOZ_ASSERT(mState == COMPLETE, "Calling CompletionStatus() in the wrong state"); return mState == COMPLETE ? mData.mAtEnd.mStatus : NS_OK; } @@ -132,36 +165,66 @@ public: return mState == READY ? mData.mIterating.mData + mData.mIterating.mOffset : nullptr; } /// If we're ready to read, returns the length of the new data. size_t Length() const { MOZ_ASSERT(mState == READY, "Calling Length() in the wrong state"); - return mState == READY ? mData.mIterating.mLength : 0; + return mState == READY ? mData.mIterating.mNextReadLength : 0; } + /// @return a count of the chunks we've advanced through. + uint32_t ChunkCount() const { return mChunkCount; } + + /// @return a count of the bytes in all chunks we've advanced through. + size_t ByteCount() const { return mByteCount; } + private: friend class SourceBuffer; SourceBufferIterator(const SourceBufferIterator&) = delete; SourceBufferIterator& operator=(const SourceBufferIterator&) = delete; bool HasMore() const { return mState != COMPLETE; } + State AdvanceFromLocalBuffer(size_t aRequestedBytes) + { + MOZ_ASSERT(mState == READY, "Advancing in the wrong state"); + MOZ_ASSERT(mData.mIterating.mAvailableLength > 0, + "The local buffer shouldn't be empty"); + MOZ_ASSERT(mData.mIterating.mNextReadLength == 0, + "Advancing without consuming previous data"); + + mData.mIterating.mNextReadLength = + std::min(mData.mIterating.mAvailableLength, aRequestedBytes); + + return READY; + } + State SetReady(uint32_t aChunk, const char* aData, - size_t aOffset, size_t aLength) + size_t aOffset, size_t aAvailableLength, + size_t aRequestedBytes) { MOZ_ASSERT(mState != COMPLETE); + mState = READY; + + // Update state. mData.mIterating.mChunk = aChunk; mData.mIterating.mData = aData; mData.mIterating.mOffset = aOffset; - mData.mIterating.mLength = aLength; - return mState = READY; + mData.mIterating.mAvailableLength = aAvailableLength; + + // Update metrics. + mChunkCount++; + mByteCount += aAvailableLength; + + // Attempt to advance by the requested number of bytes. + return AdvanceFromLocalBuffer(aRequestedBytes); } State SetWaiting() { MOZ_ASSERT(mState != COMPLETE); MOZ_ASSERT(mState != WAITING, "Did we get a spurious wakeup somehow?"); return mState = WAITING; } @@ -181,22 +244,26 @@ private: * states START, READY, and WAITING) and the status the SourceBuffer was * completed with if we're in state COMPLETE. */ union { struct { uint32_t mChunk; const char* mData; size_t mOffset; - size_t mLength; + size_t mAvailableLength; + size_t mNextReadLength; } mIterating; struct { nsresult mStatus; } mAtEnd; } mData; + + uint32_t mChunkCount; // Count of chunks we've advanced through. + size_t mByteCount; // Count of bytes in all chunks we've advanced through. }; /** * SourceBuffer is a parallel data structure used for storing image source * (compressed) data. * * SourceBuffer is a single producer, multiple consumer data structure. The * single producer calls Append() to append data to the buffer. In parallel, @@ -251,16 +318,28 @@ public: ////////////////////////////////////////////////////////////////////////////// // Consumer methods. ////////////////////////////////////////////////////////////////////////////// /// Returns an iterator to this SourceBuffer. SourceBufferIterator Iterator(); + ////////////////////////////////////////////////////////////////////////////// + // Consumer methods. + ////////////////////////////////////////////////////////////////////////////// + + /** + * The minimum chunk capacity we'll allocate, if we don't know the correct + * capacity (which would happen because ExpectLength() wasn't called or gave + * us the wrong value). This is only exposed for use by tests; if normal code + * is using this, it's doing something wrong. + */ + static const size_t MIN_CHUNK_CAPACITY = 4096; + private: friend class SourceBufferIterator; ~SourceBuffer(); ////////////////////////////////////////////////////////////////////////////// // Chunk type and chunk-related methods. ////////////////////////////////////////////////////////////////////////////// @@ -332,16 +411,17 @@ private: ////////////////////////////////////////////////////////////////////////////// void AddWaitingConsumer(IResumable* aConsumer); void ResumeWaitingConsumers(); typedef SourceBufferIterator::State State; State AdvanceIteratorOrScheduleResume(SourceBufferIterator& aIterator, + size_t aRequestedBytes, IResumable* aConsumer); bool RemainingBytesIsNoMoreThan(const SourceBufferIterator& aIterator, size_t aBytes) const; void OnIteratorRelease(); ////////////////////////////////////////////////////////////////////////////// // Helper methods. @@ -351,18 +431,16 @@ private: bool IsEmpty(); bool IsLastChunk(uint32_t aChunk); ////////////////////////////////////////////////////////////////////////////// // Member variables. ////////////////////////////////////////////////////////////////////////////// - static const size_t MIN_CHUNK_CAPACITY = 4096; - /// All private members are protected by mMutex. mutable Mutex mMutex; /// The data in this SourceBuffer, stored as a series of Chunks. FallibleTArray<Chunk> mChunks; /// Consumers which are waiting to be notified when new data is available. nsTArray<RefPtr<IResumable>> mWaitingConsumers;
--- a/image/StreamingLexer.h +++ b/image/StreamingLexer.h @@ -9,39 +9,43 @@ * 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 <cstdint> #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/Maybe.h" #include "mozilla/Variant.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. }; -/// The result of a call to StreamingLexer::Lex(). +/// Possible terminal states for the lexer. enum class TerminalState { SUCCESS, FAILURE }; +/// The result of a call to StreamingLexer::Lex(). +typedef Variant<TerminalState> LexerResult; + /** * 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 @@ -253,161 +257,172 @@ private: * 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) + : mTransition(TerminalState::FAILURE) , mToReadUnbuffered(0) - { } + { + SetTransition(aStartState); + } template <typename Func> - Maybe<TerminalState> Lex(const char* aInput, size_t aLength, Func aFunc) + Maybe<TerminalState> Lex(SourceBufferIterator& aIterator, + IResumable* aOnResume, + Func aFunc) { if (mTransition.NextStateIsTerminal()) { // 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.NextStateAsTerminal()); } - if (mToReadUnbuffered > 0) { - // We're continuing an unbuffered read. + Maybe<LexerResult> result; + do { + // Figure out how much we need to read. + const size_t toRead = mTransition.Buffering() == BufferingStrategy::UNBUFFERED + ? mToReadUnbuffered + : mTransition.Size() - mBuffer.length(); + + // Attempt to advance the iterator by |toRead| bytes. + switch (aIterator.AdvanceOrScheduleResume(toRead, aOnResume)) { + case SourceBufferIterator::WAITING: + // We can't continue because the rest of the data hasn't arrived from + // the network yet. We don't have to do anything special; the + // SourceBufferIterator will ensure that |aOnResume| gets called when + // more data is available. + return Nothing(); + + case SourceBufferIterator::COMPLETE: + // Normally even if the data is truncated, we want decoding to + // succeed so we can display whatever we got. However, if the + // SourceBuffer was completed with a failing status, we want to fail. + // This happens only in exceptional situations like SourceBuffer + // itself encountering a failure due to OOM. + result = SetTransition(NS_SUCCEEDED(aIterator.CompletionStatus()) + ? Transition::TerminateSuccess() + : Transition::TerminateFailure()); + break; - MOZ_ASSERT(mBuffer.empty(), - "Shouldn't be continuing an unbuffered read and a buffered " - "read at the same time"); + case SourceBufferIterator::READY: + // Process the new data that became available. + MOZ_ASSERT(aIterator.Data()); + + result = mTransition.Buffering() == BufferingStrategy::UNBUFFERED + ? UnbufferedRead(aIterator, aFunc) + : BufferedRead(aIterator, aFunc); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown SourceBufferIterator state"); + result = SetTransition(Transition::TerminateFailure()); + } + } while (!result); - size_t toRead = std::min(mToReadUnbuffered, aLength); + // Map |LexerResult| onto the old |Maybe<TerminalState>| API. + return result->is<TerminalState>() ? Some(result->as<TerminalState>()) + : Nothing(); + } +private: + template <typename Func> + Maybe<LexerResult> UnbufferedRead(SourceBufferIterator& aIterator, Func aFunc) + { + MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::UNBUFFERED); + MOZ_ASSERT(mBuffer.empty(), + "Buffered read at the same time as unbuffered read?"); + + if (mToReadUnbuffered > 0) { // 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 state or takes us back to the // unbuffered state. LexerTransition<State> unbufferedTransition = - aFunc(mTransition.UnbufferedState(), aInput, toRead); + aFunc(mTransition.UnbufferedState(), aIterator.Data(), aIterator.Length()); if (unbufferedTransition.NextStateIsTerminal()) { - mTransition = unbufferedTransition; - return Some(mTransition.NextStateAsTerminal()); // Done! + return SetTransition(unbufferedTransition); } + MOZ_ASSERT(mTransition.UnbufferedState() == unbufferedTransition.NextState()); - aInput += toRead; - aLength -= toRead; - mToReadUnbuffered -= toRead; + mToReadUnbuffered -= aIterator.Length(); 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 (mTransition.NextStateIsTerminal()) { - return Some(mTransition.NextStateAsTerminal()); // 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()); - - if (!mBuffer.append(aInput, toRead)) { - return Some(TerminalState::FAILURE); - } - 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 (mTransition.NextStateIsTerminal()) { - return Some(mTransition.NextStateAsTerminal()); // Done! + return Nothing(); // Keep processing. } } - 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); + // We're done with the unbuffered read, so transition to the next state. + return SetTransition(aFunc(mTransition.NextState(), nullptr, 0)); + } - // 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 state or takes us back to the - // unbuffered state. - LexerTransition<State> unbufferedTransition = - aFunc(mTransition.UnbufferedState(), aInput, toRead); - if (unbufferedTransition.NextStateIsTerminal()) { - mTransition = unbufferedTransition; - return Some(mTransition.NextStateAsTerminal()); // Done! - } - MOZ_ASSERT(mTransition.UnbufferedState() == - unbufferedTransition.NextState()); + template <typename Func> + Maybe<LexerResult> BufferedRead(SourceBufferIterator& aIterator, Func aFunc) + { + MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::BUFFERED); + MOZ_ASSERT(mToReadUnbuffered == 0, + "Buffered read at the same time as unbuffered read?"); + MOZ_ASSERT(mBuffer.length() < mTransition.Size() || + (mBuffer.length() == 0 && mTransition.Size() == 0), + "Buffered more than we needed?"); - // We're done with the unbuffered read, so transition to the next state. - mTransition = aFunc(mTransition.NextState(), nullptr, 0); - } + // If we have all the data, we don't actually need to buffer anything. + if (mBuffer.empty() && aIterator.Length() == mTransition.Size()) { + return SetTransition(aFunc(mTransition.NextState(), + aIterator.Data(), + aIterator.Length())); + } - aInput += toRead; - aLength -= toRead; - - if (mTransition.NextStateIsTerminal()) { - return Some(mTransition.NextStateAsTerminal()); // Done! - } + // We do need to buffer, so make sure the buffer has enough capacity. We + // deliberately wait until we know for sure we need to buffer to call + // reserve() since it could require memory allocation. + if (!mBuffer.reserve(mTransition.Size())) { + return SetTransition(Transition::TerminateFailure()); } - if (aLength == 0) { - // We finished right at a transition point. Just wait for more data. - return Nothing(); + // Append the new data we just got to the buffer. + if (!mBuffer.append(aIterator.Data(), aIterator.Length())) { + return SetTransition(Transition::TerminateFailure()); + } + + if (mBuffer.length() != mTransition.Size()) { + return Nothing(); // Keep processing. } - // 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 (unbufferedTransition.NextStateIsTerminal()) { - mTransition = unbufferedTransition; - return Some(mTransition.NextStateAsTerminal()); // Done! - } - MOZ_ASSERT(mTransition.UnbufferedState() == - unbufferedTransition.NextState()); + // We've buffered everything, so transition to the next state. + return SetTransition(aFunc(mTransition.NextState(), + mBuffer.begin(), + mBuffer.length())); + } - mToReadUnbuffered = mTransition.Size() - aLength; - return Nothing(); // Need more input. + Maybe<LexerResult> SetTransition(const LexerTransition<State>& aTransition) + { + mTransition = aTransition; + + // Get rid of anything left over from the previous state. + mBuffer.clear(); + mToReadUnbuffered = 0; + + // If we reached a terminal state, let the caller know. + if (mTransition.NextStateIsTerminal()) { + return Some(LexerResult(mTransition.NextStateAsTerminal())); } - // 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(TerminalState::FAILURE); // Done due to allocation failure. + // If we're entering an unbuffered state, record how long we'll stay in it. + if (mTransition.Buffering() == BufferingStrategy::UNBUFFERED) { + mToReadUnbuffered = mTransition.Size(); } - if (!mBuffer.append(aInput, aLength)) { - return Some(TerminalState::FAILURE); - } - return Nothing(); // Need more input. + + return Nothing(); // Keep processing. } -private: Vector<char, InlineBufferSize> mBuffer; LexerTransition<State> mTransition; size_t mToReadUnbuffered; }; } // namespace image } // namespace mozilla
--- a/image/SurfaceCache.cpp +++ b/image/SurfaceCache.cpp @@ -655,16 +655,21 @@ public: return LookupResult(Move(ref), matchType); } bool CanHold(const Cost aCost) const { return aCost <= mMaxCost; } + size_t MaximumCapacity() const + { + return size_t(mMaxCost); + } + void LockImage(const ImageKey aImageKey) { RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey); if (!cache) { cache = new ImageSurfaceCache; mImageCaches.Put(aImageKey, cache); } @@ -1121,10 +1126,21 @@ SurfaceCache::CollectSizeOfSurfaces(cons if (!sInstance) { return; } MutexAutoLock lock(sInstance->GetMutex()); return sInstance->CollectSizeOfSurfaces(aImageKey, aCounters, aMallocSizeOf); } +/* static */ size_t +SurfaceCache::MaximumCapacity() +{ + if (!sInstance) { + return 0; + } + + MutexAutoLock lock(sInstance->GetMutex()); + return sInstance->MaximumCapacity(); +} + } // namespace image } // namespace mozilla
--- a/image/SurfaceCache.h +++ b/image/SurfaceCache.h @@ -394,16 +394,22 @@ struct SurfaceCache * @param aCounters An array into which the report for each surface will * be written. * @param aMallocSizeOf A fallback malloc memory reporting function. */ static void CollectSizeOfSurfaces(const ImageKey aImageKey, nsTArray<SurfaceMemoryCounter>& aCounters, MallocSizeOf aMallocSizeOf); + /** + * @return maximum capacity of the SurfaceCache in bytes. This is only exposed + * for use by tests; normal code should use CanHold() instead. + */ + static size_t MaximumCapacity(); + private: virtual ~SurfaceCache() = 0; // Forbid instantiation. }; } // namespace image } // namespace mozilla #endif // mozilla_image_SurfaceCache_h
--- a/image/decoders/nsBMPDecoder.cpp +++ b/image/decoders/nsBMPDecoder.cpp @@ -217,25 +217,27 @@ nsBMPDecoder::GetCompressedImageSize() c { // In the RGB case mImageSize might not be set, so compute it manually. MOZ_ASSERT(mPixelRowSize != 0); return mH.mCompression == Compression::RGB ? mPixelRowSize * AbsoluteHeight() : mH.mImageSize; } -void +nsresult nsBMPDecoder::BeforeFinishInternal() { if (!IsMetadataDecode() && !mImageData) { - PostDataError(); + return NS_ERROR_FAILURE; // No image; something went wrong. } + + return NS_OK; } -void +nsresult nsBMPDecoder::FinishInternal() { // We shouldn't be called in error cases. MOZ_ASSERT(!HasError(), "Can't call FinishInternal on error!"); // We should never make multiple frames. MOZ_ASSERT(GetFrameCount() <= 1, "Multiple BMP frames?"); @@ -255,24 +257,33 @@ nsBMPDecoder::FinishInternal() mCurrentPos = 0; FinishRow(); } // Invalidate. nsIntRect r(0, 0, mH.mWidth, AbsoluteHeight()); PostInvalidation(r); - if (mDoesHaveTransparency) { - MOZ_ASSERT(mMayHaveTransparency); - PostFrameStop(Opacity::SOME_TRANSPARENCY); - } else { - PostFrameStop(Opacity::FULLY_OPAQUE); - } + MOZ_ASSERT_IF(mDoesHaveTransparency, mMayHaveTransparency); + + // We have transparency if we either detected some in the image itself + // (i.e., |mDoesHaveTransparency| is true) or we're in an ICO, which could + // mean we have an AND mask that provides transparency (i.e., |mIsWithinICO| + // is true). + // XXX(seth): We can tell when we create the decoder if the AND mask is + // present, so we could be more precise about this. + const Opacity opacity = mDoesHaveTransparency || mIsWithinICO + ? Opacity::SOME_TRANSPARENCY + : Opacity::FULLY_OPAQUE; + + PostFrameStop(opacity); PostDecodeDone(); } + + return NS_OK; } // ---------------------------------------- // Actual Data Processing // ---------------------------------------- void BitFields::Value::Set(uint32_t aMask) @@ -431,23 +442,21 @@ nsBMPDecoder::FinishRow() } } else { PostInvalidation(IntRect(0, mCurrentRow, mH.mWidth, 1)); } mCurrentRow--; } Maybe<TerminalState> -nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator) +nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); - MOZ_ASSERT(aIterator.Data()); - MOZ_ASSERT(aIterator.Length() > 0); - return mLexer.Lex(aIterator.Data(), aIterator.Length(), + return mLexer.Lex(aIterator, aOnResume, [=](State aState, const char* aData, size_t aLength) { switch (aState) { case State::FILE_HEADER: return ReadFileHeader(aData, aLength); case State::INFO_HEADER_SIZE: return ReadInfoHeaderSize(aData, aLength); case State::INFO_HEADER_REST: return ReadInfoHeaderRest(aData, aLength); case State::BITFIELDS: return ReadBitfields(aData, aLength); case State::COLOR_TABLE: return ReadColorTable(aData, aLength); case State::GAP: return SkipGap(); @@ -464,17 +473,16 @@ nsBMPDecoder::DoDecode(SourceBufferItera LexerTransition<nsBMPDecoder::State> nsBMPDecoder::ReadFileHeader(const char* aData, size_t aLength) { mPreGapLength += aLength; bool signatureOk = aData[0] == 'B' && aData[1] == 'M'; if (!signatureOk) { - PostDataError(); return Transition::TerminateFailure(); } // We ignore the filesize (aData + 2) and reserved (aData + 6) fields. mH.mDataOffset = LittleEndian::readUint32(aData + 10); return Transition::To(State::INFO_HEADER_SIZE, BIHSIZE_FIELD_LENGTH); @@ -491,17 +499,16 @@ nsBMPDecoder::ReadInfoHeaderSize(const c bool bihSizeOk = mH.mBIHSize == InfoHeaderLength::WIN_V2 || mH.mBIHSize == InfoHeaderLength::WIN_V3 || mH.mBIHSize == InfoHeaderLength::WIN_V4 || mH.mBIHSize == InfoHeaderLength::WIN_V5 || (mH.mBIHSize >= InfoHeaderLength::OS2_V2_MIN && mH.mBIHSize <= InfoHeaderLength::OS2_V2_MAX); if (!bihSizeOk) { - PostDataError(); return Transition::TerminateFailure(); } // ICO BMPs must have a WinBMPv3 header. nsICODecoder should have already // terminated decoding if this isn't the case. MOZ_ASSERT_IF(mIsWithinICO, mH.mBIHSize == InfoHeaderLength::WIN_V3); return Transition::To(State::INFO_HEADER_REST, mH.mBIHSize - BIHSIZE_FIELD_LENGTH); @@ -546,17 +553,16 @@ nsBMPDecoder::ReadInfoHeaderRest(const c // BMPs with negative width are invalid. Also, reject extremely wide images // to keep the math sane. And reject INT_MIN as a height because you can't // get its absolute value (because -INT_MIN is one more than INT_MAX). const int32_t k64KWidth = 0x0000FFFF; bool sizeOk = 0 <= mH.mWidth && mH.mWidth <= k64KWidth && mH.mHeight != INT_MIN; if (!sizeOk) { - PostDataError(); return Transition::TerminateFailure(); } // Check mBpp and mCompression. bool bppCompressionOk = (mH.mCompression == Compression::RGB && (mH.mBpp == 1 || mH.mBpp == 4 || mH.mBpp == 8 || mH.mBpp == 16 || mH.mBpp == 24 || mH.mBpp == 32)) || @@ -565,24 +571,21 @@ nsBMPDecoder::ReadInfoHeaderRest(const c (mH.mCompression == Compression::BITFIELDS && // For BITFIELDS compression we require an exact match for one of the // WinBMP BIH sizes; this clearly isn't an OS2 BMP. (mH.mBIHSize == InfoHeaderLength::WIN_V3 || mH.mBIHSize == InfoHeaderLength::WIN_V4 || mH.mBIHSize == InfoHeaderLength::WIN_V5) && (mH.mBpp == 16 || mH.mBpp == 32)); if (!bppCompressionOk) { - PostDataError(); return Transition::TerminateFailure(); } - // Post our size to the superclass. - uint32_t absHeight = AbsoluteHeight(); - PostSize(mH.mWidth, absHeight); - mCurrentRow = absHeight; + // Initialize our current row to the top of the image. + mCurrentRow = AbsoluteHeight(); // Round it up to the nearest byte count, then pad to 4-byte boundary. // Compute this even for a metadate decode because GetCompressedImageSize() // relies on it. mPixelRowSize = (mH.mBpp * mH.mWidth + 7) / 8; uint32_t surplus = mPixelRowSize % 4; if (surplus != 0) { mPixelRowSize += 4 - surplus; @@ -640,16 +643,19 @@ nsBMPDecoder::ReadBitfields(const char* mH.mCompression == Compression::RLE8 || mH.mCompression == Compression::RLE4 || (mH.mCompression == Compression::BITFIELDS && mBitFields.mAlpha.IsPresent()); if (mMayHaveTransparency) { PostHasTransparency(); } + // Post our size to the superclass. + PostSize(mH.mWidth, AbsoluteHeight()); + // We've now read all the headers. If we're doing a metadata decode, we're // done. if (IsMetadataDecode()) { return Transition::TerminateSuccess(); } // Set up the color table, if present; it'll be filled in by ReadColorTable(). if (mH.mBpp <= 8) { @@ -710,17 +716,16 @@ nsBMPDecoder::ReadColorTable(const char* // We know how many bytes we've read so far (mPreGapLength) and we know the // offset of the pixel data (mH.mDataOffset), so we can determine the length // of the gap (possibly zero) between the color table and the pixel data. // // If the gap is negative the file must be malformed (e.g. mH.mDataOffset // points into the middle of the color palette instead of past the end) and // we give up. if (mPreGapLength > mH.mDataOffset) { - PostDataError(); return Transition::TerminateFailure(); } uint32_t gapLength = mH.mDataOffset - mPreGapLength; return Transition::ToUnbuffered(State::AFTER_GAP, State::GAP, gapLength); } LexerTransition<nsBMPDecoder::State>
--- a/image/decoders/nsBMPDecoder.h +++ b/image/decoders/nsBMPDecoder.h @@ -137,26 +137,20 @@ public: /// Mark this BMP as being within an ICO file. Only used for testing purposes /// because the ICO-specific constructor does this marking automatically. void SetIsWithinICO() { mIsWithinICO = true; } /// Did the BMP file have alpha data of any kind? (Only use this after the /// bitmap has been fully decoded.) bool HasTransparency() const { return mDoesHaveTransparency; } - /// Force transparency from outside. (Used by the ICO decoder.) - void SetHasTransparency() - { - mMayHaveTransparency = true; - mDoesHaveTransparency = true; - } - - Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override; - virtual void BeforeFinishInternal() override; - virtual void FinishInternal() override; + Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult BeforeFinishInternal() override; + nsresult FinishInternal() override; private: friend class DecoderFactory; enum class State { FILE_HEADER, INFO_HEADER_SIZE, INFO_HEADER_REST,
--- a/image/decoders/nsGIFDecoder2.cpp +++ b/image/decoders/nsGIFDecoder2.cpp @@ -95,29 +95,31 @@ nsGIFDecoder2::nsGIFDecoder2(RasterImage mGIFStruct.loop_count = 1; } nsGIFDecoder2::~nsGIFDecoder2() { free(mGIFStruct.local_colormap); } -void +nsresult nsGIFDecoder2::FinishInternal() { MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); // If the GIF got cut off, handle it anyway if (!IsMetadataDecode() && mGIFOpen) { if (mCurrentFrameIndex == mGIFStruct.images_decoded) { EndImageFrame(); } PostDecodeDone(mGIFStruct.loop_count - 1); mGIFOpen = false; } + + return NS_OK; } void nsGIFDecoder2::FlushImageData() { Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect(); if (!invalidRect) { return; @@ -450,23 +452,21 @@ ConvertColormap(uint32_t* aColormap, uin // NB: can't use 32-bit reads, they might read off the end of the buffer while (c--) { from -= 3; *--to = gfxPackedPixel(0xFF, from[0], from[1], from[2]); } } Maybe<TerminalState> -nsGIFDecoder2::DoDecode(SourceBufferIterator& aIterator) +nsGIFDecoder2::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); - MOZ_ASSERT(aIterator.Data()); - MOZ_ASSERT(aIterator.Length() > 0); - return mLexer.Lex(aIterator.Data(), aIterator.Length(), + return mLexer.Lex(aIterator, aOnResume, [=](State aState, const char* aData, size_t aLength) { switch(aState) { case State::GIF_HEADER: return ReadGIFHeader(aData); case State::SCREEN_DESCRIPTOR: return ReadScreenDescriptor(aData); case State::GLOBAL_COLOR_TABLE: return ReadGlobalColorTable(aData, aLength);
--- a/image/decoders/nsGIFDecoder2.h +++ b/image/decoders/nsGIFDecoder2.h @@ -19,18 +19,19 @@ class RasterImage; ////////////////////////////////////////////////////////////////////// // nsGIFDecoder2 Definition class nsGIFDecoder2 : public Decoder { public: ~nsGIFDecoder2(); - Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override; - virtual void FinishInternal() override; + Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; virtual Telemetry::ID SpeedHistogram() override; private: friend class DecoderFactory; // Decoders should only be instantiated via DecoderFactory. explicit nsGIFDecoder2(RasterImage* aImage);
--- a/image/decoders/nsICODecoder.cpp +++ b/image/decoders/nsICODecoder.cpp @@ -49,73 +49,73 @@ nsICODecoder::GetNumColors() } } return numColors; } nsICODecoder::nsICODecoder(RasterImage* aImage) : Decoder(aImage) , mLexer(Transition::To(ICOState::HEADER, ICOHEADERSIZE)) - , mDoNotResume(WrapNotNull(new DoNotResume)) , mBiggestResourceColorDepth(0) , mBestResourceDelta(INT_MIN) , mBestResourceColorDepth(0) , mNumIcons(0) , mCurrIcon(0) , mBPP(0) , mMaskRowSize(0) , mCurrMaskLine(0) , mIsCursor(false) , mHasMaskAlpha(false) { } -void +nsresult nsICODecoder::FinishInternal() { // We shouldn't be called in error cases MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!"); - GetFinalStateFromContainedDecoder(); + return GetFinalStateFromContainedDecoder(); } -void +nsresult nsICODecoder::FinishWithErrorInternal() { - GetFinalStateFromContainedDecoder(); + return GetFinalStateFromContainedDecoder(); } -void +nsresult nsICODecoder::GetFinalStateFromContainedDecoder() { if (!mContainedDecoder) { - return; + return NS_OK; } MOZ_ASSERT(mContainedSourceBuffer, "Should have a SourceBuffer if we have a decoder"); // Let the contained decoder finish up if necessary. if (!mContainedSourceBuffer->IsComplete()) { mContainedSourceBuffer->Complete(NS_OK); - if (NS_FAILED(mContainedDecoder->Decode(mDoNotResume))) { - PostDataError(); - } + mContainedDecoder->Decode(); } // Make our state the same as the state of the contained decoder. mDecodeDone = mContainedDecoder->GetDecodeDone(); - mDataError = mDataError || mContainedDecoder->HasDataError(); - mFailCode = NS_SUCCEEDED(mFailCode) ? mContainedDecoder->GetDecoderError() - : mFailCode; mDecodeAborted = mContainedDecoder->WasAborted(); mProgress |= mContainedDecoder->TakeProgress(); mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect()); mCurrentFrame = mContainedDecoder->GetCurrentFrameRef(); - MOZ_ASSERT(HasError() || !mCurrentFrame || mCurrentFrame->IsFinished()); + // Propagate errors. + nsresult rv = HasError() || mContainedDecoder->HasError() + ? NS_ERROR_FAILURE + : NS_OK; + + MOZ_ASSERT(NS_FAILED(rv) || !mCurrentFrame || mCurrentFrame->IsFinished()); + return rv; } bool nsICODecoder::CheckAndFixBitmapSize(int8_t* aBIH) { // Get the width from the BMP file information header. This is // (unintuitively) a signed integer; see the documentation at: // @@ -567,25 +567,16 @@ nsICODecoder::FinishMask() // Iterate through the alpha values, copying from mask to image. MOZ_ASSERT(mMaskBuffer); MOZ_ASSERT(bmpDecoder->GetImageDataLength() > 0); for (size_t i = 3 ; i < bmpDecoder->GetImageDataLength() ; i += 4) { imageData[i] = mMaskBuffer[i]; } } - // If the mask contained any transparent pixels, record that fact. - if (mHasMaskAlpha) { - PostHasTransparency(); - - RefPtr<nsBMPDecoder> bmpDecoder = - static_cast<nsBMPDecoder*>(mContainedDecoder.get()); - bmpDecoder->SetHasTransparency(); - } - return Transition::To(ICOState::FINISHED_RESOURCE, 0); } LexerTransition<ICOState> nsICODecoder::FinishResource() { // Make sure the actual size of the resource matches the size in the directory // entry. If not, we consider the image corrupt. @@ -593,23 +584,21 @@ nsICODecoder::FinishResource() mContainedDecoder->GetSize() != GetRealSize()) { return Transition::TerminateFailure(); } return Transition::TerminateSuccess(); } Maybe<TerminalState> -nsICODecoder::DoDecode(SourceBufferIterator& aIterator) +nsICODecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); - MOZ_ASSERT(aIterator.Data()); - MOZ_ASSERT(aIterator.Length() > 0); - return mLexer.Lex(aIterator.Data(), aIterator.Length(), + return mLexer.Lex(aIterator, aOnResume, [=](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); @@ -644,31 +633,31 @@ nsICODecoder::WriteToContainedDecoder(co { MOZ_ASSERT(mContainedDecoder); MOZ_ASSERT(mContainedSourceBuffer); // Append the provided data to the SourceBuffer that the contained decoder is // reading from. mContainedSourceBuffer->Append(aBuffer, aCount); + bool succeeded = true; + // Write to the contained decoder. If we run out of data, the ICO decoder will // get resumed when there's more data available, as usual, so we don't need // the contained decoder to get resumed too. To avoid that, we provide an // IResumable which just does nothing. - if (NS_FAILED(mContainedDecoder->Decode(mDoNotResume))) { - PostDataError(); + if (NS_FAILED(mContainedDecoder->Decode())) { + succeeded = false; } - // Make our state the same as the state of the contained decoder. + // Make our state the same as the state of the contained decoder, and + // propagate errors. mProgress |= mContainedDecoder->TakeProgress(); mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect()); - if (mContainedDecoder->HasDataError()) { - PostDataError(); - } - if (mContainedDecoder->HasDecoderError()) { - PostDecoderError(mContainedDecoder->GetDecoderError()); + if (mContainedDecoder->HasError()) { + succeeded = false; } - return !HasError(); + return succeeded; } } // namespace image } // namespace mozilla
--- a/image/decoders/nsICODecoder.h +++ b/image/decoders/nsICODecoder.h @@ -6,17 +6,16 @@ #ifndef mozilla_image_decoders_nsICODecoder_h #define mozilla_image_decoders_nsICODecoder_h #include "StreamingLexer.h" #include "Decoder.h" #include "imgFrame.h" #include "mozilla/gfx/2D.h" -#include "mozilla/NotNull.h" #include "nsBMPDecoder.h" #include "nsPNGDecoder.h" #include "ICOFileHeaders.h" namespace mozilla { namespace image { class RasterImage; @@ -65,32 +64,33 @@ public: gfx::IntSize GetRealSize() const { return gfx::IntSize(GetRealWidth(), GetRealHeight()); } /// @return The offset from the beginning of the ICO to the first resource. size_t FirstResourceOffset() const; - Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override; - virtual void FinishInternal() override; - virtual void FinishWithErrorInternal() override; + Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; + nsresult FinishWithErrorInternal() override; private: friend class DecoderFactory; // Decoders should only be instantiated via DecoderFactory. explicit nsICODecoder(RasterImage* aImage); // Writes to the contained decoder and sets the appropriate errors // Returns true if there are no errors. bool WriteToContainedDecoder(const char* aBuffer, uint32_t aCount); // Gets decoder state from the contained decoder so it's visible externally. - void GetFinalStateFromContainedDecoder(); + nsresult GetFinalStateFromContainedDecoder(); /** * Verifies that the width and height values in @aBIH are valid and match the * values we read from the ICO directory entry. If everything looks OK, the * height value in @aBIH is updated to compensate for the AND mask, which the * underlying BMP decoder doesn't know about. * * @return true if the width and height values in @aBIH are valid and correct. @@ -106,32 +106,19 @@ private: 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); LexerTransition<ICOState> FinishMask(); LexerTransition<ICOState> FinishResource(); - // A helper implementation of IResumable which just does nothing; see - // WriteToContainedDecoder() for more details. - class DoNotResume final : public IResumable - { - public: - NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DoNotResume, override) - void Resume() override { } - - private: - virtual ~DoNotResume() { } - }; - StreamingLexer<ICOState, 32> mLexer; // The lexer. RefPtr<Decoder> mContainedDecoder; // Either a BMP or PNG decoder. RefPtr<SourceBuffer> mContainedSourceBuffer; // SourceBuffer for mContainedDecoder. - NotNull<RefPtr<IResumable>> mDoNotResume; // IResumable helper for SourceBuffer. UniquePtr<uint8_t[]> mMaskBuffer; // A temporary buffer for the alpha mask. char mBIHraw[bmp::InfoHeaderLength::WIN_ICO]; // The bitmap information header. IconDirEntry mDirEntry; // The dir entry for the selected resource. gfx::IntSize mBiggestResourceSize; // Used to select the intrinsic size. gfx::IntSize mBiggestResourceHotSpot; // Used to select the intrinsic size. uint16_t mBiggestResourceColorDepth; // Used to select the intrinsic size. int32_t mBestResourceDelta; // Used to select the best resource. uint16_t mBestResourceColorDepth; // Used to select the best resource.
--- a/image/decoders/nsIconDecoder.cpp +++ b/image/decoders/nsIconDecoder.cpp @@ -22,23 +22,21 @@ nsIconDecoder::nsIconDecoder(RasterImage { // Nothing to do } nsIconDecoder::~nsIconDecoder() { } Maybe<TerminalState> -nsIconDecoder::DoDecode(SourceBufferIterator& aIterator) +nsIconDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); - MOZ_ASSERT(aIterator.Data()); - MOZ_ASSERT(aIterator.Length() > 0); - return mLexer.Lex(aIterator.Data(), aIterator.Length(), + return mLexer.Lex(aIterator, aOnResume, [=](State aState, const char* aData, size_t aLength) { switch (aState) { case State::HEADER: return ReadHeader(aData); case State::ROW_OF_PIXELS: return ReadRowOfPixels(aData, aLength); case State::FINISH: return Finish();
--- a/image/decoders/nsIconDecoder.h +++ b/image/decoders/nsIconDecoder.h @@ -32,17 +32,18 @@ class RasterImage; // //////////////////////////////////////////////////////////////////////////////// class nsIconDecoder : public Decoder { public: virtual ~nsIconDecoder(); - Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override; + Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; private: friend class DecoderFactory; // Decoders should only be instantiated via DecoderFactory. explicit nsIconDecoder(RasterImage* aImage); enum class State {
--- a/image/decoders/nsJPEGDecoder.cpp +++ b/image/decoders/nsJPEGDecoder.cpp @@ -123,34 +123,33 @@ nsJPEGDecoder::~nsJPEGDecoder() } Telemetry::ID nsJPEGDecoder::SpeedHistogram() { return Telemetry::IMAGE_DECODE_SPEED_JPEG; } -void +nsresult nsJPEGDecoder::InitInternal() { mCMSMode = gfxPlatform::GetCMSMode(); if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) { mCMSMode = eCMSMode_Off; } // We set up the normal JPEG error routines, then override error_exit. mInfo.err = jpeg_std_error(&mErr.pub); // mInfo.err = jpeg_std_error(&mErr.pub); mErr.pub.error_exit = my_error_exit; // Establish the setjmp return context for my_error_exit to use. if (setjmp(mErr.setjmp_buffer)) { - // If we get here, the JPEG code has signaled an error. - // We need to clean up the JPEG object, close the input file, and return. - PostDecoderError(NS_ERROR_FAILURE); - return; + // If we get here, the JPEG code has signaled an error, and initialization + // has failed. + return NS_ERROR_FAILURE; } // Step 1: allocate and initialize JPEG decompression object jpeg_create_decompress(&mInfo); // Set the source manager mInfo.src = &mSourceMgr; // Step 2: specify data source (eg, a file) @@ -161,37 +160,39 @@ nsJPEGDecoder::InitInternal() mSourceMgr.skip_input_data = skip_input_data; mSourceMgr.resync_to_restart = jpeg_resync_to_restart; mSourceMgr.term_source = term_source; // Record app markers for ICC data for (uint32_t m = 0; m < 16; m++) { jpeg_save_markers(&mInfo, JPEG_APP0 + m, 0xFFFF); } + + return NS_OK; } -void +nsresult nsJPEGDecoder::FinishInternal() { // If we're not in any sort of error case, force our state to JPEG_DONE. if ((mState != JPEG_DONE && mState != JPEG_SINK_NON_JPEG_TRAILER) && (mState != JPEG_ERROR) && !IsMetadataDecode()) { mState = JPEG_DONE; } + + return NS_OK; } Maybe<TerminalState> -nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator) +nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); - MOZ_ASSERT(aIterator.Data()); - MOZ_ASSERT(aIterator.Length() > 0); - return mLexer.Lex(aIterator.Data(), aIterator.Length(), + return mLexer.Lex(aIterator, aOnResume, [=](State aState, const char* aData, size_t aLength) { switch (aState) { case State::JPEG_DATA: return ReadJPEGData(aData, aLength); case State::FINISHED_JPEG_DATA: return FinishedJPEGData(); } MOZ_CRASH("Unknown State"); @@ -212,17 +213,16 @@ nsJPEGDecoder::ReadJPEGData(const char* if (error_code == NS_ERROR_FAILURE) { // Error due to corrupt data. Make sure that we don't feed any more data // to libjpeg-turbo. mState = JPEG_SINK_NON_JPEG_TRAILER; MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, ("} (setjmp returned NS_ERROR_FAILURE)")); } else { // Error for another reason. (Possibly OOM.) - PostDecoderError(error_code); mState = JPEG_ERROR; MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, ("} (setjmp returned an error)")); } return Transition::TerminateFailure(); } @@ -296,34 +296,32 @@ nsJPEGDecoder::ReadJPEGData(const char* break; case JCS_CMYK: case JCS_YCCK: // qcms doesn't support cmyk mismatch = true; break; default: mState = JPEG_ERROR; - PostDataError(); MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, ("} (unknown colorpsace (1))")); return Transition::TerminateFailure(); } if (!mismatch) { qcms_data_type type; switch (mInfo.out_color_space) { case JCS_GRAYSCALE: type = QCMS_DATA_GRAY_8; break; case JCS_RGB: type = QCMS_DATA_RGB_8; break; default: mState = JPEG_ERROR; - PostDataError(); MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, ("} (unknown colorpsace (2))")); return Transition::TerminateFailure(); } #if 0 // We don't currently support CMYK profiles. The following // code dealt with lcms types. Add something like this // back when we gain support for CMYK. @@ -372,17 +370,16 @@ nsJPEGDecoder::ReadJPEGData(const char* break; case JCS_CMYK: case JCS_YCCK: // libjpeg can convert from YCCK to CMYK, but not to RGB mInfo.out_color_space = JCS_CMYK; break; default: mState = JPEG_ERROR; - PostDataError(); MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug, ("} (unknown colorpsace (3))")); return Transition::TerminateFailure(); } } // Don't allocate a giant and superfluous memory buffer // when not doing a progressive decode.
--- a/image/decoders/nsJPEGDecoder.h +++ b/image/decoders/nsJPEGDecoder.h @@ -52,19 +52,20 @@ class nsJPEGDecoder : public Decoder public: virtual ~nsJPEGDecoder(); virtual void SetSampleSize(int aSampleSize) override { mSampleSize = aSampleSize; } - virtual void InitInternal() override; - Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override; - virtual void FinishInternal() override; + nsresult InitInternal() override; + Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; + nsresult FinishInternal() override; virtual Telemetry::ID SpeedHistogram() override; void NotifyDone(); protected: Orientation ReadOrientationFromEXIF(); void OutputScanlines(bool* suspend);
--- a/image/decoders/nsPNGDecoder.cpp +++ b/image/decoders/nsPNGDecoder.cpp @@ -257,17 +257,17 @@ nsPNGDecoder::EndImageFrame() if (format == gfx::SurfaceFormat::B8G8R8X8) { opacity = Opacity::FULLY_OPAQUE; } PostFrameStop(opacity, mAnimInfo.mDispose, mAnimInfo.mTimeout, mAnimInfo.mBlend, Some(mFrameRect)); } -void +nsresult nsPNGDecoder::InitInternal() { mCMSMode = gfxPlatform::GetCMSMode(); if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) { mCMSMode = eCMSMode_Off; } mDisablePremultipliedAlpha = bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA); @@ -293,25 +293,23 @@ nsPNGDecoder::InitInternal() // Initialize the container's source image header // Always decode to 24 bit pixdepth mPNG = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nsPNGDecoder::error_callback, nsPNGDecoder::warning_callback); if (!mPNG) { - PostDecoderError(NS_ERROR_OUT_OF_MEMORY); - return; + return NS_ERROR_OUT_OF_MEMORY; } mInfo = png_create_info_struct(mPNG); if (!mInfo) { - PostDecoderError(NS_ERROR_OUT_OF_MEMORY); png_destroy_read_struct(&mPNG, nullptr, nullptr); - return; + return NS_ERROR_OUT_OF_MEMORY; } #ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED // Ignore unused chunks if (mCMSMode == eCMSMode_Off || IsMetadataDecode()) { png_set_keep_unknown_chunks(mPNG, 1, color_chunks, 2); } @@ -341,26 +339,25 @@ nsPNGDecoder::InitInternal() #endif // use this as libpng "progressive pointer" (retrieve in callbacks) png_set_progressive_read_fn(mPNG, static_cast<png_voidp>(this), nsPNGDecoder::info_callback, nsPNGDecoder::row_callback, nsPNGDecoder::end_callback); + return NS_OK; } Maybe<TerminalState> -nsPNGDecoder::DoDecode(SourceBufferIterator& aIterator) +nsPNGDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume) { MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!"); - MOZ_ASSERT(aIterator.Data()); - MOZ_ASSERT(aIterator.Length() > 0); - return mLexer.Lex(aIterator.Data(), aIterator.Length(), + return mLexer.Lex(aIterator, aOnResume, [=](State aState, const char* aData, size_t aLength) { switch (aState) { case State::PNG_DATA: return ReadPNGData(aData, aLength); case State::FINISHED_PNG_DATA: return FinishedPNGData(); } MOZ_CRASH("Unknown State");
--- a/image/decoders/nsPNGDecoder.h +++ b/image/decoders/nsPNGDecoder.h @@ -17,18 +17,19 @@ namespace mozilla { namespace image { class RasterImage; class nsPNGDecoder : public Decoder { public: virtual ~nsPNGDecoder(); - virtual void InitInternal() override; - Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override; + nsresult InitInternal() override; + Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator, + IResumable* aOnResume) override; virtual Telemetry::ID SpeedHistogram() override; /// @return true if this PNG is a valid ICO resource. bool IsValidICO() const; private: friend class DecoderFactory;
--- a/image/test/gtest/Common.cpp +++ b/image/test/gtest/Common.cpp @@ -528,16 +528,25 @@ ImageTestCase GreenFirstFrameAnimatedPNG } ImageTestCase CorruptTestCase() { return ImageTestCase("corrupt.jpg", "image/jpeg", IntSize(100, 100), TEST_CASE_HAS_ERROR); } +ImageTestCase CorruptBMPWithTruncatedHeader() +{ + // This BMP has a header which is truncated right between the BIH and the + // bitfields, which is a particularly error-prone place w.r.t. the BMP decoder + // state machine. + return ImageTestCase("invalid-truncated-metadata.bmp", "image/bmp", + IntSize(100, 100), TEST_CASE_HAS_ERROR); +} + ImageTestCase CorruptICOWithBadBMPWidthTestCase() { // This ICO contains a BMP icon which has a width that doesn't match the size // listed in the corresponding ICO directory entry. return ImageTestCase("corrupt-with-bad-bmp-width.ico", "image/x-icon", IntSize(100, 100), TEST_CASE_HAS_ERROR); }
--- a/image/test/gtest/Common.h +++ b/image/test/gtest/Common.h @@ -10,16 +10,17 @@ #include "gtest/gtest.h" #include "mozilla/Maybe.h" #include "mozilla/UniquePtr.h" #include "mozilla/gfx/2D.h" #include "Decoder.h" #include "gfxColor.h" +#include "imgITools.h" #include "nsCOMPtr.h" #include "SurfacePipe.h" #include "SurfacePipeFactory.h" class nsIInputStream; namespace mozilla { namespace image { @@ -94,16 +95,33 @@ struct BGRAColor uint8_t mAlpha; }; /////////////////////////////////////////////////////////////////////////////// // General Helpers /////////////////////////////////////////////////////////////////////////////// +/** + * A RAII class that ensure that ImageLib services are available. Any tests that + * require ImageLib to be initialized (for example, any test that uses the + * SurfaceCache; see image::EnsureModuleInitialized() for the full list) can + * use this class to ensure that ImageLib services are available. Failure to do + * so can result in strange, non-deterministic failures. + */ +struct AutoInitializeImageLib +{ + AutoInitializeImageLib() + { + // Ensure that ImageLib services are initialized. + nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); + EXPECT_TRUE(imgTools != nullptr); + } +}; + /// Loads a file from the current directory. @return an nsIInputStream for it. already_AddRefed<nsIInputStream> LoadFile(const char* aRelativePath); /** * @returns true if every pixel of @aSurface is @aColor. * * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color * component. This may be necessary for tests that involve JPEG images or @@ -165,16 +183,47 @@ bool PalettedRectIsSolidColor(Decoder* a /** * @returns true if the pixels in @aRow of @aSurface match the pixels given in * @aPixels. */ bool RowHasPixels(gfx::SourceSurface* aSurface, int32_t aRow, const std::vector<BGRAColor>& aPixels); +// ExpectNoResume is an IResumable implementation for use by tests that expect +// Resume() to never get called. +class ExpectNoResume final : public IResumable +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExpectNoResume, override) + + void Resume() override { FAIL() << "Resume() should not get called"; } + +private: + ~ExpectNoResume() override { } +}; + +// CountResumes is an IResumable implementation for use by tests that expect +// Resume() to get called a certain number of times. +class CountResumes : public IResumable +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CountResumes, override) + + CountResumes() : mCount(0) { } + + void Resume() override { mCount++; } + uint32_t Count() const { return mCount; } + +private: + ~CountResumes() override { } + + uint32_t mCount; +}; + /////////////////////////////////////////////////////////////////////////////// // SurfacePipe Helpers /////////////////////////////////////////////////////////////////////////////// /** * Creates a decoder with no data associated with, suitable for testing code * that requires a decoder to initialize or to allocate surfaces but doesn't @@ -335,16 +384,17 @@ ImageTestCase GreenJPGTestCase(); ImageTestCase GreenBMPTestCase(); ImageTestCase GreenICOTestCase(); ImageTestCase GreenIconTestCase(); ImageTestCase GreenFirstFrameAnimatedGIFTestCase(); ImageTestCase GreenFirstFrameAnimatedPNGTestCase(); ImageTestCase CorruptTestCase(); +ImageTestCase CorruptBMPWithTruncatedHeader(); ImageTestCase CorruptICOWithBadBMPWidthTestCase(); ImageTestCase CorruptICOWithBadBMPHeightTestCase(); ImageTestCase TransparentPNGTestCase(); ImageTestCase TransparentGIFTestCase(); ImageTestCase FirstFramePaddingGIFTestCase(); ImageTestCase NoFrameDelayGIFTestCase(); ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase();
--- a/image/test/gtest/TestDecodeToSurface.cpp +++ b/image/test/gtest/TestDecodeToSurface.cpp @@ -82,23 +82,18 @@ RunDecodeToSurface(const ImageTestCase& thread->Shutdown(); // Explicitly release the SourceSurface on the main thread. surface = nullptr; } class ImageDecodeToSurface : public ::testing::Test { - protected: - static void SetUpTestCase() - { - // Ensure that ImageLib services are initialized. - nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); - EXPECT_TRUE(imgTools != nullptr); - } +protected: + AutoInitializeImageLib mInit; }; TEST_F(ImageDecodeToSurface, PNG) { RunDecodeToSurface(GreenPNGTestCase()); } TEST_F(ImageDecodeToSurface, GIF) { RunDecodeToSurface(GreenGIFTestCase()); } TEST_F(ImageDecodeToSurface, JPG) { RunDecodeToSurface(GreenJPGTestCase()); } TEST_F(ImageDecodeToSurface, BMP) { RunDecodeToSurface(GreenBMPTestCase()); } TEST_F(ImageDecodeToSurface, ICO) { RunDecodeToSurface(GreenICOTestCase()); } TEST_F(ImageDecodeToSurface, Icon) { RunDecodeToSurface(GreenIconTestCase()); }
--- a/image/test/gtest/TestDecoders.cpp +++ b/image/test/gtest/TestDecoders.cpp @@ -201,23 +201,18 @@ CheckDownscaleDuringDecode(const ImageTe EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 27)); EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 47)); EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 27)); }); } class ImageDecoders : public ::testing::Test { - protected: - static void SetUpTestCase() - { - // Ensure that ImageLib services are initialized. - nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); - EXPECT_TRUE(imgTools != nullptr); - } +protected: + AutoInitializeImageLib mInit; }; TEST_F(ImageDecoders, PNGSingleChunk) { CheckDecoderSingleChunk(GreenPNGTestCase()); } TEST_F(ImageDecoders, PNGMultiChunk) @@ -335,16 +330,26 @@ TEST_F(ImageDecoders, CorruptSingleChunk CheckDecoderSingleChunk(CorruptTestCase()); } TEST_F(ImageDecoders, CorruptMultiChunk) { CheckDecoderMultiChunk(CorruptTestCase()); } +TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderSingleChunk) +{ + CheckDecoderSingleChunk(CorruptBMPWithTruncatedHeader()); +} + +TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderMultiChunk) +{ + CheckDecoderMultiChunk(CorruptBMPWithTruncatedHeader()); +} + TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthSingleChunk) { CheckDecoderSingleChunk(CorruptICOWithBadBMPWidthTestCase()); } TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthMultiChunk) { CheckDecoderMultiChunk(CorruptICOWithBadBMPWidthTestCase());
--- a/image/test/gtest/TestDeinterlacingFilter.cpp +++ b/image/test/gtest/TestDeinterlacingFilter.cpp @@ -56,23 +56,18 @@ AssertConfiguringDeinterlacingFilterFail AssertConfiguringPipelineFails(decoder, DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true}, SurfaceConfig { decoder, 0, aSize, SurfaceFormat::B8G8R8A8, false }); } class ImageDeinterlacingFilter : public ::testing::Test { - protected: - static void SetUpTestCase() - { - // Ensure that ImageLib services are initialized. - nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); - EXPECT_TRUE(imgTools != nullptr); - } +protected: + AutoInitializeImageLib mInit; }; TEST_F(ImageDeinterlacingFilter, WritePixels100_100) { WithDeinterlacingFilter(IntSize(100, 100), /* aProgressiveDisplay = */ true, [](Decoder* aDecoder, SurfaceFilter* aFilter) { CheckWritePixels(aDecoder, aFilter, /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
--- a/image/test/gtest/TestMetadata.cpp +++ b/image/test/gtest/TestMetadata.cpp @@ -130,23 +130,18 @@ CheckMetadata(const ImageTestCase& aTest // discover during the metadata decode, unless the image is animated. EXPECT_TRUE(!(fullProgress & FLAG_HAS_TRANSPARENCY) || (metadataProgress & FLAG_HAS_TRANSPARENCY) || (fullProgress & FLAG_IS_ANIMATED)); } class ImageDecoderMetadata : public ::testing::Test { - protected: - static void SetUpTestCase() - { - // Ensure that ImageLib services are initialized. - nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); - EXPECT_TRUE(imgTools != nullptr); - } +protected: + AutoInitializeImageLib mInit; }; TEST_F(ImageDecoderMetadata, PNG) { CheckMetadata(GreenPNGTestCase()); } TEST_F(ImageDecoderMetadata, TransparentPNG) { CheckMetadata(TransparentPNGTestCase()); } TEST_F(ImageDecoderMetadata, GIF) { CheckMetadata(GreenGIFTestCase()); } TEST_F(ImageDecoderMetadata, TransparentGIF) { CheckMetadata(TransparentGIFTestCase()); } TEST_F(ImageDecoderMetadata, JPG) { CheckMetadata(GreenJPGTestCase()); } TEST_F(ImageDecoderMetadata, BMP) { CheckMetadata(GreenBMPTestCase()); }
new file mode 100644 --- /dev/null +++ b/image/test/gtest/TestSourceBuffer.cpp @@ -0,0 +1,810 @@ +/* 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 <algorithm> +#include <cstdint> + +#include "mozilla/Move.h" +#include "SourceBuffer.h" +#include "SurfaceCache.h" + +using namespace mozilla; +using namespace mozilla::image; + +using std::min; + +void +ExpectChunkAndByteCount(const SourceBufferIterator& aIterator, + uint32_t aChunks, + size_t aBytes) +{ + EXPECT_EQ(aChunks, aIterator.ChunkCount()); + EXPECT_EQ(aBytes, aIterator.ByteCount()); +} + +void +ExpectRemainingBytes(const SourceBufferIterator& aIterator, size_t aBytes) +{ + EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes)); + EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes + 1)); + + if (aBytes > 0) { + EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(0)); + EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(aBytes - 1)); + } +} + +char +GenerateByte(size_t aIndex) +{ + uint8_t byte = aIndex % 256; + return *reinterpret_cast<char*>(&byte); +} + +void +GenerateData(char* aOutput, size_t aOffset, size_t aLength) +{ + for (size_t i = 0; i < aLength; ++i) { + aOutput[i] = GenerateByte(aOffset + i); + } +} + +void +GenerateData(char* aOutput, size_t aLength) +{ + GenerateData(aOutput, 0, aLength); +} + +void +CheckData(const char* aData, size_t aOffset, size_t aLength) +{ + for (size_t i = 0; i < aLength; ++i) { + ASSERT_EQ(GenerateByte(aOffset + i), aData[i]); + } +} + +enum class AdvanceMode +{ + eAdvanceAsMuchAsPossible, + eAdvanceByLengthExactly +}; + +class ImageSourceBuffer : public ::testing::Test +{ +public: + ImageSourceBuffer() + : mSourceBuffer(new SourceBuffer) + , mExpectNoResume(new ExpectNoResume) + , mCountResumes(new CountResumes) + { + GenerateData(mData, sizeof(mData)); + EXPECT_FALSE(mSourceBuffer->IsComplete()); + } + +protected: + void CheckedAppendToBuffer(const char* aData, size_t aLength) + { + EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->Append(aData, aLength))); + } + + void CheckedAppendToBufferLastByteForLength(size_t aLength) + { + const char lastByte = GenerateByte(aLength); + CheckedAppendToBuffer(&lastByte, 1); + } + + void CheckedAppendToBufferInChunks(size_t aChunkLength, size_t aTotalLength) + { + char* data = new char[aChunkLength]; + + size_t bytesWritten = 0; + while (bytesWritten < aTotalLength) { + GenerateData(data, bytesWritten, aChunkLength); + size_t toWrite = min(aChunkLength, aTotalLength - bytesWritten); + CheckedAppendToBuffer(data, toWrite); + bytesWritten += toWrite; + } + + delete[] data; + } + + void CheckedCompleteBuffer(nsresult aCompletionStatus = NS_OK) + { + mSourceBuffer->Complete(aCompletionStatus); + EXPECT_TRUE(mSourceBuffer->IsComplete()); + } + + void CheckedCompleteBuffer(SourceBufferIterator& aIterator, + size_t aLength, + nsresult aCompletionStatus = NS_OK) + { + CheckedCompleteBuffer(aCompletionStatus); + ExpectRemainingBytes(aIterator, aLength); + } + + void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator, + size_t aLength, + uint32_t aChunks, + size_t aTotalLength, + AdvanceMode aAdvanceMode + = AdvanceMode::eAdvanceAsMuchAsPossible) + { + const size_t advanceBy = aAdvanceMode == AdvanceMode::eAdvanceAsMuchAsPossible + ? SIZE_MAX + : aLength; + + auto state = aIterator.AdvanceOrScheduleResume(advanceBy, mExpectNoResume); + ASSERT_EQ(SourceBufferIterator::READY, state); + EXPECT_TRUE(aIterator.Data()); + EXPECT_EQ(aLength, aIterator.Length()); + + ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength); + } + + void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator, + size_t aLength) + { + CheckedAdvanceIteratorStateOnly(aIterator, aLength, 1, aLength); + } + + void CheckedAdvanceIterator(SourceBufferIterator& aIterator, + size_t aLength, + uint32_t aChunks, + size_t aTotalLength, + AdvanceMode aAdvanceMode + = AdvanceMode::eAdvanceAsMuchAsPossible) + { + // Check that the iterator is in the expected state. + CheckedAdvanceIteratorStateOnly(aIterator, aLength, aChunks, + aTotalLength, aAdvanceMode); + + // Check that we read the expected data. To do this, we need to compute our + // offset in the SourceBuffer, but fortunately that's pretty easy: it's the + // total number of bytes the iterator has advanced through, minus the length + // of the current chunk. + const size_t offset = aIterator.ByteCount() - aIterator.Length(); + CheckData(aIterator.Data(), offset, aIterator.Length()); + } + + void CheckedAdvanceIterator(SourceBufferIterator& aIterator, size_t aLength) + { + CheckedAdvanceIterator(aIterator, aLength, 1, aLength); + } + + void CheckIteratorMustWait(SourceBufferIterator& aIterator, + IResumable* aOnResume) + { + auto state = aIterator.AdvanceOrScheduleResume(1, aOnResume); + EXPECT_EQ(SourceBufferIterator::WAITING, state); + } + + void CheckIteratorIsComplete(SourceBufferIterator& aIterator, + uint32_t aChunks, + size_t aTotalLength, + nsresult aCompletionStatus = NS_OK) + { + ASSERT_TRUE(mSourceBuffer->IsComplete()); + auto state = aIterator.AdvanceOrScheduleResume(1, mExpectNoResume); + ASSERT_EQ(SourceBufferIterator::COMPLETE, state); + EXPECT_EQ(aCompletionStatus, aIterator.CompletionStatus()); + ExpectRemainingBytes(aIterator, 0); + ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength); + } + + void CheckIteratorIsComplete(SourceBufferIterator& aIterator, + size_t aTotalLength) + { + CheckIteratorIsComplete(aIterator, 1, aTotalLength); + } + + AutoInitializeImageLib mInit; + char mData[9]; + RefPtr<SourceBuffer> mSourceBuffer; + RefPtr<ExpectNoResume> mExpectNoResume; + RefPtr<CountResumes> mCountResumes; +}; + +TEST_F(ImageSourceBuffer, InitialState) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // RemainingBytesIsNoMoreThan() should always return false in the initial + // state, since we can't know the answer until Complete() has been called. + EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(0)); + EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(SIZE_MAX)); + + // We haven't advanced our iterator at all, so its counters should be zero. + ExpectChunkAndByteCount(iterator, 0, 0); + + // Attempt to advance; we should fail, and end up in the WAITING state. We + // expect no resumes because we don't actually append anything to the + // SourceBuffer in this test. + CheckIteratorMustWait(iterator, mExpectNoResume); +} + +TEST_F(ImageSourceBuffer, ZeroLengthBufferAlwaysFails) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Complete the buffer without writing to it, providing a successful + // completion status. + CheckedCompleteBuffer(iterator, 0); + + // Completing a buffer without writing to it results in an automatic failure; + // make sure that the actual completion status we get from the iterator + // reflects this. + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE); +} + +TEST_F(ImageSourceBuffer, CompleteSuccess) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write a single byte to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 1); + CheckedCompleteBuffer(iterator, 1); + + // We should be able to advance once (to read the single byte) and then should + // reach the COMPLETE state with a successful status. + CheckedAdvanceIterator(iterator, 1); + CheckIteratorIsComplete(iterator, 1); +} + +TEST_F(ImageSourceBuffer, CompleteFailure) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write a single byte to the buffer and complete the buffer. (We have to + // write at least one byte because completing a zero length buffer always + // fails; see the ZeroLengthBufferAlwaysFails test.) + CheckedAppendToBuffer(mData, 1); + CheckedCompleteBuffer(iterator, 1, NS_ERROR_FAILURE); + + // Advance the iterator. Because a failing status is propagated to the + // iterator as soon as it advances, we won't be able to read the single byte + // that we wrote above; we go directly into the COMPLETE state. + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE); +} + +TEST_F(ImageSourceBuffer, Append) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write test data to the buffer. + EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData)))); + CheckedAppendToBuffer(mData, sizeof(mData)); + CheckedCompleteBuffer(iterator, sizeof(mData)); + + // Verify that we can read it back via the iterator, and that the final state + // is what we expect. + CheckedAdvanceIterator(iterator, sizeof(mData)); + CheckIteratorIsComplete(iterator, sizeof(mData)); +} + +TEST_F(ImageSourceBuffer, HugeAppendFails) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // We should fail to append anything bigger than what the SurfaceCache can + // hold, so use the SurfaceCache's maximum capacity to calculate what a + // "massive amount of data" (see below) consists of on this platform. + ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX); + const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1; + + // Attempt to write a massive amount of data and verify that it fails. (We'd + // get a buffer overrun during the test if it succeeds, but if it succeeds + // that's the least of our problems.) + EXPECT_TRUE(NS_FAILED(mSourceBuffer->Append(mData, hugeSize))); + EXPECT_TRUE(mSourceBuffer->IsComplete()); + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY); +} + +TEST_F(ImageSourceBuffer, AppendFromInputStream) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Construct an input stream with some arbitrary data. (We use test data from + // one of the decoder tests.) + nsCOMPtr<nsIInputStream> inputStream = LoadFile(GreenPNGTestCase().mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + ASSERT_TRUE(NS_SUCCEEDED(inputStream->Available(&length))); + + // Write test data to the buffer. + EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->AppendFromInputStream(inputStream, + length))); + CheckedCompleteBuffer(iterator, length); + + // Verify that the iterator sees the appropriate amount of data. + CheckedAdvanceIteratorStateOnly(iterator, length); + CheckIteratorIsComplete(iterator, length); +} + +TEST_F(ImageSourceBuffer, AppendAfterComplete) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write test data to the buffer. + EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData)))); + CheckedAppendToBuffer(mData, sizeof(mData)); + CheckedCompleteBuffer(iterator, sizeof(mData)); + + // Verify that we can read it back via the iterator, and that the final state + // is what we expect. + CheckedAdvanceIterator(iterator, sizeof(mData)); + CheckIteratorIsComplete(iterator, sizeof(mData)); + + // Write more data to the completed buffer. + EXPECT_TRUE(NS_FAILED(mSourceBuffer->Append(mData, sizeof(mData)))); + + // Try to read with a new iterator and verify that the new data got ignored. + SourceBufferIterator iterator2 = mSourceBuffer->Iterator(); + CheckedAdvanceIterator(iterator2, sizeof(mData)); + CheckIteratorIsComplete(iterator2, sizeof(mData)); +} + +TEST_F(ImageSourceBuffer, MinChunkCapacity) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write test data to the buffer using many small appends. Since + // ExpectLength() isn't being called, we should be able to write up to + // SourceBuffer::MIN_CHUNK_CAPACITY bytes without a second chunk being + // allocated. + CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Verify that the iterator sees the appropriate amount of data. + CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(SourceBuffer::MIN_CHUNK_CAPACITY); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1); + CheckIteratorIsComplete(iterator, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1); +} + +TEST_F(ImageSourceBuffer, ExpectLengthDoesNotShrinkBelowMinCapacity) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the buffer, + // but call ExpectLength() first to make SourceBuffer expect only a single + // byte. We expect this to still result in only one chunk, because + // regardless of ExpectLength() we won't allocate a chunk smaller than + // MIN_CHUNK_CAPACITY bytes. + EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(1))); + CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY); + CheckedCompleteBuffer(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Verify that the iterator sees a single chunk. + CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + CheckIteratorIsComplete(iterator, 1, SourceBuffer::MIN_CHUNK_CAPACITY); +} + +TEST_F(ImageSourceBuffer, ExpectLengthGrowsAboveMinCapacity) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer, calling ExpectLength() with the correct length first. We expect + // this to result in only one chunk, because ExpectLength() allows us to + // allocate a larger first chunk than MIN_CHUNK_CAPACITY bytes. + const size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY; + EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(length))); + CheckedAppendToBufferInChunks(10, length); + + // Verify that the iterator sees a single chunk. + CheckedAdvanceIterator(iterator, length); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(length); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 2, length + 1); + CheckIteratorIsComplete(iterator, 2, length + 1); +} + +TEST_F(ImageSourceBuffer, HugeExpectLengthFails) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // ExpectLength() should fail if the length is bigger than what the + // SurfaceCache can hold, so use the SurfaceCache's maximum capacity to + // calculate what a "massive amount of data" (see below) consists of on this + // platform. + ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX); + const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1; + + // Attempt to write a massive amount of data and verify that it fails. (We'd + // get a buffer overrun during the test if it succeeds, but if it succeeds + // that's the least of our problems.) + EXPECT_TRUE(NS_FAILED(mSourceBuffer->ExpectLength(hugeSize))); + EXPECT_TRUE(mSourceBuffer->IsComplete()); + CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY); +} + +TEST_F(ImageSourceBuffer, LargeAppendsAllocateOnlyOneChunk) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. We expect this to result in only one + // chunk even though ExpectLength() wasn't called, because we should always + // allocate a new chunk large enough to store the data we have at hand. + constexpr size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY; + char data[length]; + GenerateData(data, sizeof(data)); + CheckedAppendToBuffer(data, length); + + // Verify that the iterator sees a single chunk. + CheckedAdvanceIterator(iterator, length); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(length); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 2, length + 1); + CheckIteratorIsComplete(iterator, 2, length + 1); +} + +TEST_F(ImageSourceBuffer, LargeAppendsAllocateAtMostOneChunk) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Allocate some data we'll use below. + constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2; + constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = firstWriteLength + secondWriteLength; + char data[totalLength]; + GenerateData(data, sizeof(data)); + + // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. This should fill half of the first chunk. + CheckedAppendToBuffer(data, firstWriteLength); + + // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. We expect this to result in the first of + // the first chunk being filled and a new chunk being allocated for the + // remainder. + CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength); + + // Verify that the iterator sees a MIN_CHUNK_CAPACITY-length chunk. + CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY); + + // Verify that the iterator sees a second chunk of the length we expect. + const size_t expectedSecondChunkLength = + totalLength - SourceBuffer::MIN_CHUNK_CAPACITY; + CheckedAdvanceIterator(iterator, expectedSecondChunkLength, 2, totalLength); + + // Write one more byte; we expect to see that it triggers an allocation. + CheckedAppendToBufferLastByteForLength(totalLength); + CheckedCompleteBuffer(iterator, 1); + + // Verify that the iterator sees the new byte and a new chunk has been + // allocated. + CheckedAdvanceIterator(iterator, 1, 3, totalLength + 1); + CheckIteratorIsComplete(iterator, 3, totalLength + 1); +} + +TEST_F(ImageSourceBuffer, CompactionHappensWhenBufferIsComplete) +{ + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. + CheckedAppendToBufferInChunks(chunkLength, totalLength); + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Verify that the iterator sees two chunks. + CheckedAdvanceIterator(iterator, chunkLength); + CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength); + } + + // Complete the buffer, which should trigger compaction implicitly. + CheckedCompleteBuffer(); + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Verify that compaction happened and there's now only one chunk. + CheckedAdvanceIterator(iterator, totalLength); + CheckIteratorIsComplete(iterator, 1, totalLength); + } +} + +TEST_F(ImageSourceBuffer, CompactionIsDelayedWhileIteratorsExist) +{ + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + { + SourceBufferIterator outerIterator = mSourceBuffer->Iterator(); + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Write enough data to create two chunks. + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + // Verify that the iterator sees two chunks. Since there are live + // iterators, compaction shouldn't have happened when we completed the + // buffer. + CheckedAdvanceIterator(iterator, chunkLength); + CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength); + CheckIteratorIsComplete(iterator, 2, totalLength); + } + + // Now |iterator| has been destroyed, but |outerIterator| still exists, so + // we expect no compaction to have occurred at this point. + CheckedAdvanceIterator(outerIterator, chunkLength); + CheckedAdvanceIterator(outerIterator, chunkLength, 2, totalLength); + CheckIteratorIsComplete(outerIterator, 2, totalLength); + } + + // Now all iterators have been destroyed. Since the buffer was already + // complete, we expect compaction to happen implicitly here. + + { + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Verify that compaction happened and there's now only one chunk. + CheckedAdvanceIterator(iterator, totalLength); + CheckIteratorIsComplete(iterator, 1, totalLength); + } +} + +TEST_F(ImageSourceBuffer, SourceBufferIteratorsCanBeMoved) +{ + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. We create an iterator here to make + // sure that compaction doesn't happen during the test. + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + auto GetIterator = [&]{ + SourceBufferIterator lambdaIterator = mSourceBuffer->Iterator(); + CheckedAdvanceIterator(lambdaIterator, chunkLength); + return lambdaIterator; + }; + + // Move-construct |movedIterator| from the iterator returned from + // GetIterator() and check that its state is as we expect. + SourceBufferIterator movedIterator = Move(GetIterator()); + EXPECT_TRUE(movedIterator.Data()); + EXPECT_EQ(chunkLength, movedIterator.Length()); + ExpectChunkAndByteCount(movedIterator, 1, chunkLength); + + // Make sure that we can advance the iterator. + CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength); + + // Make sure that the iterator handles completion properly. + CheckIteratorIsComplete(movedIterator, 2, totalLength); + + // Move-assign |movedIterator| from the iterator returned from + // GetIterator() and check that its state is as we expect. + movedIterator = Move(GetIterator()); + EXPECT_TRUE(movedIterator.Data()); + EXPECT_EQ(chunkLength, movedIterator.Length()); + ExpectChunkAndByteCount(movedIterator, 1, chunkLength); + + // Make sure that we can advance the iterator. + CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength); + + // Make sure that the iterator handles completion properly. + CheckIteratorIsComplete(movedIterator, 2, totalLength); +} + +TEST_F(ImageSourceBuffer, SubchunkAdvance) +{ + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. We create our iterator here to make + // sure that compaction doesn't happen during the test. + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + // Advance through the first chunk. The chunk count should not increase. + // We check that by always passing 1 for the |aChunks| parameter of + // CheckedAdvanceIteratorStateOnly(). We have to call CheckData() manually + // because the offset calculation in CheckedAdvanceIterator() assumes that + // we're advancing a chunk at a time. + size_t offset = 0; + while (offset < chunkLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + } + + // Read the first byte of the second chunk. This is the point at which we + // can't advance within the same chunk, so the chunk count should increase. We + // check that by passing 2 for the |aChunks| parameter of + // CheckedAdvanceIteratorStateOnly(). + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + + // Read the rest of the second chunk. The chunk count should not increase. + while (offset < totalLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + } + + // Make sure we reached the end. + CheckIteratorIsComplete(iterator, 2, totalLength); +} + +TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvance) +{ + constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = 2 * chunkLength; + + // Write enough data to create two chunks. We create our iterator here to make + // sure that compaction doesn't happen during the test. + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + CheckedAppendToBufferInChunks(chunkLength, totalLength); + CheckedCompleteBuffer(iterator, totalLength); + + // Make an initial zero-length advance. Although a zero-length advance + // normally won't cause us to read a chunk from the SourceBuffer, we'll do so + // if the iterator is in the initial state to keep the invariant that + // SourceBufferIterator in the READY state always returns a non-null pointer + // from Data(). + CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + + // Advance through the first chunk. As in the |SubchunkAdvance| test, the + // chunk count should not increase. We do a zero-length advance after each + // normal advance to ensure that zero-length advances do not change the + // iterator's position or cause a new chunk to be read. + size_t offset = 0; + while (offset < chunkLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength, + AdvanceMode::eAdvanceByLengthExactly); + } + + // Read the first byte of the second chunk. This is the point at which we + // can't advance within the same chunk, so the chunk count should increase. As + // before, we do a zero-length advance afterward. + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + + // Read the rest of the second chunk. The chunk count should not increase. As + // before, we do a zero-length advance after each normal advance. + while (offset < totalLength) { + CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + CheckData(iterator.Data(), offset++, iterator.Length()); + CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength, + AdvanceMode::eAdvanceByLengthExactly); + } + + // Make sure we reached the end. + CheckIteratorIsComplete(iterator, 2, totalLength); +} + +TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvanceWithNoData) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that advancing by zero bytes still makes us enter the WAITING state. + // This is because if we entered the READY state before reading any data at + // all, we'd break the invariant that SourceBufferIterator::Data() always + // returns a non-null pointer in the READY state. + auto state = iterator.AdvanceOrScheduleResume(0, mCountResumes); + EXPECT_EQ(SourceBufferIterator::WAITING, state); + + // Call Complete(). This should trigger a resume. + CheckedCompleteBuffer(); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, NullIResumable) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, nullptr); + + // Append to the buffer, which would cause a resume if we had passed a + // non-null IResumable. + CheckedAppendToBuffer(mData, sizeof(mData)); + CheckedCompleteBuffer(iterator, sizeof(mData)); +} + +TEST_F(ImageSourceBuffer, AppendTriggersResume) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mCountResumes); + + // Call Append(). This should trigger a resume. + mSourceBuffer->Append(mData, sizeof(mData)); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, OnlyOneResumeTriggeredPerAppend) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mCountResumes); + + // Allocate some data we'll use below. + constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2; + constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY; + constexpr size_t totalLength = firstWriteLength + secondWriteLength; + char data[totalLength]; + GenerateData(data, sizeof(data)); + + // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. This should fill half of the first chunk. + // This should trigger a resume. + CheckedAppendToBuffer(data, firstWriteLength); + EXPECT_EQ(1u, mCountResumes->Count()); + + // Advance past the new data and wait again. + CheckedAdvanceIterator(iterator, firstWriteLength); + CheckIteratorMustWait(iterator, mCountResumes); + + // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the + // buffer in a single Append() call. We expect this to result in the first of + // the first chunk being filled and a new chunk being allocated for the + // remainder. Even though two chunks are getting written to here, only *one* + // resume should get triggered, for a total of two in this test. + CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength); + EXPECT_EQ(2u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, CompleteTriggersResume) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mCountResumes); + + // Call Complete(). This should trigger a resume. + CheckedCompleteBuffer(); + EXPECT_EQ(1u, mCountResumes->Count()); +} + +TEST_F(ImageSourceBuffer, ExpectLengthDoesNotTriggerResume) +{ + SourceBufferIterator iterator = mSourceBuffer->Iterator(); + + // Check that we can't advance. + CheckIteratorMustWait(iterator, mExpectNoResume); + + // Call ExpectLength(). If this triggers a resume, |mExpectNoResume| will + // ensure that the test fails. + mSourceBuffer->ExpectLength(1000); +}
--- a/image/test/gtest/TestStreamingLexer.cpp +++ b/image/test/gtest/TestStreamingLexer.cpp @@ -14,248 +14,454 @@ enum class TestState { ONE, TWO, THREE, UNBUFFERED }; void -CheckData(const char* aData, size_t aLength) +CheckLexedData(const char* aData, size_t aLength, size_t aExpectedLength) { - EXPECT_TRUE(aLength == 3); - EXPECT_EQ(1, aData[0]); - EXPECT_EQ(2, aData[1]); - EXPECT_EQ(3, aData[2]); + EXPECT_TRUE(aLength == aExpectedLength); + + for (size_t i = 0; i < aLength; ++i) { + EXPECT_EQ(aData[i], char((i % 3) + 1)); + } } LexerTransition<TestState> DoLex(TestState aState, const char* aData, size_t aLength) { switch (aState) { case TestState::ONE: - CheckData(aData, aLength); + CheckLexedData(aData, aLength, 3); return Transition::To(TestState::TWO, 3); case TestState::TWO: - CheckData(aData, aLength); + CheckLexedData(aData, aLength, 3); return Transition::To(TestState::THREE, 3); case TestState::THREE: - CheckData(aData, aLength); + CheckLexedData(aData, aLength, 3); return Transition::TerminateSuccess(); default: - MOZ_CRASH("Unknown TestState"); + MOZ_CRASH("Unexpected or unhandled TestState"); } } LexerTransition<TestState> DoLexWithUnbuffered(TestState aState, const char* aData, size_t aLength, Vector<char>& aUnbufferedVector) { switch (aState) { case TestState::ONE: - CheckData(aData, aLength); + CheckLexedData(aData, aLength, 3); return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3); case TestState::UNBUFFERED: EXPECT_TRUE(aLength <= 3); EXPECT_TRUE(aUnbufferedVector.append(aData, aLength)); return Transition::ContinueUnbuffered(TestState::UNBUFFERED); case TestState::TWO: - CheckData(aUnbufferedVector.begin(), aUnbufferedVector.length()); + CheckLexedData(aUnbufferedVector.begin(), aUnbufferedVector.length(), 3); return Transition::To(TestState::THREE, 3); case TestState::THREE: - CheckData(aData, aLength); + CheckLexedData(aData, aLength, 3); return Transition::TerminateSuccess(); default: - MOZ_CRASH("Unknown TestState"); + MOZ_CRASH("Unexpected or unhandled TestState"); } } LexerTransition<TestState> DoLexWithUnbufferedTerminate(TestState aState, const char* aData, size_t aLength) { switch (aState) { case TestState::ONE: - CheckData(aData, aLength); + CheckLexedData(aData, aLength, 3); return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3); case TestState::UNBUFFERED: return Transition::TerminateSuccess(); default: - MOZ_CRASH("Unknown TestState"); + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +LexerTransition<TestState> +DoLexWithZeroLengthStates(TestState aState, const char* aData, size_t aLength) +{ + switch (aState) { + case TestState::ONE: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::TWO, 0); + case TestState::TWO: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::THREE, 9); + case TestState::THREE: + CheckLexedData(aData, aLength, 9); + return Transition::TerminateSuccess(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); } } -TEST(ImageStreamingLexer, SingleChunk) +LexerTransition<TestState> +DoLexWithZeroLengthStatesUnbuffered(TestState aState, + const char* aData, + size_t aLength) +{ + switch (aState) { + case TestState::ONE: + EXPECT_TRUE(aLength == 0); + return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 0); + case TestState::TWO: + EXPECT_TRUE(aLength == 0); + return Transition::To(TestState::THREE, 9); + case TestState::THREE: + CheckLexedData(aData, aLength, 9); + return Transition::TerminateSuccess(); + case TestState::UNBUFFERED: + ADD_FAILURE() << "Should not enter zero-length unbuffered state"; + return Transition::TerminateFailure(); + default: + MOZ_CRASH("Unexpected or unhandled TestState"); + } +} + +class ImageStreamingLexer : public ::testing::Test { - StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3)); - char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 }; +public: + ImageStreamingLexer() + : mLexer(Transition::To(TestState::ONE, 3)) + , mSourceBuffer(new SourceBuffer) + , mIterator(mSourceBuffer->Iterator()) + , mExpectNoResume(new ExpectNoResume) + , mCountResumes(new CountResumes) + { } + +protected: + AutoInitializeImageLib mInit; + const char mData[9] { 1, 2, 3, 1, 2, 3, 1, 2, 3 }; + StreamingLexer<TestState> mLexer; + RefPtr<SourceBuffer> mSourceBuffer; + SourceBufferIterator mIterator; + RefPtr<ExpectNoResume> mExpectNoResume; + RefPtr<CountResumes> mCountResumes; +}; +TEST_F(ImageStreamingLexer, ZeroLengthData) +{ + // Test a zero-length input. + mSourceBuffer->Complete(NS_OK); + + Maybe<TerminalState> result = mLexer.Lex(mIterator, mExpectNoResume, DoLex); + + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::FAILURE), result); +} + +TEST_F(ImageStreamingLexer, SingleChunk) +{ // Test delivering all the data at once. - Maybe<TerminalState> result = lexer.Lex(data, sizeof(data), DoLex); + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + Maybe<TerminalState> result = mLexer.Lex(mIterator, mExpectNoResume, DoLex); + EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } -TEST(ImageStreamingLexer, SingleChunkWithUnbuffered) +TEST_F(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. + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + Maybe<TerminalState> result = - lexer.Lex(data, sizeof(data), - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(mIterator, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector); }); + EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } -TEST(ImageStreamingLexer, ChunkPerState) +TEST_F(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<TerminalState> result = lexer.Lex(data + 3 * i, 3, DoLex); + mSourceBuffer->Append(mData + 3 * i, 3); + Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex); if (i == 2) { EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } else { EXPECT_TRUE(result.isNothing()); } } + + EXPECT_EQ(2u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); } -TEST(ImageStreamingLexer, ChunkPerStateWithUnbuffered) +TEST_F(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) { + mSourceBuffer->Append(mData + 3 * i, 3); Maybe<TerminalState> result = - lexer.Lex(data + 3 * i, 3, - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(mIterator, mCountResumes, + [&](TestState aState, const char* aData, size_t aLength) { return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector); }); if (i == 2) { EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } else { EXPECT_TRUE(result.isNothing()); } } + + EXPECT_EQ(2u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); } -TEST(ImageStreamingLexer, OneByteChunks) +TEST_F(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<TerminalState> result = lexer.Lex(data + i, 1, DoLex); + mSourceBuffer->Append(mData + i, 1); + Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex); if (i == 8) { EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } else { EXPECT_TRUE(result.isNothing()); } } + + EXPECT_EQ(8u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); } -TEST(ImageStreamingLexer, OneByteChunksWithUnbuffered) +TEST_F(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) { + mSourceBuffer->Append(mData + i, 1); Maybe<TerminalState> result = - lexer.Lex(data + i, 1, - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(mIterator, mCountResumes, + [&](TestState aState, const char* aData, size_t aLength) { return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector); }); if (i == 8) { EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } else { EXPECT_TRUE(result.isNothing()); } } + + EXPECT_EQ(8u, mCountResumes->Count()); + mSourceBuffer->Complete(NS_OK); +} + +TEST_F(ImageStreamingLexer, ZeroLengthState) +{ + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to be zero length. + StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 0)); + + Maybe<TerminalState> result = + lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStates); + + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::SUCCESS), result); } -TEST(ImageStreamingLexer, TerminateSuccess) +TEST_F(ImageStreamingLexer, ZeroLengthStateWithUnbuffered) { - StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3)); - char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 }; + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); + + // Create a special StreamingLexer for this test because we want the first + // state to be both zero length and unbuffered. + StreamingLexer<TestState> lexer(Transition::ToUnbuffered(TestState::ONE, + TestState::UNBUFFERED, + 0)); + + Maybe<TerminalState> result = + lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStatesUnbuffered); + + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::SUCCESS), result); +} + +TEST_F(ImageStreamingLexer, TerminateSuccess) +{ + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); // Test that Terminate is "sticky". + SourceBufferIterator iterator = mSourceBuffer->Iterator(); Maybe<TerminalState> result = - lexer.Lex(data, sizeof(data), - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(iterator, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { EXPECT_TRUE(aState == TestState::ONE); return Transition::TerminateSuccess(); }); EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); + SourceBufferIterator iterator2 = mSourceBuffer->Iterator(); result = - lexer.Lex(data, sizeof(data), - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(iterator2, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { EXPECT_TRUE(false); // Shouldn't get here. return Transition::TerminateFailure(); }); EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } -TEST(ImageStreamingLexer, TerminateFailure) +TEST_F(ImageStreamingLexer, TerminateFailure) { - StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3)); - char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 }; + mSourceBuffer->Append(mData, sizeof(mData)); + mSourceBuffer->Complete(NS_OK); // Test that Terminate is "sticky". + SourceBufferIterator iterator = mSourceBuffer->Iterator(); Maybe<TerminalState> result = - lexer.Lex(data, sizeof(data), - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(iterator, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { EXPECT_TRUE(aState == TestState::ONE); return Transition::TerminateFailure(); }); EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::FAILURE), result); + SourceBufferIterator iterator2 = mSourceBuffer->Iterator(); result = - lexer.Lex(data, sizeof(data), - [&](TestState aState, const char* aData, size_t aLength) { + mLexer.Lex(iterator2, mExpectNoResume, + [&](TestState aState, const char* aData, size_t aLength) { EXPECT_TRUE(false); // Shouldn't get here. return Transition::TerminateFailure(); }); EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::FAILURE), result); } -TEST(ImageStreamingLexer, TerminateUnbuffered) +TEST_F(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) { + mSourceBuffer->Append(mData + i, 1); Maybe<TerminalState> result = - lexer.Lex(data + i, 1, DoLexWithUnbufferedTerminate); + mLexer.Lex(mIterator, mCountResumes, DoLexWithUnbufferedTerminate); if (i > 2) { EXPECT_TRUE(result.isSome()); EXPECT_EQ(Some(TerminalState::SUCCESS), result); } else { EXPECT_TRUE(result.isNothing()); } } + + // We expect 3 resumes because TestState::ONE consumes 3 bytes and then + // transitions to TestState::UNBUFFERED, which calls TerminateSuccess() as + // soon as it receives a single byte. That's four bytes total, which are + // delivered one at a time, requiring 3 resumes. + EXPECT_EQ(3u, mCountResumes->Count()); + + mSourceBuffer->Complete(NS_OK); } + +TEST_F(ImageStreamingLexer, SourceBufferImmediateComplete) +{ + // Test calling SourceBuffer::Complete() without appending any data. This + // causes the SourceBuffer to automatically have a failing completion status, + // no matter what you pass, so we expect TerminalState::FAILURE below. + mSourceBuffer->Complete(NS_OK); + + Maybe<TerminalState> result = mLexer.Lex(mIterator, mExpectNoResume, DoLex); + + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::FAILURE), result); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedSuccess) +{ + // Test that calling SourceBuffer::Complete() with a successful status results + // in an immediate TerminalState::SUCCESS result. + for (unsigned i = 0; i < 9; ++i) { + if (i < 2) { + mSourceBuffer->Append(mData + i, 1); + } else if (i == 2) { + mSourceBuffer->Complete(NS_OK); + } + + Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex); + + if (i >= 2) { + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::SUCCESS), result); + } else { + EXPECT_TRUE(result.isNothing()); + } + } + + EXPECT_EQ(2u, mCountResumes->Count()); +} + +TEST_F(ImageStreamingLexer, SourceBufferTruncatedFailure) +{ + // Test that calling SourceBuffer::Complete() with a failing status results in + // an immediate TerminalState::FAILURE result. + for (unsigned i = 0; i < 9; ++i) { + if (i < 2) { + mSourceBuffer->Append(mData + i, 1); + } else if (i == 2) { + mSourceBuffer->Complete(NS_ERROR_FAILURE); + } + + Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex); + + if (i >= 2) { + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::FAILURE), result); + } else { + EXPECT_TRUE(result.isNothing()); + } + } + + EXPECT_EQ(2u, mCountResumes->Count()); +} + +TEST_F(ImageStreamingLexer, NoSourceBufferResumable) +{ + // Test delivering in one byte chunks with no IResumable. + for (unsigned i = 0; i < 9; ++i) { + mSourceBuffer->Append(mData + i, 1); + Maybe<TerminalState> result = mLexer.Lex(mIterator, nullptr, DoLex); + + if (i == 8) { + EXPECT_TRUE(result.isSome()); + EXPECT_EQ(Some(TerminalState::SUCCESS), result); + } else { + EXPECT_TRUE(result.isNothing()); + } + } + + mSourceBuffer->Complete(NS_OK); +}
--- a/image/test/gtest/TestSurfacePipeIntegration.cpp +++ b/image/test/gtest/TestSurfacePipeIntegration.cpp @@ -126,23 +126,18 @@ CheckPalettedSurfacePipeMethodResults(Su aPipe->ResetToFirstRow(); EXPECT_FALSE(aPipe->IsSurfaceFinished()); invalidRect = aPipe->TakeInvalidRect(); EXPECT_TRUE(invalidRect.isNothing()); } class ImageSurfacePipeIntegration : public ::testing::Test { - protected: - static void SetUpTestCase() - { - // Ensure that ImageLib services are initialized. - nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); - EXPECT_TRUE(imgTools != nullptr); - } +protected: + AutoInitializeImageLib mInit; }; TEST_F(ImageSurfacePipeIntegration, SurfacePipe) { // Test that SurfacePipe objects can be initialized and move constructed. SurfacePipe pipe = TestSurfacePipeFactory::SimpleSurfacePipe(); // Test that SurfacePipe objects can be move assigned.
new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..228c5c99923bbc940c6178ed08637c86cf38bdc4 GIT binary patch literal 54 qc${<cb&6nO00Ac;)&OD$Mh1otK$?+3fPon#0hE6MWV`@#AOHZEfCUQx
--- a/image/test/gtest/moz.build +++ b/image/test/gtest/moz.build @@ -10,16 +10,17 @@ UNIFIED_SOURCES = [ 'Common.cpp', 'TestADAM7InterpolatingFilter.cpp', 'TestCopyOnWrite.cpp', 'TestDecoders.cpp', 'TestDecodeToSurface.cpp', 'TestDeinterlacingFilter.cpp', 'TestMetadata.cpp', 'TestRemoveFrameRectFilter.cpp', + 'TestSourceBuffer.cpp', 'TestStreamingLexer.cpp', 'TestSurfaceSink.cpp', ] if CONFIG['MOZ_ENABLE_SKIA']: UNIFIED_SOURCES += [ 'TestDownscalingFilter.cpp', 'TestSurfacePipeIntegration.cpp', @@ -45,16 +46,17 @@ TEST_HARNESS_FILES.gtest += [ 'first-frame-green.png', 'first-frame-padding.gif', 'green.bmp', 'green.gif', 'green.ico', 'green.icon', 'green.jpg', 'green.png', + 'invalid-truncated-metadata.bmp', 'no-frame-delay.gif', 'rle4.bmp', 'rle8.bmp', 'transparent-ico-with-and-mask.ico', 'transparent-if-within-ico.bmp', 'transparent.gif', 'transparent.png', ]
--- a/intl/icu_sources_data.py +++ b/intl/icu_sources_data.py @@ -14,21 +14,23 @@ from __future__ import print_function import glob import os import shutil import subprocess import sys import tempfile +from mozpack import path as mozpath + def find_source_file(dir, filename): base = os.path.splitext(filename)[0] for ext in ('.cpp', '.c'): - f = os.path.join(dir, base + ext) + f = mozpath.join(dir, base + ext) if os.path.isfile(f): return f raise Exception("Couldn't find source file for: %s" % filename) def get_sources_from_makefile(makefile): import pymake.parser from pymake.parserdata import SetVariable @@ -48,106 +50,109 @@ def write_sources(mozbuild, sources): 'DO NOT EDIT\n' + 'SOURCES += [\n') f.write(''.join(" '/%s',\n" % s for s in sources)) f.write(']\n') def update_sources(topsrcdir): print('Updating ICU sources lists...') - sys.path.append(os.path.join(topsrcdir, 'build/pymake')) + sys.path.append(mozpath.join(topsrcdir, 'build/pymake')) for d in ['common', 'i18n']: - makefile = os.path.join(topsrcdir, + makefile = mozpath.join(topsrcdir, 'intl/icu/source/%s/Makefile.in' % d) - mozbuild = os.path.join(topsrcdir, + mozbuild = mozpath.join(topsrcdir, 'config/external/icu/%s/sources.mozbuild' % d) - sources = [os.path.relpath(s, topsrcdir) + sources = [mozpath.relpath(s, topsrcdir) for s in get_sources_from_makefile(makefile)] write_sources(mozbuild, sources) def try_run(name, command, cwd=None, **kwargs): - with tempfile.NamedTemporaryFile(prefix=name, delete=False) as f: - if subprocess.call(command, - stdout=f, - stderr=subprocess.STDOUT, - cwd=cwd, - **kwargs) == 0: - os.unlink(f.name) - return True - print('''Error running "{}" in directory {} -See output in {}'''.format(' '.join(command), cwd, f.name), - file=sys.stderr) - return False + try: + with tempfile.NamedTemporaryFile(prefix=name, delete=False) as f: + subprocess.check_call(command, cwd=cwd, stdout=f, + stderr=subprocess.STDOUT, **kwargs) + except subprocess.CalledProcessError: + print('''Error running "{}" in directory {} + See output in {}'''.format(' '.join(command), cwd, f.name), + file=sys.stderr) + return False + else: + os.unlink(f.name) + return True def get_data_file(data_dir): - files = glob.glob(os.path.join(data_dir, 'icudt*.dat')) + files = glob.glob(mozpath.join(data_dir, 'icudt*.dat')) return files[0] if files else None def update_data_file(topsrcdir): objdir = tempfile.mkdtemp(prefix='icu-obj-') - configure = os.path.join(topsrcdir, 'intl/icu/source/configure') + configure = mozpath.join(topsrcdir, 'intl/icu/source/configure') env = dict(os.environ) # bug 1262101 - these should be shared with the moz.build files env.update({ 'CPPFLAGS': ('-DU_NO_DEFAULT_INCLUDE_UTF_HEADERS=1 ' + '-DUCONFIG_NO_LEGACY_CONVERSION ' + '-DUCONFIG_NO_TRANSLITERATION ' + '-DUCONFIG_NO_REGULAR_EXPRESSIONS ' + '-DUCONFIG_NO_BREAK_ITERATION ' + '-DU_CHARSET_IS_UTF8') }) print('Running ICU configure...') if not try_run( 'icu-configure', - [configure, + ['sh', configure, '--with-data-packaging=archive', '--enable-static', '--disable-shared', '--disable-extras', '--disable-icuio', '--disable-layout', '--disable-tests', '--disable-samples', '--disable-strict'], cwd=objdir, env=env): return False print('Running ICU make...') if not try_run('icu-make', ['make'], cwd=objdir): return False print('Copying ICU data file...') - tree_data_path = os.path.join(topsrcdir, + tree_data_path = mozpath.join(topsrcdir, 'config/external/icu/data/') old_data_file = get_data_file(tree_data_path) if not old_data_file: print('Error: no ICU data file in %s' % tree_data_path, file=sys.stderr) return False - new_data_file = get_data_file(os.path.join(objdir, 'data/out')) + new_data_file = get_data_file(mozpath.join(objdir, 'data/out')) if not new_data_file: print('Error: no ICU data in ICU objdir', file=sys.stderr) return False if os.path.basename(old_data_file) != os.path.basename(new_data_file): # Data file name has the major version number embedded. os.unlink(old_data_file) shutil.copy(new_data_file, tree_data_path) - shutil.rmtree(objdir) + try: + shutil.rmtree(objdir) + except: + print('Warning: failed to remove %s' % objdir, file=sys.stderr) return True def main(): if len(sys.argv) != 2: print('Usage: icu_sources_data.py <mozilla topsrcdir>', file=sys.stderr) sys.exit(1) - topsrcdir = os.path.abspath(sys.argv[1]) + topsrcdir = mozpath.abspath(sys.argv[1]) update_sources(topsrcdir) if not update_data_file(topsrcdir): print('Error updating ICU data file', file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()