author | Bogdan Tara <btara@mozilla.com> |
Tue, 18 Sep 2018 00:58:07 +0300 | |
changeset 436857 | 87a95e1b7ec691bef7b938e722fe1b01cce68664 |
parent 436781 | ed612eec41a44867a1330aa893040bacb7ac5b74 (current diff) |
parent 436856 | 5ab4db62dc822625b054a9afec6f065e96ebd771 (diff) |
child 436876 | 68024a7f328bcfe2482bdc8fec1cdc0c55d6b23f |
child 436884 | a11fe256f24e4e6a2692f394c881b04f50eab2b1 |
push id | 34660 |
push user | btara@mozilla.com |
push date | Mon, 17 Sep 2018 21:58:52 +0000 |
treeherder | mozilla-central@87a95e1b7ec6 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 64.0a1 |
first release with | nightly linux32
87a95e1b7ec6
/
64.0a1
/
20180917220115
/
files
nightly linux64
87a95e1b7ec6
/
64.0a1
/
20180917220115
/
files
nightly mac
87a95e1b7ec6
/
64.0a1
/
20180917220115
/
files
nightly win32
87a95e1b7ec6
/
64.0a1
/
20180917220115
/
files
nightly win64
87a95e1b7ec6
/
64.0a1
/
20180917220115
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
64.0a1
/
20180917220115
/
pushlog to previous
nightly linux64
64.0a1
/
20180917220115
/
pushlog to previous
nightly mac
64.0a1
/
20180917220115
/
pushlog to previous
nightly win32
64.0a1
/
20180917220115
/
pushlog to previous
nightly win64
64.0a1
/
20180917220115
/
pushlog to previous
|
devtools/client/webconsole/test/mochitest/browser_webconsole_closure_inspection.js | file | annotate | diff | comparison | revisions | |
dom/base/nsGlobalWindowInner.cpp | file | annotate | diff | comparison | revisions | |
dom/commandhandler/nsCommandGroup.cpp | file | annotate | diff | comparison | revisions | |
dom/commandhandler/nsCommandGroup.h | file | annotate | diff | comparison | revisions | |
testing/web-platform/meta/content-security-policy/inside-worker/shared-script.html.ini | file | annotate | diff | comparison | revisions | |
testing/web-platform/meta/content-security-policy/script-src/script-src-1_4_2.html.ini | file | annotate | diff | comparison | revisions | |
testing/web-platform/meta/content-security-policy/script-src/worker-set-timeout-blocked.sub.html.ini | file | annotate | diff | comparison | revisions |
--- a/browser/themes/shared/controlcenter/panel.inc.css +++ b/browser/themes/shared/controlcenter/panel.inc.css @@ -394,16 +394,21 @@ description#identity-popup-content-verif } #identity-popup-content-blocking-content:not([enabled]) #identity-popup-content-blocking-disabled-label { display: -moz-box; background-color: #d70022; stroke: #d70022; } +#identity-popup-content-blocking-content:not([contentBlockingUI]):not([enabled]) #identity-popup-content-blocking-disabled-label { + background-color: #b1b1b3; + stroke: #b1b1b3; +} + #identity-popup-content-blocking-content:not([enabled]) #identity-popup-content-blocking-disabled-label-exception { display: none; } #identity-popup-content-blocking-disabled-label > label { margin: 0; line-height: 18px; }
--- a/browser/themes/shared/incontentprefs/privacy.css +++ b/browser/themes/shared/incontentprefs/privacy.css @@ -122,25 +122,33 @@ margin: 16px 0; } .content-blocking-category-labels { padding-inline-start: 4px; margin-inline-start: 25px !important; } -#trackingProtectionMenu, -#blockCookiesCB { +#trackingProtectionMenu { margin-top: 0.75em; } #blockCookiesCBDeck { max-width: 444px; } +#blockCookiesCBDeck:not([selectedIndex]) > .warning-description, +#blockCookiesCBDeck[selectedIndex="0"] > .warning-description { + display: none; +} + +#blockCookiesCBDeck > .warning-description { + margin-bottom: 0.75em !important; +} + #changeBlockListLink { font-size: 90%; /* In order to override the margins set in preferences.inc.css, we have to use !important. */ margin-top: 1em !important; } .content-blocking-category-description { font-size: 90%;
--- a/devtools/client/application/test/browser.ini +++ b/devtools/client/application/test/browser.ini @@ -7,16 +7,17 @@ support-files = service-workers/debug.html service-workers/dynamic-registration.html service-workers/empty.html service-workers/empty-sw.js service-workers/scope-page.html service-workers/simple.html service-workers/simple-unicode.html !/devtools/client/debugger/new/test/mochitest/helpers.js + !/devtools/client/debugger/new/test/mochitest/helpers/context.js !/devtools/client/shared/test/frame-script-utils.js !/devtools/client/shared/test/shared-head.js !/devtools/client/shared/test/telemetry-test-helpers.js [browser_application_panel_debug-service-worker.js] [browser_application_panel_list-domain-workers.js] [browser_application_panel_list-several-workers.js] [browser_application_panel_list-single-worker.js]
--- a/devtools/client/application/test/browser_application_panel_debug-service-worker.js +++ b/devtools/client/application/test/browser_application_panel_debug-service-worker.js @@ -3,16 +3,21 @@ "use strict"; /* import-globals-from ../../debugger/new/test/mochitest/helpers.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", this); +/* import-globals-from ../../debugger/new/test/mochitest/helpers/context.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js", + this); + const TAB_URL = URL_ROOT + "service-workers/debug.html"; add_task(async function() { await enableApplicationPanel(); const { panel, tab, target } = await openNewTabAndApplicationPanel(TAB_URL); const doc = panel.panelWin.document;
--- a/devtools/client/canvasdebugger/test/browser.ini +++ b/devtools/client/canvasdebugger/test/browser.ini @@ -12,16 +12,17 @@ support-files = doc_simple-canvas-transparent.html doc_webgl-bindings.html doc_webgl-enum.html doc_webgl-drawArrays.html doc_webgl-drawElements.html head.js !/devtools/client/shared/test/frame-script-utils.js !/devtools/client/shared/test/shared-head.js + !/devtools/client/debugger/new/test/mochitest/helpers/context.js !/devtools/client/shared/test/telemetry-test-helpers.js [browser_canvas-actor-test-01.js] [browser_canvas-actor-test-02.js] [browser_canvas-actor-test-03.js] [browser_canvas-actor-test-04.js] [browser_canvas-actor-test-05.js] [browser_canvas-actor-test-06.js]
--- a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js @@ -1,20 +1,16 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Tests if the a function call's stack is properly displayed in the UI. */ -// Force the old debugger UI since it's directly used (see Bug 1301705) -Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); -registerCleanupFunction(function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); -}); +requestLongerTimeout(2); async function ifTestingSupported() { const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL); const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; await reload(target); const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); @@ -65,18 +61,14 @@ async function ifTestingSupported() { "doc_simple-canvas-deep-stack.html:35", "The fourth function on the stack has the correct location."); const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-stack-fn-location", callItem.target)); await jumpedToSource; const toolbox = await gDevTools.getToolbox(target); - const { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger"); - - is(view.Sources.selectedValue, getSourceActor(view.Sources, SIMPLE_CANVAS_DEEP_STACK_URL), - "The expected source was shown in the debugger."); - is(view.editor.getCursor().line, 25, - "The expected source line is highlighted in the debugger."); + const dbg = createDebuggerContext(toolbox); + await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 26); await teardown(panel); finish(); }
--- a/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js +++ b/devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js @@ -1,21 +1,17 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Tests if the a function call's stack is properly displayed in the UI * and jumping to source in the debugger for the topmost call item works. */ -// Force the old debugger UI since it's directly used (see Bug 1301705) -Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); -registerCleanupFunction(function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); -}); +requestLongerTimeout(2); async function ifTestingSupported() { const { target, panel } = await initCanvasDebuggerFrontend(SIMPLE_CANVAS_DEEP_STACK_URL); const { window, $, $all, EVENTS, SnapshotsListView, CallsListView } = panel.panelWin; await reload(target); const recordingFinished = once(window, EVENTS.SNAPSHOT_RECORDING_FINISHED); @@ -40,18 +36,14 @@ async function ifTestingSupported() { ok($all(".call-item-stack-fn", callItem.target).length >= 4, "There should be at least 4 functions on the stack for the draw call."); const jumpedToSource = once(window, EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); EventUtils.sendMouseEvent({ type: "mousedown" }, $(".call-item-location", callItem.target)); await jumpedToSource; const toolbox = await gDevTools.getToolbox(target); - const { panelWin: { DebuggerView: view } } = toolbox.getPanel("jsdebugger"); - - is(view.Sources.selectedValue, getSourceActor(view.Sources, SIMPLE_CANVAS_DEEP_STACK_URL), - "The expected source was shown in the debugger."); - is(view.editor.getCursor().line, 23, - "The expected source line is highlighted in the debugger."); + const dbg = createDebuggerContext(toolbox); + await validateDebuggerLocation(dbg, SIMPLE_CANVAS_DEEP_STACK_URL, 24); await teardown(panel); finish(); }
--- a/devtools/client/canvasdebugger/test/head.js +++ b/devtools/client/canvasdebugger/test/head.js @@ -1,21 +1,27 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /* eslint no-unused-vars: [2, {"vars": "local"}] */ /* import-globals-from ../../shared/test/shared-head.js */ +/* import-globals-from ../../debugger/new/test/mochitest/helpers/context.js */ "use strict"; // Load the shared-head file first. Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", this); +// Import helpers for the new debugger +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js", + this); + var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); var { DebuggerClient } = require("devtools/shared/client/debugger-client"); var { DebuggerServer } = require("devtools/server/main"); var { CallWatcherFront } = require("devtools/shared/fronts/call-watcher"); var { CanvasFront } = require("devtools/shared/fronts/canvas"); var { Toolbox } = require("devtools/client/framework/toolbox"); var { isWebGLSupported } = require("devtools/client/shared/webgl-utils"); @@ -168,8 +174,18 @@ function teardown({target}) { removeTab(tab); }); } function getSourceActor(aSources, aURL) { const item = aSources.getItemForAttachment(a => a.source.url === aURL); return item ? item.value : null; } + +async function validateDebuggerLocation(dbg, url, line) { + const location = dbg.selectors.getSelectedLocation(dbg.getState()); + const sourceUrl = dbg.selectors.getSelectedSource(dbg.getState()).url; + + is(sourceUrl, url, + "The expected source was shown in the debugger."); + is(location.line, line, + "The expected source line is highlighted in the debugger."); +}
--- a/devtools/client/debugger/new/test/mochitest/browser.ini +++ b/devtools/client/debugger/new/test/mochitest/browser.ini @@ -1,15 +1,16 @@ [DEFAULT] tags = devtools subsuite = devtools skip-if = (os == 'linux' && debug && bits == 32) support-files = head.js helpers.js + helpers/context.js !/devtools/client/shared/test/shared-head.js !/devtools/client/shared/test/telemetry-test-helpers.js ## START-SOURCEMAPPED-FIXTURES - Generated by examples/sourcemapped/build.js examples/sourcemapped/polyfill-bundle.js examples/sourcemapped/output/parcel/babel-bindings-with-flow.js examples/sourcemapped/output/parcel/babel-bindings-with-flow.map examples/sourcemapped/output/parcel/babel-classes.js examples/sourcemapped/output/parcel/babel-classes.map
--- a/devtools/client/debugger/new/test/mochitest/helpers.js +++ b/devtools/client/debugger/new/test/mochitest/helpers.js @@ -1,16 +1,21 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Helper methods to drive with the debugger during mochitests. This file can be safely * required from other panel test files. */ +// Import helpers for the new debugger +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js", + this); + var { Toolbox } = require("devtools/client/framework/toolbox"); var { Task } = require("devtools/shared/task"); var asyncStorage = require("devtools/shared/async-storage"); const sourceUtils = { isLoaded: source => source.loadedState === "loaded" }; @@ -457,33 +462,16 @@ function isSelectedFrameSelected(dbg, st const isLoaded = source.loadedState && sourceUtils.isLoaded(source); if (!isLoaded) { return false; } return source.id == sourceId; } -function createDebuggerContext(toolbox) { - const panel = toolbox.getPanel("jsdebugger"); - const win = panel.panelWin; - const { store, client, selectors, actions } = panel.getVarsForTests(); - - return { - actions: actions, - selectors: selectors, - getState: store.getState, - store: store, - client: client, - toolbox: toolbox, - win: win, - panel: panel - }; -} - /** * Clear all the debugger related preferences. */ function clearDebuggerPreferences() { asyncStorage.clear() Services.prefs.clearUserPref("devtools.recordreplay.enabled"); Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions"); Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions");
new file mode 100644 --- /dev/null +++ b/devtools/client/debugger/new/test/mochitest/helpers/context.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Helper method to create a "dbg" context for other tools to use + */ + +function createDebuggerContext(toolbox) { + const panel = toolbox.getPanel("jsdebugger"); + const win = panel.panelWin; + const { store, client, selectors, actions } = panel.getVarsForTests(); + + return { + actions: actions, + selectors: selectors, + getState: store.getState, + store: store, + client: client, + toolbox: toolbox, + win: win, + panel: panel + }; +} \ No newline at end of file
--- a/devtools/client/framework/test/browser.ini +++ b/devtools/client/framework/test/browser.ini @@ -40,16 +40,17 @@ support-files = browser_toolbox_options_enable_serviceworkers_testing_frame_script.js browser_toolbox_options_enable_serviceworkers_testing.html serviceworker.js sjs_code_reload.sjs sjs_code_bundle_reload_map.sjs test_browser_toolbox_debugger.js !/devtools/client/debugger/new/test/mochitest/head.js !/devtools/client/debugger/new/test/mochitest/helpers.js + !/devtools/client/debugger/new/test/mochitest/helpers/context.js !/devtools/client/shared/test/frame-script-utils.js !/devtools/client/shared/test/shared-head.js !/devtools/client/shared/test/shared-redux-head.js !/devtools/client/shared/test/telemetry-test-helpers.js [browser_browser_toolbox.js] skip-if = coverage # Bug 1387827 [browser_browser_toolbox_debugger.js]
--- a/devtools/client/framework/test/browser_browser_toolbox_debugger.js +++ b/devtools/client/framework/test/browser_browser_toolbox_debugger.js @@ -14,16 +14,17 @@ PromiseTestUtils.whitelistRejectionsGlob // On debug test runner, it takes about 50s to run the test. requestLongerTimeout(4); const { fetch } = require("devtools/shared/DevToolsUtils"); const debuggerHeadURL = CHROME_URL_ROOT + "../../debugger/new/test/mochitest/head.js"; const helpersURL = CHROME_URL_ROOT + "../../debugger/new/test/mochitest/helpers.js"; +const helpersContextURL = CHROME_URL_ROOT + "../../debugger/new/test/mochitest/helpers/context.js"; const testScriptURL = CHROME_URL_ROOT + "test_browser_toolbox_debugger.js"; add_task(async function runTest() { await new Promise(done => { const options = {"set": [ ["devtools.debugger.prompt-connection", false], ["devtools.debugger.remote-enabled", true], ["devtools.chrome.enabled", true], @@ -119,25 +120,26 @@ add_task(async function runTest() { } }).toSource().replace(/^\(function\(\) \{|\}\)$/g, ""); /* eslint-enable no-unused-vars */ // Stringify testHead's function and remove `(function {` prefix and `})` suffix // to ensure inner symbols gets exposed to next pieces of code // Then inject new debugger head file let { content: debuggerHead } = await fetch(debuggerHeadURL); + // Also include the debugger helpers which are separated from debugger's head to be + // reused in other modules. + const { content: debuggerHelpers } = await fetch(helpersURL); + const { content: debuggerContextHelpers } = await fetch(helpersContextURL); + debuggerHead = debuggerHead + debuggerContextHelpers + debuggerHelpers; + // We remove its import of shared-head, which isn't available in browser toolbox process // And isn't needed thanks to testHead's symbols debuggerHead = debuggerHead.replace(/Services.scriptloader.loadSubScript[^\)]*\);/g, ""); - // Also include the debugger helpers which are separated from debugger's head to be - // reused in other modules. - const { content: debuggerHelpers } = await fetch(helpersURL); - debuggerHead = debuggerHead + debuggerHelpers; - // Finally, fetch the debugger test script that is going to be execute in the browser // toolbox process const testScript = (await fetch(testScriptURL)).content; const source = "try { let testUrl = \"" + testUrl + "\";" + testHead + debuggerHead + testScript + "} catch (e) {" + " dump('Exception: '+ e + ' at ' + e.fileName + ':' + " + " e.lineNumber + '\\nStack: ' + e.stack + '\\n');" + "}";
--- a/devtools/client/framework/test/browser_keybindings_01.js +++ b/devtools/client/framework/test/browser_keybindings_01.js @@ -46,22 +46,16 @@ function buildDevtoolsKeysetMap(keyset) function setupKeyBindingsTest() { for (const win of gDevToolsBrowser._trackedBrowserWindows) { buildDevtoolsKeysetMap(win.document.getElementById("devtoolsKeyset")); } } add_task(async function() { - // Use the new debugger frontend because the old one swallows the netmonitor shortcut: - // https://bugzilla.mozilla.org/show_bug.cgi?id=1370442#c7 - await SpecialPowers.pushPrefEnv({set: [ - ["devtools.debugger.new-debugger-frontend", true] - ]}); - await addTab(TEST_URL); await new Promise(done => waitForFocus(done)); setupKeyBindingsTest(); info("Test the first inspector key (there are 2 of them on Mac)"); const inspectorKeys = allKeys.filter(({ toolId }) => { return toolId === "inspector" || toolId === "inspectorMac";
--- a/devtools/client/framework/test/browser_source_map-01.js +++ b/devtools/client/framework/test/browser_source_map-01.js @@ -16,18 +16,16 @@ PromiseTestUtils.whitelistRejectionsGlob PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/); // Empty page const PAGE_URL = `${URL_ROOT}doc_empty-tab-01.html`; const JS_URL = `${URL_ROOT}code_binary_search.js`; const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`; add_task(async function() { - await pushPref("devtools.debugger.new-debugger-frontend", true); - const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); const service = toolbox.sourceMapURLService; // Inject JS script const sourceSeen = waitForSourceLoad(toolbox, JS_URL); await createScript(JS_URL); await sourceSeen;
--- a/devtools/client/framework/test/browser_source_map-absolute.js +++ b/devtools/client/framework/test/browser_source_map-absolute.js @@ -11,18 +11,16 @@ const { PromiseTestUtils } = scopedCuImp PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/); // Empty page const PAGE_URL = `${URL_ROOT}doc_empty-tab-01.html`; const JS_URL = `${URL_ROOT}code_binary_search_absolute.js`; const ORIGINAL_URL = `${URL_ROOT}code_binary_search.coffee`; add_task(async function() { - await pushPref("devtools.debugger.new-debugger-frontend", true); - const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); const service = toolbox.sourceMapURLService; // Inject JS script const sourceSeen = waitForSourceLoad(toolbox, JS_URL); await createScript(JS_URL); await sourceSeen;
--- a/devtools/client/framework/test/browser_source_map-inline.js +++ b/devtools/client/framework/test/browser_source_map-inline.js @@ -13,18 +13,16 @@ PromiseTestUtils.whitelistRejectionsGlob const TEST_ROOT = "http://example.com/browser/devtools/client/framework/test/"; // Empty page const PAGE_URL = `${TEST_ROOT}doc_empty-tab-01.html`; const JS_URL = `${TEST_ROOT}code_inline_bundle.js`; const ORIGINAL_URL = "webpack:///code_inline_original.js"; add_task(async function() { - await pushPref("devtools.debugger.new-debugger-frontend", true); - const toolbox = await openNewTabAndToolbox(PAGE_URL, "jsdebugger"); const service = toolbox.sourceMapURLService; // Inject JS script const sourceSeen = waitForSourceLoad(toolbox, JS_URL); await createScript(JS_URL); await sourceSeen;
--- a/devtools/client/framework/test/browser_source_map-reload.js +++ b/devtools/client/framework/test/browser_source_map-reload.js @@ -11,18 +11,16 @@ const JS_URL = URL_ROOT + "sjs_code_relo const ORIGINAL_URL_1 = "webpack:///code_reload_1.js"; const ORIGINAL_URL_2 = "webpack:///code_reload_2.js"; const GENERATED_LINE = 86; const ORIGINAL_LINE = 13; add_task(async function() { - await pushPref("devtools.debugger.new-debugger-frontend", true); - // Start with the empty page, then navigate, so that we can properly // listen for new sources arriving. const toolbox = await openNewTabAndToolbox(INITIAL_URL, "webconsole"); const service = toolbox.sourceMapURLService; const tab = toolbox.target.tab; let sourceSeen = waitForSourceLoad(toolbox, JS_URL); tab.linkedBrowser.loadURI(PAGE_URL);
--- a/devtools/client/framework/test/browser_toolbox_split_console.js +++ b/devtools/client/framework/test/browser_toolbox_split_console.js @@ -9,22 +9,16 @@ // * toolbox.useKeyWithSplitConsole() // * toolbox.isSplitConsoleFocused let gToolbox = null; let panelWin = null; const URL = "data:text/html;charset=utf8,test split console key delegation"; -// Force the old debugger UI since it's directly used (see Bug 1301705) -Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); -registerCleanupFunction(function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); -}); - add_task(async function() { const tab = await addTab(URL); const target = TargetFactory.forTab(tab); gToolbox = await gDevTools.showToolbox(target, "jsdebugger"); panelWin = gToolbox.getPanel("jsdebugger").panelWin; await gToolbox.openSplitConsole(); await testIsSplitConsoleFocused();
--- a/devtools/client/framework/test/browser_toolbox_view_source_01.js +++ b/devtools/client/framework/test/browser_toolbox_view_source_01.js @@ -6,39 +6,26 @@ /** * Tests that Toolbox#viewSourceInDebugger works when debugger is not * yet opened. */ var URL = `${URL_ROOT}doc_viewsource.html`; var JS_URL = `${URL_ROOT}code_math.js`; -// Force the old debugger UI since it's directly used (see Bug 1301705) -Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); -registerCleanupFunction(function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); -}); - async function viewSource() { const toolbox = await openNewTabAndToolbox(URL); await toolbox.viewSourceInDebugger(JS_URL, 2); const debuggerPanel = toolbox.getPanel("jsdebugger"); ok(debuggerPanel, "The debugger panel was opened."); is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected."); - const { DebuggerView } = debuggerPanel.panelWin; - const Sources = DebuggerView.Sources; - - is(Sources.selectedValue, getSourceActor(Sources, JS_URL), - "The correct source is shown in the debugger."); - is(DebuggerView.editor.getCursor().line + 1, 2, - "The correct line is highlighted in the debugger's source editor."); - + assertSelectedLocationInDebugger(debuggerPanel, 2, undefined); await closeToolboxAndTab(toolbox); finish(); } function test() { viewSource().then(finish, (aError) => { ok(false, "Got an error: " + aError.message + "\n" + aError.stack); finish();
--- a/devtools/client/framework/test/browser_toolbox_view_source_02.js +++ b/devtools/client/framework/test/browser_toolbox_view_source_02.js @@ -5,47 +5,27 @@ /** * Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded. */ var URL = `${URL_ROOT}doc_viewsource.html`; var JS_URL = `${URL_ROOT}code_math.js`; -// Force the old debugger UI since it's directly used (see Bug 1301705) -Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); -registerCleanupFunction(function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); -}); - async function viewSource() { const toolbox = await openNewTabAndToolbox(URL); - const { panelWin: debuggerWin } = await toolbox.selectTool("jsdebugger"); - const debuggerEvents = debuggerWin.EVENTS; - const { DebuggerView } = debuggerWin; - const Sources = DebuggerView.Sources; - - await debuggerWin.once(debuggerEvents.SOURCE_SHOWN); - ok("A source was shown in the debugger."); - - is(Sources.selectedValue, getSourceActor(Sources, JS_URL), - "The correct source is initially shown in the debugger."); - is(DebuggerView.editor.getCursor().line, 0, - "The correct line is initially highlighted in the debugger's source editor."); + await toolbox.selectTool("jsdebugger"); await toolbox.viewSourceInDebugger(JS_URL, 2); const debuggerPanel = toolbox.getPanel("jsdebugger"); ok(debuggerPanel, "The debugger panel was opened."); is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected."); - is(Sources.selectedValue, getSourceActor(Sources, JS_URL), - "The correct source is shown in the debugger."); - is(DebuggerView.editor.getCursor().line + 1, 2, - "The correct line is highlighted in the debugger's source editor."); + assertSelectedLocationInDebugger(debuggerPanel, 2, undefined); await closeToolboxAndTab(toolbox); finish(); } function test() { viewSource().then(finish, (aError) => { ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
--- a/devtools/client/framework/test/head.js +++ b/devtools/client/framework/test/head.js @@ -366,8 +366,16 @@ async function resizeWindow(toolbox, wid const originalHeight = hostWindow.outerHeight; const toWidth = width || originalWidth; const toHeight = height || originalHeight; const onResize = once(hostWindow, "resize"); hostWindow.resizeTo(toWidth, toHeight); await onResize; } + +function assertSelectedLocationInDebugger(debuggerPanel, line, column) { + const location = debuggerPanel._selectors.getSelectedLocation( + debuggerPanel._getState() + ); + is(location.line, line); + is(location.column, column); +}
--- a/devtools/client/framework/toolbox-options.js +++ b/devtools/client/framework/toolbox-options.js @@ -363,21 +363,16 @@ OptionsPanel.prototype = { const isNightly = AppConstants.NIGHTLY_BUILD; if (!isNightly) { return; } // Labels for these new buttons are nightly only and mostly intended for working on // devtools. const prefDefinitions = [{ - pref: "devtools.debugger.new-debugger-frontend", - label: L10N.getStr("toolbox.options.enableNewDebugger.label"), - id: "devtools-new-debugger", - parentId: "debugger-options" - }, { pref: "devtools.performance.new-panel-enabled", label: "Enable new performance recorder (then re-open DevTools)", id: "devtools-new-performance", parentId: "context-options" }]; const createPreferenceOption = ({pref, label, id}) => { const inputLabel = this.panelDoc.createElement("label");
--- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -103,17 +103,16 @@ var connect = async function() { function setPrefDefaults() { Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true); Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true); Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true); Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true); Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true); // Bug 1225160 - Using source maps with browser debugging can lead to a crash Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false); - Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true); Services.prefs.setBoolPref("devtools.preference.new-panel-enabled", false); Services.prefs.setBoolPref("layout.css.emulate-moz-box-with-flex", false); Services.prefs.setBoolPref("devtools.performance.enabled", false); } window.addEventListener("load", async function() { gShortcuts = new KeyShortcuts({window});
--- a/devtools/client/inspector/markup/test/browser.ini +++ b/devtools/client/inspector/markup/test/browser.ini @@ -65,16 +65,17 @@ support-files = lib_react_16.2.0_min.js lib_react_dom_15.3.1_min.js lib_react_dom_15.4.1.js lib_react_dom_16.2.0_min.js lib_react_with_addons_15.3.1_min.js lib_react_with_addons_15.4.1.js react_external_listeners.js !/devtools/client/debugger/new/test/mochitest/helpers.js + !/devtools/client/debugger/new/test/mochitest/helpers/context.js !/devtools/client/inspector/test/head.js !/devtools/client/inspector/test/shared-head.js !/devtools/client/shared/test/shared-head.js !/devtools/client/shared/test/shared-redux-head.js !/devtools/client/shared/test/telemetry-test-helpers.js !/devtools/client/shared/test/test-actor.js !/devtools/client/shared/test/test-actor-registry.js
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js +++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_open_debugger.js @@ -10,16 +10,21 @@ // Test that the markup view is correctly updated to show those items if the custom // element definition happens after opening the inspector. /* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", this); +/* import-globals-from ../../../debugger/new/test/mochitest/helpers/context.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js", + this); + const TEST_URL = `data:text/html;charset=utf-8,` + encodeURIComponent(` <test-component></test-component> <other-component>some-content</other-component> <script> "use strict"; window.attachTestComponent = function() { customElements.define("test-component", class extends HTMLElement {
--- a/devtools/client/shared/view-source.js +++ b/devtools/client/shared/view-source.js @@ -1,17 +1,16 @@ /* 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"; var Services = require("Services"); var { gDevTools } = require("devtools/client/framework/devtools"); -var { getSourceText } = require("devtools/client/debugger/content/queries"); /** * Tries to open a Stylesheet file in the Style Editor. If the file is not * found, it is opened in source view instead. * Returns a promise resolving to a boolean indicating whether or not * the source was able to be displayed in the StyleEditor, as the built-in * Firefox View Source is the fallback. * @@ -46,83 +45,24 @@ exports.viewSourceInStyleEditor = async * @param {string} sourceURL * @param {number} sourceLine * @param {string} [reason=unknown] * * @return {Promise<boolean>} */ exports.viewSourceInDebugger = async function(toolbox, sourceURL, sourceLine, reason = "unknown") { - // If the Debugger was already open, switch to it and try to show the - // source immediately. Otherwise, initialize it and wait for the sources - // to be added first. - const debuggerAlreadyOpen = toolbox.getPanel("jsdebugger"); const dbg = await toolbox.loadTool("jsdebugger"); - - // New debugger frontend - if (Services.prefs.getBoolPref("devtools.debugger.new-debugger-frontend")) { - const source = dbg.getSource(sourceURL); - if (source) { - await toolbox.selectTool("jsdebugger", reason); - dbg.selectSource(sourceURL, sourceLine); - return true; - } - - exports.viewSource(toolbox, sourceURL, sourceLine); - return false; - } - - const win = dbg.panelWin; - - // Old debugger frontend - if (!debuggerAlreadyOpen) { - await win.DebuggerController.waitForSourcesLoaded(); - } - - const { DebuggerView } = win; - const { Sources } = DebuggerView; - - const item = Sources.getItemForAttachment(a => a.source.url === sourceURL); - if (item) { + const source = dbg.getSource(sourceURL); + if (source) { await toolbox.selectTool("jsdebugger", reason); - - // Determine if the source has already finished loading. There's two cases - // in which we need to wait for the source to be shown: - // 1) The requested source is not yet selected and will be shown once it is - // selected and loaded - // 2) The requested source is selected BUT the source text is still loading. - const { actor } = item.attachment.source; - const state = win.DebuggerController.getState(); - - // (1) Is the source selected? - const selected = state.sources.selectedSource; - const isSelected = selected === actor; - - // (2) Has the source text finished loading? - let isLoading = false; - - // Only check if the source is loading when the source is already selected. - // If the source is not selected, we will select it below and the already - // pending load will be cancelled and this check is useless. - if (isSelected) { - const sourceTextInfo = getSourceText(state, selected); - isLoading = sourceTextInfo && sourceTextInfo.loading; - } - - // Select the requested source - DebuggerView.setEditorLocation(actor, sourceLine, { noDebug: true }); - - // Wait for it to load - if (!isSelected || isLoading) { - await win.DebuggerController.waitForSourceShown(sourceURL); - } + dbg.selectSource(sourceURL, sourceLine); return true; } - // If not found, still attempt to open in View Source exports.viewSource(toolbox, sourceURL, sourceLine); return false; }; /** * Tries to open a JavaScript file in the corresponding Scratchpad. * * @param {string} sourceURL
--- a/devtools/client/webconsole/test/mochitest/browser.ini +++ b/devtools/client/webconsole/test/mochitest/browser.ini @@ -151,16 +151,18 @@ support-files = test-trackingprotection-securityerrors.html test-webconsole-error-observer.html test-websocket.html test-websocket.js testscript.js !/devtools/client/netmonitor/test/sjs_cors-test-server.sjs !/image/test/mochitest/blue.png !/devtools/client/shared/test/shared-head.js + !/devtools/client/debugger/new/test/mochitest/helpers.js + !/devtools/client/debugger/new/test/mochitest/helpers/context.js !/devtools/client/shared/test/telemetry-test-helpers.js !/devtools/client/shared/test/test-actor.js !/devtools/client/shared/test/test-actor-registry.js [browser_console.js] [browser_console_clear_cache.js] [browser_console_clear_method.js] skip-if = true # Bug 1437843 @@ -246,17 +248,16 @@ tags = mcb [browser_webconsole_cd_iframe.js] [browser_webconsole_certificate_messages.js] [browser_webconsole_clear_cache.js] [browser_webconsole_click_function_to_source.js] [browser_webconsole_clickable_urls.js] [browser_webconsole_close_unfocused_window.js] [browser_webconsole_closing_after_completion.js] [browser_webconsole_close_sidebar.js] -[browser_webconsole_closure_inspection.js] skip-if = true # Bug 1405250 [browser_webconsole_console_api_iframe.js] [browser_webconsole_console_dir.js] [browser_webconsole_console_dir_uninspectable.js] [browser_webconsole_console_error_expand_object.js] [browser_webconsole_console_group.js] [browser_webconsole_console_logging_workers_api.js] [browser_webconsole_console_table.js]
--- a/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_in_debugger_stackframe.js +++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_in_debugger_stackframe.js @@ -3,32 +3,35 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Test that makes sure web console autocomplete happens in the user-selected // stackframe from the js debugger. "use strict"; +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", + this); + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/test-autocomplete-in-stackframe.html"; add_task(async function() { // Run test with legacy JsTerm await pushPref("devtools.webconsole.jsterm.codeMirror", false); await performTests(); // And then run it with the CodeMirror-powered one. await pushPref("devtools.webconsole.jsterm.codeMirror", true); await performTests(); }); async function performTests() { - // Force the old debugger UI since it's directly used (see Bug 1301705) - await pushPref("devtools.debugger.new-debugger-frontend", false); - const { jsterm } = await openNewTabAndConsole(TEST_URI); const { autocompletePopup: popup, } = jsterm; const target = TargetFactory.forTab(gBrowser.selectedTab); const toolbox = gDevTools.getToolbox(target); @@ -55,34 +58,36 @@ async function performTests() { `"foo1Obj." gave the expected suggestions`); // Test if 'foo1Obj.prop2.' gives 'prop21' await jstermComplete("foo1Obj.prop2."); ok(getPopupLabels(popup).includes("prop21"), `"foo1Obj.prop2." gave the expected suggestions`); info("Opening Debugger"); - const {panel} = await openDebugger(); + await openDebugger(); + const dbg = createDebuggerContext(toolbox); info("Waiting for pause"); - const stackFrames = await pauseDebugger(panel); + await pauseDebugger(dbg); + const stackFrames = dbg.selectors.getCallStackFrames(dbg.getState()); info("Opening Console again"); await toolbox.selectTool("webconsole"); // Test if 'foo' gives 'foo3' and 'foo1' but not 'foo2', since we are paused in // the `secondCall` function (called by `firstCall`, which we call in `pauseDebugger`). await jstermComplete("foo"); is(getPopupLabels(popup).join("-"), "foo1-foo1Obj-foo3-foo3Obj", `"foo" gave the expected suggestions`); await openDebugger(); // Select the frame for the `firstCall` function. - stackFrames.selectFrame(1); + await dbg.actions.selectFrame(stackFrames[1]); info("openConsole"); await toolbox.selectTool("webconsole"); // Test if 'foo' gives 'foo2' and 'foo1' but not 'foo3', since we are now in the // `firstCall` frame. await jstermComplete("foo"); is(getPopupLabels(popup).join("-"), "foo1-foo1Obj-foo2-foo2Obj", @@ -104,23 +109,15 @@ async function performTests() { await jstermComplete("foo2Obj[0]."); is(getPopupLabels(popup).length, 0, "no items for foo2Obj[0]"); } function getPopupLabels(popup) { return popup.getItems().map(item => item.label); } -function pauseDebugger(debuggerPanel) { - const debuggerWin = debuggerPanel.panelWin; - const debuggerController = debuggerWin.DebuggerController; - const thread = debuggerController.activeThread; - - return new Promise(resolve => { - thread.addOneTimeListener("framesadded", () => - resolve(debuggerController.StackFrames)); - - info("firstCall()"); - ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { - content.wrappedJSObject.firstCall(); - }); +async function pauseDebugger(dbg) { + info("Waiting for debugger to pause"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() { + content.wrappedJSObject.firstCall(); }); + await waitForPaused(dbg); }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_click_function_to_source.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_click_function_to_source.js @@ -2,45 +2,53 @@ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Tests that clicking on a function displays its source in the debugger. See Bug 1050691. "use strict"; +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", + this); + +requestLongerTimeout(5); + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/" + "test-click-function-to-source.html"; -// Force the old debugger UI since it's directly used (see Bug 1301705) -pushPref("devtools.debugger.new-debugger-frontend", false); +const TEST_SCRIPT_URI = "http://example.com/browser/devtools/client/webconsole/" + + "test/mochitest/" + + "test-click-function-to-source.js"; add_task(async function() { const hud = await openNewTabAndConsole(TEST_URI); info("Open the Debugger panel."); - const {panel} = await openDebugger(); - const panelWin = panel.panelWin; + await openDebugger(); info("And right after come back to the Console panel."); await openConsole(); info("Log a function"); const onLoggedFunction = waitForMessage(hud, "function foo"); ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { content.wrappedJSObject.foo(); }); const {node} = await onLoggedFunction; const jumpIcon = node.querySelector(".jump-definition"); ok(jumpIcon, "A jump to definition button is rendered, as expected"); info("Click on the jump to definition button."); - const onEditorLocationSet = panelWin.once(panelWin.EVENTS.EDITOR_LOCATION_SET); jumpIcon.click(); - await onEditorLocationSet; - const {editor} = panelWin.DebuggerView; - const {line, ch} = editor.getCursor(); - // Source editor starts counting line and column numbers from 0. - is(line, 8, "Debugger is open at the expected line"); - is(ch, 0, "Debugger is open at the expected character"); + const toolbox = gDevTools.getToolbox(hud.target); + const dbg = createDebuggerContext(toolbox); + await waitForSelectedSource(dbg, TEST_SCRIPT_URI); + + const pendingLocation = dbg.selectors.getPendingSelectedLocation(dbg.getState()); + const {line} = pendingLocation; + is(line, 9, "Debugger is open at the expected line"); });
deleted file mode 100644 --- a/devtools/client/webconsole/test/mochitest/browser_webconsole_closure_inspection.js +++ /dev/null @@ -1,104 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -// XXX Remove this when the file is migrated to the new frontend. -/* eslint-disable no-undef */ - -// Check that inspecting a closure in the variables view sidebar works when -// execution is paused. - -"use strict"; - -const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + - "test/mochitest/test-closures.html"; - -var gWebConsole, gJSTerm, gVariablesView; - -// Force the old debugger UI since it's directly used (see Bug 1301705) -Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false); -registerCleanupFunction(function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); -}); - -function test() { - registerCleanupFunction(() => { - gWebConsole = gJSTerm = gVariablesView = null; - }); - - function fetchScopes(hud, toolbox, panelWin, deferred) { - panelWin.once(panelWin.EVENTS.FETCHED_SCOPES, () => { - ok(true, "Scopes were fetched"); - toolbox.selectTool("webconsole").then(() => consoleOpened(hud)); - deferred.resolve(); - }); - } - - loadTab(TEST_URI).then(() => { - openConsole().then((hud) => { - openDebugger().then(({ toolbox, panelWin }) => { - const deferred = defer(); - fetchScopes(hud, toolbox, panelWin, deferred); - - // eslint-disable-next-line - ContentTask.spawn(gBrowser.selectedBrowser, {}, () => { - const button = content.document.querySelector("button"); - ok(button, "button element found"); - button.click(); - }); - - return deferred.promise; - }); - }); - }); -} - -function consoleOpened(hud) { - gWebConsole = hud; - gJSTerm = hud.jsterm; - gJSTerm.execute("window.george.getName"); - - waitForMessages({ - webconsole: gWebConsole, - messages: [{ - text: "getName()", - category: CATEGORY_OUTPUT, - objects: true, - }], - }).then(onExecuteGetName); -} - -function onExecuteGetName(results) { - const clickable = results[0].clickableElements[0]; - ok(clickable, "clickable object found"); - - gJSTerm.once("variablesview-fetched", onGetNameFetch); - const contextMenu = - gWebConsole.iframeWindow.document.getElementById("output-contextmenu"); - waitForContextMenu(contextMenu, clickable, () => { - const openInVarView = contextMenu.querySelector("#menu_openInVarView"); - ok(openInVarView.disabled === false, - "the \"Open In Variables View\" context menu item should be clickable"); - // EventUtils.synthesizeMouseAtCenter seems to fail here in Mac OSX - openInVarView.click(); - }); -} - -function onGetNameFetch(view) { - gVariablesView = view._variablesView; - ok(gVariablesView, "variables view object"); - - findVariableViewProperties(view, [ - { name: /_pfactory/, value: "" }, - ], { webconsole: gWebConsole }).then(onExpandClosure); -} - -function onExpandClosure(results) { - const prop = results[0].matchedProp; - ok(prop, "matched the name property in the variables view"); - - gVariablesView.window.focus(); - gJSTerm.once("sidebar-closed", finishTest); - EventUtils.synthesizeKey("VK_ESCAPE", {}, gVariablesView.window); -}
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe.js @@ -3,23 +3,26 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Test that makes sure web console eval happens in the user-selected stackframe // from the js debugger. "use strict"; +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", + this); + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/test-eval-in-stackframe.html"; add_task(async function() { - // Force the old debugger UI since it's directly used (see Bug 1301705). - await pushPref("devtools.debugger.new-debugger-frontend", false); - info("open the console"); const hud = await openNewTabAndConsole(TEST_URI); const {jsterm} = hud; info("Check `foo` value"); let onResultMessage = waitForMessage(hud, "globalFooBug783499"); jsterm.execute("foo"); await onResultMessage; @@ -27,47 +30,46 @@ add_task(async function() { info("Assign and check `foo2` value"); onResultMessage = waitForMessage(hud, "newFoo"); jsterm.execute("foo2 = 'newFoo'; window.foo2"); await onResultMessage; ok(true, "'newFoo' is displayed after adding `foo2`"); info("Open the debugger and then select the console again"); - const {panel} = await openDebugger(); - const {activeThread, StackFrames: stackFrames} = panel.panelWin.DebuggerController; + await openDebugger(); + const toolbox = gDevTools.getToolbox(hud.target); + const dbg = createDebuggerContext(toolbox); await openConsole(); info("Check `foo + foo2` value"); onResultMessage = waitForMessage(hud, "globalFooBug783499newFoo"); jsterm.execute("foo + foo2"); await onResultMessage; info("Select the debugger again"); await openDebugger(); + await pauseDebugger(dbg); - const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded"); - // firstCall calls secondCall, which has a debugger statement, so we'll be paused. - ContentTask.spawn(gBrowser.selectedBrowser, {}, function() { - content.wrappedJSObject.firstCall(); - }); - await onFirstCallFramesAdded; + const stackFrames = dbg.selectors.getCallStackFrames(dbg.getState()); info("frames added, select the console again"); await openConsole(); info("Check `foo + foo2` value when paused"); onResultMessage = waitForMessage(hud, "globalFooBug783499foo2SecondCall"); jsterm.execute("foo + foo2"); ok(true, "`foo + foo2` from `secondCall()`"); info("select the debugger and select the frame (1)"); await openDebugger(); - stackFrames.selectFrame(1); + + await dbg.actions.selectFrame(stackFrames[1]); + await openConsole(); info("Check `foo + foo2 + foo3` value when paused on a given frame"); onResultMessage = waitForMessage(hud, "fooFirstCallnewFoofoo3FirstCall"); jsterm.execute("foo + foo2 + foo3"); await onResultMessage; ok(true, "`foo + foo2 + foo3` from `firstCall()`"); @@ -77,8 +79,16 @@ add_task(async function() { ok(true, "`foo + foo3` updated in `firstCall()`"); await ContentTask.spawn(gBrowser.selectedBrowser, null, function() { is(content.wrappedJSObject.foo, "globalFooBug783499", "`foo` in content window"); is(content.wrappedJSObject.foo2, "newFoo", "`foo2` in content window"); ok(!content.wrappedJSObject.foo3, "`foo3` was not added to the content window"); }); }); + +async function pauseDebugger(dbg) { + info("Waiting for debugger to pause"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() { + content.wrappedJSObject.firstCall(); + }); + await waitForPaused(dbg); +}
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe2.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_eval_in_debugger_stackframe2.js @@ -4,44 +4,48 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ // Test to make sure that web console commands can fire while paused at a // breakpoint that was triggered from a JS call. Relies on asynchronous js // evaluation over the protocol - see Bug 1088861. "use strict"; +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", + this); + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/test-eval-in-stackframe.html"; add_task(async function() { - // Force the old debugger UI since it's directly used (see Bug 1301705). - await pushPref("devtools.debugger.new-debugger-frontend", false); - info("open the console"); const hud = await openNewTabAndConsole(TEST_URI); const {jsterm} = hud; info("open the debugger"); - const {panel} = await openDebugger(); - const {activeThread} = panel.panelWin.DebuggerController; + await openDebugger(); - const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded"); + const toolbox = gDevTools.getToolbox(hud.target); + const dbg = createDebuggerContext(toolbox); + // firstCall calls secondCall, which has a debugger statement, so we'll be paused. const onFirstCallMessageReceived = waitForMessage(hud, "undefined"); const unresolvedSymbol = Symbol(); let firstCallEvaluationResult = unresolvedSymbol; onFirstCallMessageReceived.then(message => { firstCallEvaluationResult = message; }); jsterm.execute("firstCall()"); info("Waiting for a frame to be added"); - await onFirstCallFramesAdded; + await waitForPaused(dbg); info("frames added, select the console again"); await openConsole(); info("Executing basic command while paused"); let onMessageReceived = waitForMessage(hud, "3"); jsterm.execute("1 + 2"); let message = await onMessageReceived; @@ -52,14 +56,14 @@ add_task(async function() { jsterm.execute("foo + foo2"); message = await onMessageReceived; ok(message, "`foo + foo2` was evaluated as expected with debugger paused"); info("Checking the first command, which is the last to resolve since it paused"); ok(firstCallEvaluationResult === unresolvedSymbol, "firstCall was not evaluated yet"); info("Resuming the thread"); - activeThread.resume(); + dbg.actions.resume(dbg.getState()); message = await onFirstCallMessageReceived; ok(firstCallEvaluationResult !== unresolvedSymbol, "firstCall() returned correct value"); });
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_location_debugger_link.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_location_debugger_link.js @@ -14,19 +14,16 @@ const { PromiseTestUtils } = scopedCuImp PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/); requestLongerTimeout(2); const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/test-location-debugger-link.html"; add_task(async function() { - // Force the new debugger UI, in case this gets uplifted with the old - // debugger still turned on - await pushPref("devtools.debugger.new-debugger-frontend", true); await pushPref("devtools.webconsole.filter.error", true); await pushPref("devtools.webconsole.filter.log", true); // On e10s, the exception thrown in test-location-debugger-link-errors.js // is triggered in child process and is ignored by test harness if (!Services.appinfo.browserTabsRemoteAutostart) { expectUncaughtException(); }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_while_debugging_and_inspecting.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_object_inspector_while_debugging_and_inspecting.js @@ -3,33 +3,39 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Test that makes sure web console eval works while the js debugger paused the // page, and while the inspector is active. See bug 886137. "use strict"; +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", + this); + const TEST_URI = "https://example.com/browser/devtools/client/webconsole/" + "test/mochitest/test-eval-in-stackframe.html"; add_task(async function() { - // Force the old debugger UI since it's directly used (see Bug 1301705) - await pushPref("devtools.debugger.new-debugger-frontend", false); const hud = await openNewTabAndConsole(TEST_URI); info("Switch to the debugger"); await openDebugger(); info("Switch to the inspector"); const target = TargetFactory.forTab(gBrowser.selectedTab); await gDevTools.showToolbox(target, "inspector"); info("Call firstCall() and wait for the debugger statement to be reached."); - await waitForFrameAdded(); + const toolbox = gDevTools.getToolbox(target); + const dbg = createDebuggerContext(toolbox); + await pauseDebugger(dbg); info("Switch back to the console"); await gDevTools.showToolbox(target, "webconsole"); info("Test logging and inspecting objects while on a breakpoint."); const jsterm = hud.jsterm; const onMessage = waitForMessage(hud, '{ testProp2: "testValue2" }'); @@ -58,21 +64,15 @@ add_task(async function() { const oiNodes = oi.querySelectorAll(".node"); is(oiNodes.length, 3, "There is the expected number of nodes in the tree"); ok(oiNodes[0].textContent.includes(`{\u2026}`)); ok(oiNodes[1].textContent.includes(`testProp2: "testValue2"`)); ok(oiNodes[2].textContent.includes(`<prototype>: Object { \u2026 }`)); }); -async function waitForFrameAdded() { - const target = TargetFactory.forTab(gBrowser.selectedTab); - const toolbox = gDevTools.getToolbox(target); - const thread = toolbox.threadClient; - - info("Waiting for framesadded"); - await new Promise(resolve => { - thread.addOneTimeListener("framesadded", resolve); - ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() { - content.wrappedJSObject.firstCall(); - }); +async function pauseDebugger(dbg) { + info("Waiting for debugger to pause"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() { + content.wrappedJSObject.firstCall(); }); + await waitForPaused(dbg); }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_optimized_out_vars.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_optimized_out_vars.js @@ -3,57 +3,61 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Check that inspecting an optimized out variable works when execution is // paused. "use strict"; +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers.js", + this); + const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/" + "test-closure-optimized-out.html"; add_task(async function() { - // Force the old debugger UI since it's directly used (see Bug 1301705) - await pushPref("devtools.debugger.new-debugger-frontend", false); - const hud = await openNewTabAndConsole(TEST_URI); - const { toolbox, panel: debuggerPanel } = await openDebugger(); + await openDebugger(); - const sources = debuggerPanel.panelWin.DebuggerView.Sources; - await debuggerPanel.addBreakpoint({ actor: sources.values[0], line: 18 }); - await ensureThreadClientState(debuggerPanel, "resumed"); + const toolbox = gDevTools.getToolbox(hud.target); + const dbg = createDebuggerContext(toolbox); - const { FETCHED_SCOPES } = debuggerPanel.panelWin.EVENTS; - const fetchedScopes = debuggerPanel.panelWin.once(FETCHED_SCOPES); + await addBreakpoint(dbg, "test-closure-optimized-out.html", 18); + await waitForThreadEvents(dbg, "resumed"); // Cause the debuggee to pause - ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() { - const button = content.document.querySelector("button"); - button.click(); - }); - - await fetchedScopes; - ok(true, "Scopes were fetched"); + await pauseDebugger(dbg); await toolbox.selectTool("webconsole"); // This is the meat of the test: evaluate the optimized out variable. const onMessage = waitForMessage(hud, "optimized out"); hud.jsterm.execute("upvar"); info("Waiting for optimized out message"); await onMessage; ok(true, "Optimized out message logged"); + + info("Open the debugger"); + await openDebugger(); + + info("Resume"); + await resume(dbg); + + info("Remove the breakpoint"); + const source = findSource(dbg, "test-closure-optimized-out.html"); + await removeBreakpoint(dbg, source.id, 18); }); -// Debugger helper functions adapted from devtools/client/debugger/test/head.js. - -async function ensureThreadClientState(debuggerPanel, state) { - const thread = debuggerPanel.panelWin.gThreadClient; - info(`Thread is: '${thread.state}'.`); - if (thread.state != state) { - info("Waiting for thread event: '${state}'."); - await thread.addOneTimeListener(state); - } +async function pauseDebugger(dbg) { + info("Waiting for debugger to pause"); + ContentTask.spawn(gBrowser.selectedBrowser, {}, async function() { + const button = content.document.querySelector("button"); + button.click(); + }); + await waitForPaused(dbg); }
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_sourcemap_nosource.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_sourcemap_nosource.js @@ -20,19 +20,16 @@ const PAGE_URL = `data:text/html, <body> <script src="${JS_URL}"></script> </body> </html>`; add_task(async function() { - // Force the new debugger UI, in case this gets uplifted with the old - // debugger still turned on - await pushPref("devtools.debugger.new-debugger-frontend", true); await pushPref("devtools.source-map.client-service.enabled", true); const hud = await openNewTabAndConsole(PAGE_URL); const toolbox = hud.ui.consoleOutput.toolbox; info("Finding \"here\" message and waiting for source map to be applied"); await waitFor(() => { const node = findMessage(hud, "here");
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_stacktrace_location_debugger_link.js +++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_stacktrace_location_debugger_link.js @@ -14,22 +14,18 @@ const { PromiseTestUtils } = scopedCuImp PromiseTestUtils.whitelistRejectionsGlobally(/Component not initialized/); PromiseTestUtils.whitelistRejectionsGlobally(/this\.worker is null/); const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/mochitest/" + "test-stacktrace-location-debugger-link.html"; add_task(async function() { - // Force the new debugger UI, in case this gets uplifted with the old - // debugger still turned on - Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true); Services.prefs.setBoolPref("devtools.webconsole.filter.log", true); registerCleanupFunction(async function() { - Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend"); Services.prefs.clearUserPref("devtools.webconsole.filter.log"); }); const hud = await openNewTabAndConsole(TEST_URI); const target = TargetFactory.forTab(gBrowser.selectedTab); const toolbox = gDevTools.getToolbox(target); await testOpenInDebugger(hud, toolbox, "console.trace()");
--- a/devtools/client/webconsole/test/mochitest/head.js +++ b/devtools/client/webconsole/test/mochitest/head.js @@ -14,16 +14,22 @@ Services.scriptloader.loadSubScript( // shared-head.js handles imports, constants, and utility functions // Load the shared-head file first. /* import-globals-from ../../../shared/test/shared-head.js */ Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", this); +// Import helpers for the new debugger +/* import-globals-from ../../../debugger/new/test/mochitest/helpers/context.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/debugger/new/test/mochitest/helpers/context.js", + this); + var {HUDService} = require("devtools/client/webconsole/hudservice"); var WCUL10n = require("devtools/client/webconsole/webconsole-l10n"); const DOCS_GA_PARAMS = `?${new URLSearchParams({ "utm_source": "mozilla", "utm_medium": "firefox-console-errors", "utm_campaign": "default" })}`; const STATUS_CODES_GA_PARAMS = `?${new URLSearchParams({ @@ -530,19 +536,27 @@ async function openDebugger(options = {} panel: toolbox.getCurrentPanel() }; } toolbox = await gDevTools.showToolbox(target, "jsdebugger"); const panel = toolbox.getCurrentPanel(); // Do not clear VariableView lazily so it doesn't disturb test ending. - panel._view.Variables.lazyEmpty = false; + if (panel._view) { + panel._view.Variables.lazyEmpty = false; + } - await panel.panelWin.DebuggerController.waitForSourcesLoaded(); + // Old debugger + if (panel.panelWin && panel.panelWin.DebuggerController) { + await panel.panelWin.DebuggerController.waitForSourcesLoaded(); + } else { + // New debugger + await toolbox.threadClient.getSources(); + } return {target, toolbox, panel}; } async function openInspector(options = {}) { if (!options.tab) { options.tab = gBrowser.selectedTab; }
--- a/devtools/server/tests/unit/test_objectgrips-17.js +++ b/devtools/server/tests/unit/test_objectgrips-17.js @@ -86,16 +86,23 @@ async function testPrincipal(globalPrinc for (const globalHasXrays of [true, false]) { gIsOpaque = gSubsumes && globalPrincipal !== systemPrincipal && (sameOrigin && gDebuggeeHasXrays || globalHasXrays); for (gGlobalIsInvisible of [true, false]) { gGlobal = Cu.Sandbox(globalPrincipal, { wantXrays: globalHasXrays, invisibleToDebugger: gGlobalIsInvisible }); + // Previously, the Sandbox constructor would (bizarrely) waive xrays on + // the return Sandbox if wantXrays was false. This has now been fixed, + // but we need to mimic that behavior here to make the test continue + // to pass. + if (!globalHasXrays) { + gGlobal = Cu.waiveXrays(gGlobal); + } await test(); } } } function test() { return new Promise(function(resolve) { gThreadClient.addOneTimeListener("paused", async function(event, packet) {
--- a/dom/base/IframeSandboxKeywordList.h +++ b/dom/base/IframeSandboxKeywordList.h @@ -20,8 +20,10 @@ SANDBOX_KEYWORD("allow-pointer-lock", al SANDBOX_KEYWORD("allow-orientation-lock", alloworientationlock, SANDBOXED_ORIENTATION_LOCK) SANDBOX_KEYWORD("allow-popups", allowpopups, SANDBOXED_AUXILIARY_NAVIGATION) SANDBOX_KEYWORD("allow-modals", allowmodals, SANDBOXED_MODALS) SANDBOX_KEYWORD("allow-popups-to-escape-sandbox", allowpopupstoescapesandbox, SANDBOX_PROPAGATES_TO_AUXILIARY_BROWSING_CONTEXTS) SANDBOX_KEYWORD("allow-presentation", allowpresentation, SANDBOXED_PRESENTATION) +SANDBOX_KEYWORD("allow-storage-access-by-user-activation", + allowstorageaccessbyuseractivatetion, SANDBOXED_STORAGE_ACCESS)
--- a/dom/base/nsContentList.cpp +++ b/dom/base/nsContentList.cpp @@ -20,16 +20,17 @@ #include "nsGkAtoms.h" #include "mozilla/dom/HTMLCollectionBinding.h" #include "mozilla/dom/NodeListBinding.h" #include "mozilla/Likely.h" #include "nsGenericHTMLElement.h" #include "jsfriendapi.h" #include <algorithm> #include "mozilla/dom/NodeInfoInlines.h" +#include "mozilla/MruCache.h" #include "PLDHashTable.h" #ifdef DEBUG_CONTENT_LIST #include "nsIContentIterator.h" #define ASSERT_IN_SYNC AssertInSync() #else #define ASSERT_IN_SYNC PR_BEGIN_MACRO PR_END_MACRO @@ -167,25 +168,30 @@ nsIContent* nsEmptyContentList::Item(uint32_t aIndex) { return nullptr; } // Hashtable for storing nsContentLists static PLDHashTable* gContentListHashTable; -#define RECENTLY_USED_CONTENT_LIST_CACHE_SIZE 31 -static nsContentList* - sRecentlyUsedContentLists[RECENTLY_USED_CONTENT_LIST_CACHE_SIZE] = {}; +struct ContentListCache : + public MruCache<nsContentListKey, nsContentList*, ContentListCache> +{ + static HashNumber Hash(const nsContentListKey& aKey) + { + return aKey.GetHash(); + } + static bool Match(const nsContentListKey& aKey, const nsContentList* aVal) + { + return aVal->MatchesKey(aKey); + } +}; -static MOZ_ALWAYS_INLINE uint32_t -RecentlyUsedCacheIndex(const nsContentListKey& aKey) -{ - return aKey.GetHash() % RECENTLY_USED_CONTENT_LIST_CACHE_SIZE; -} +static ContentListCache sRecentlyUsedContentLists; struct ContentListHashEntry : public PLDHashEntryHdr { nsContentList* mContentList; }; static PLDHashNumber ContentListHashtableHashKey(const void *key) @@ -210,20 +216,19 @@ NS_GetContentList(nsINode* aRootNode, int32_t aMatchNameSpaceId, const nsAString& aTagname) { NS_ASSERTION(aRootNode, "content list has to have a root"); RefPtr<nsContentList> list; nsContentListKey hashKey(aRootNode, aMatchNameSpaceId, aTagname, aRootNode->OwnerDoc()->IsHTMLDocument()); - uint32_t recentlyUsedCacheIndex = RecentlyUsedCacheIndex(hashKey); - nsContentList* cachedList = sRecentlyUsedContentLists[recentlyUsedCacheIndex]; - if (cachedList && cachedList->MatchesKey(hashKey)) { - list = cachedList; + auto p = sRecentlyUsedContentLists.Lookup(hashKey); + if (p) { + list = p.Data(); return list.forget(); } static const PLDHashTableOps hash_table_ops = { ContentListHashtableHashKey, ContentListHashtableMatchEntry, PLDHashTable::MoveEntryStub, @@ -255,17 +260,17 @@ NS_GetContentList(nsINode* aRootNode, htmlAtom = xmlAtom; } list = new nsContentList(aRootNode, aMatchNameSpaceId, htmlAtom, xmlAtom); if (entry) { entry->mContentList = list; } } - sRecentlyUsedContentLists[recentlyUsedCacheIndex] = list; + p.Set(list); return list.forget(); } #ifdef DEBUG const nsCacheableFuncStringContentList::ContentListType nsCachableElementsByNameNodeList::sType = nsCacheableFuncStringContentList::eNodeList; const nsCacheableFuncStringContentList::ContentListType nsCacheableFuncStringHTMLCollection::sType = nsCacheableFuncStringContentList::eHTMLCollection; @@ -923,20 +928,17 @@ nsContentList::RemoveFromHashtable() { if (mFunc) { // This can't be in the table anyway return; } nsDependentAtomString str(mXMLMatchAtom); nsContentListKey key(mRootNode, mMatchNameSpaceId, str, mIsHTMLDocument); - uint32_t recentlyUsedCacheIndex = RecentlyUsedCacheIndex(key); - if (sRecentlyUsedContentLists[recentlyUsedCacheIndex] == this) { - sRecentlyUsedContentLists[recentlyUsedCacheIndex] = nullptr; - } + sRecentlyUsedContentLists.Remove(key); if (!gContentListHashTable) return; gContentListHashTable->Remove(&key); if (gContentListHashTable->EntryCount() == 0) { delete gContentListHashTable;
--- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -112,16 +112,17 @@ #include "nsIPermissionManager.h" #include "nsIPrincipal.h" #include "ExpandedPrincipal.h" #include "mozilla/NullPrincipal.h" #include "nsIDOMWindow.h" #include "nsPIDOMWindow.h" #include "nsFocusManager.h" +#include "nsICookieService.h" // for radio group stuff #include "nsIRadioVisitor.h" #include "nsIFormControl.h" #include "nsBidiUtils.h" #include "nsContentCreatorFunctions.h" @@ -13542,16 +13543,222 @@ nsIDocument::GetSelection(ErrorResult& a if (!window->IsCurrentInnerWindow()) { return nullptr; } return nsGlobalWindowInner::Cast(window)->GetSelection(aRv); } +already_AddRefed<mozilla::dom::Promise> +nsIDocument::HasStorageAccess(mozilla::ErrorResult& aRv) +{ + nsIGlobalObject* global = GetScopeObject(); + if (!global) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (NodePrincipal()->GetIsNullPrincipal()) { + promise->MaybeResolve(false); + return promise.forget(); + } + + if (IsTopLevelContentDocument()) { + promise->MaybeResolve(true); + return promise.forget(); + } + + nsCOMPtr<nsIDocument> topLevelDoc = GetTopLevelContentDocument(); + if (!topLevelDoc) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; + } + if (topLevelDoc->NodePrincipal()->Equals(NodePrincipal())) { + promise->MaybeResolve(true); + return promise.forget(); + } + + if (StaticPrefs::browser_contentblocking_enabled() && + StaticPrefs::network_cookie_cookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER) { + // If we need to abide by Content Blocking cookie restrictions, ensure to + // first do all of our storage access checks. If storage access isn't + // disabled in our document, given that we're a third-party, we must either + // not be a tracker, or be whitelisted for some reason (e.g. a storage + // access permission being granted). In that case, resolve the promise and + // say we have obtained storage access. + if (!nsContentUtils::StorageDisabledByAntiTracking(this, nullptr)) { + // Note, storage might be allowed because the top-level document is on + // the content blocking allowlist! In that case, don't provide special + // treatment here. + bool isOnAllowList = false; + if (NS_SUCCEEDED(AntiTrackingCommon::IsOnContentBlockingAllowList( + topLevelDoc->GetDocumentURI(), isOnAllowList)) && + !isOnAllowList) { + promise->MaybeResolve(true); + return promise.forget(); + } + } + } + + nsPIDOMWindowInner* inner = GetInnerWindow(); + nsGlobalWindowOuter* outer = nullptr; + if (inner) { + outer = nsGlobalWindowOuter::Cast(inner->GetOuterWindow()); + promise->MaybeResolve(outer->HasStorageAccess()); + } else { + promise->MaybeRejectWithUndefined(); + } + return promise.forget(); +} + +already_AddRefed<mozilla::dom::Promise> +nsIDocument::RequestStorageAccess(mozilla::ErrorResult& aRv) +{ + nsIGlobalObject* global = GetScopeObject(); + if (!global) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Step 1. If the document already has been granted access, resolve. + nsPIDOMWindowInner* inner = GetInnerWindow(); + nsGlobalWindowOuter* outer = nullptr; + if (inner) { + outer = nsGlobalWindowOuter::Cast(inner->GetOuterWindow()); + if (outer->HasStorageAccess()) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + } + + // Step 2. If the document has a null origin, reject. + if (NodePrincipal()->GetIsNullPrincipal()) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + // Only enforce third-party checks when there is a reason to enforce them. + if (StaticPrefs::network_cookie_cookieBehavior() != + nsICookieService::BEHAVIOR_ACCEPT) { + // Step 3. If the document's frame is the main frame, resolve. + if (IsTopLevelContentDocument()) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // Step 4. If the sub frame's origin is equal to the main frame's, resolve. + nsCOMPtr<nsIDocument> topLevelDoc = GetTopLevelContentDocument(); + if (!topLevelDoc) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; + } + if (topLevelDoc->NodePrincipal()->Equals(NodePrincipal())) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + } + + // Step 5. If the sub frame is not sandboxed, skip to step 7. + // Step 6. If the sub frame doesn't have the token + // "allow-storage-access-by-user-activation", reject. + if (mSandboxFlags & SANDBOXED_STORAGE_ACCESS) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + // Step 7. If the sub frame's parent frame is not the top frame, reject. + nsIDocument* parent = GetParentDocument(); + if (parent && !parent->IsTopLevelContentDocument()) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + // Step 8. If the browser is not processing a user gesture, reject. + if (!EventStateManager::IsHandlingUserInput()) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + // Step 9. Check any additional rules that the browser has. + // Examples: Whitelists, blacklists, on-device classification, + // user settings, anti-clickjacking heuristics, or prompting the + // user for explicit permission. Reject if some rule is not fulfilled. + + if (nsContentUtils::IsInPrivateBrowsing(this)) { + // If the document is in PB mode, it doesn't have access to its persistent + // cookie jar, so reject the promise here. + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + bool granted = true; + bool isTrackingWindow = false; + if (StaticPrefs::browser_contentblocking_enabled() && + StaticPrefs::network_cookie_cookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER) { + // Only do something special for third-party tracking content. + if (nsContentUtils::StorageDisabledByAntiTracking(this, nullptr)) { + // Note: If this has returned true, the top-level document is guaranteed + // to not be on the Content Blocking allow list. + DebugOnly<bool> isOnAllowList = false; + MOZ_ASSERT_IF(NS_SUCCEEDED(AntiTrackingCommon::IsOnContentBlockingAllowList( + parent->GetDocumentURI(), isOnAllowList)), + !isOnAllowList); + + isTrackingWindow = true; + // TODO: prompt for permission + } + } + + // Step 10. Grant the document access to cookies and store that fact for + // the purposes of future calls to hasStorageAccess() and + // requestStorageAccess(). + if (granted && inner) { + outer->SetHasStorageAccess(true); + if (isTrackingWindow) { + nsCOMPtr<nsIURI> uri = GetDocumentURI(); + if (NS_WARN_IF(!uri)) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; + } + nsAutoString origin; + nsresult rv = nsContentUtils::GetUTFOrigin(uri, origin); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + AntiTrackingCommon::AddFirstPartyStorageAccessGrantedFor(origin, + inner, + AntiTrackingCommon::eStorageAccessAPI) + ->Then(GetCurrentThreadSerialEventTarget(), __func__, + [promise] (bool) { + promise->MaybeResolveWithUndefined(); + }, + [promise] (bool) { + promise->MaybeRejectWithUndefined(); + }); + } else { + promise->MaybeResolveWithUndefined(); + } + } + return promise.forget(); +} + void nsIDocument::RecordNavigationTiming(ReadyState aReadyState) { if (!XRE_IsContentProcess()) { return; } if (!IsTopLevelContentDocument()) { return;
--- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -6269,16 +6269,22 @@ nsGlobalWindowInner::GetTopLevelPrincipa } return topLevelPrincipal; } nsIPrincipal* nsGlobalWindowInner::GetTopLevelStorageAreaPrincipal() { + if (mDoc && ((mDoc->GetSandboxFlags() & SANDBOXED_STORAGE_ACCESS) != 0 || + nsContentUtils::IsInPrivateBrowsing(mDoc))) { + // Storage access is disabled + return nullptr; + } + nsPIDOMWindowOuter* outerWindow = GetParentInternal(); if (!outerWindow) { // No outer window available! return nullptr; } if (!outerWindow->IsTopLevelWindow()) { return nullptr;
--- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -11,16 +11,17 @@ #include "mozilla/MemoryReporting.h" // Local Includes #include "Navigator.h" #include "nsContentSecurityManager.h" #include "nsScreen.h" #include "nsHistory.h" #include "nsDOMNavigationTiming.h" +#include "nsICookieService.h" #include "nsIDOMStorageManager.h" #include "nsISecureBrowserUI.h" #include "nsIWebProgressListener.h" #include "mozilla/AntiTrackingCommon.h" #include "mozilla/dom/ContentFrameMessageManager.h" #include "mozilla/dom/EventTarget.h" #include "mozilla/dom/LocalStorage.h" #include "mozilla/dom/Storage.h" @@ -86,16 +87,17 @@ #include "mozilla/dom/ToJSValue.h" #include "nsJSPrincipals.h" #include "mozilla/Attributes.h" #include "mozilla/Debug.h" #include "mozilla/EventListenerManager.h" #include "mozilla/EventStates.h" #include "mozilla/MouseEvents.h" #include "mozilla/ProcessHangMonitor.h" +#include "mozilla/StaticPrefs.h" #include "mozilla/ThrottledEventQueue.h" #include "AudioChannelService.h" #include "nsAboutProtocolUtils.h" #include "nsCharTraits.h" // NS_IS_HIGH/LOW_SURROGATE #include "PostMessageEvent.h" #include "mozilla/dom/DocGroup.h" #include "mozilla/dom/TabGroup.h" @@ -824,16 +826,17 @@ nsGlobalWindowOuter::nsGlobalWindowOuter mHadOriginalOpener(false), mIsPopupSpam(false), mBlockScriptedClosingFlag(false), mWasOffline(false), mCreatingInnerWindow(false), mIsChrome(false), mAllowScriptsToClose(false), mTopLevelOuterContentWindow(false), + mHasStorageAccess(false), mSerial(0), #ifdef DEBUG mSetOpenerWindowCalled(false), #endif mCleanedUp(false), #ifdef DEBUG mIsValidatingTabGroup(false), #endif @@ -2010,16 +2013,35 @@ nsGlobalWindowOuter::SetNewDocument(nsID PreloadLocalStorage(); // If we have a recorded interesting Large-Allocation header status, report it // to the newly attached document. ReportLargeAllocStatus(); mLargeAllocStatus = LargeAllocStatus::NONE; + mHasStorageAccess = false; + nsIURI* uri = aDocument->GetDocumentURI(); + if (newInnerWindow) { + if (StaticPrefs::browser_contentblocking_enabled() && + StaticPrefs::network_cookie_cookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER && + nsContentUtils::IsThirdPartyWindowOrChannel(newInnerWindow, nullptr, + uri) && + nsContentUtils::IsTrackingResourceWindow(newInnerWindow)) { + // Grant storage access by default if the first-party storage access + // permission has been granted already. + // Don't notify in this case, since we would be notifying the user needlessly. + mHasStorageAccess = + AntiTrackingCommon::IsFirstPartyStorageAccessGrantedFor(newInnerWindow, + uri, + nullptr); + } + } + return NS_OK; } void nsGlobalWindowOuter::PreloadLocalStorage() { if (!Storage::StoragePrefIsEnabled()) { return; @@ -2919,17 +2941,17 @@ nsGlobalWindowOuter::IndexedGetterOuter( return windows->IndexedGetter(aIndex); } nsIControllers* nsGlobalWindowOuter::GetControllersOuter(ErrorResult& aError) { if (!mControllers) { - mControllers = NS_NewXULControllers(); + mControllers = new nsXULControllers(); if (!mControllers) { aError.Throw(NS_ERROR_FAILURE); return nullptr; } // Add in the default controller nsCOMPtr<nsIController> controller = nsBaseCommandController::CreateWindowController();
--- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h @@ -959,16 +959,25 @@ public: bool WindowExists(const nsAString& aName, bool aForceNoOpener, bool aLookForCallerOnJSStack); already_AddRefed<nsIWidget> GetMainWidget(); nsIWidget* GetNearestWidget() const; bool IsInModalState(); + bool HasStorageAccess() const + { + return mHasStorageAccess; + } + void SetHasStorageAccess(bool aHasStorageAccess) + { + mHasStorageAccess = aHasStorageAccess; + } + // Convenience functions for the many methods that need to scale // from device to CSS pixels or vice versa. Note: if a presentation // context is not available, they will assume a 1:1 ratio. int32_t DevToCSSIntPixels(int32_t px); int32_t CSSToDevIntPixels(int32_t px); nsIntSize DevToCSSIntPixels(nsIntSize px); nsIntSize CSSToDevIntPixels(nsIntSize px); @@ -1083,16 +1092,19 @@ protected: bool mIsChrome : 1; // whether scripts may close the window, // even if "dom.allow_scripts_to_close_windows" is false. bool mAllowScriptsToClose : 1; bool mTopLevelOuterContentWindow : 1; + // whether storage access has been granted to this frame. + bool mHasStorageAccess : 1; + nsCOMPtr<nsIScriptContext> mContext; nsWeakPtr mOpener; nsCOMPtr<nsIControllers> mControllers; // For |window.arguments|, via |openDialog|. nsCOMPtr<nsIArray> mArguments; RefPtr<nsDOMWindowList> mFrames;
--- a/dom/base/nsIDocument.h +++ b/dom/base/nsIDocument.h @@ -1233,16 +1233,21 @@ public: /** * Return the root element for this document. */ Element* GetRootElement() const; mozilla::dom::Selection* GetSelection(mozilla::ErrorResult& aRv); + already_AddRefed<mozilla::dom::Promise> + HasStorageAccess(mozilla::ErrorResult& aRv); + already_AddRefed<mozilla::dom::Promise> + RequestStorageAccess(mozilla::ErrorResult& aRv); + /** * Gets the event target to dispatch key events to if there is no focused * content in the document. */ virtual nsIContent* GetUnfocusedKeyEventTarget(); /** * Retrieve information about the viewport as a data structure.
--- a/dom/base/nsJSTimeoutHandler.cpp +++ b/dom/base/nsJSTimeoutHandler.cpp @@ -4,16 +4,17 @@ * 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 <algorithm> #include "mozilla/Attributes.h" #include "mozilla/Likely.h" #include "mozilla/Maybe.h" +#include "mozilla/dom/CSPEvalChecker.h" #include "mozilla/dom/FunctionBinding.h" #include "mozilla/dom/WorkerPrivate.h" #include "nsCOMPtr.h" #include "nsContentUtils.h" #include "nsError.h" #include "nsGlobalWindow.h" #include "nsIContentSecurityPolicy.h" #include "nsIDocument.h" @@ -40,17 +41,18 @@ public: ErrorResult& aError); nsJSScriptTimeoutHandler(JSContext* aCx, nsGlobalWindowInner* aWindow, const nsAString& aExpression, bool* aAllowEval, ErrorResult& aError); nsJSScriptTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate, Function& aFunction, nsTArray<JS::Heap<JS::Value>>&& aArguments); nsJSScriptTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate, - const nsAString& aExpression); + const nsAString& aExpression, bool* aAllowEval, + ErrorResult& aRv); virtual const nsAString& GetHandlerText() override; virtual Function* GetCallback() override { return mFunction; } @@ -158,64 +160,16 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( NS_INTERFACE_MAP_ENTRY(nsIScriptTimeoutHandler) NS_INTERFACE_MAP_ENTRY(nsITimeoutHandler) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsJSScriptTimeoutHandler) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsJSScriptTimeoutHandler) -static bool -CheckCSPForEval(JSContext* aCx, nsGlobalWindowInner* aWindow, - const nsAString& aExpression, ErrorResult& aError) -{ - // if CSP is enabled, and setTimeout/setInterval was called with a string, - // disable the registration and log an error - nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); - if (!doc) { - // if there's no document, we don't have to do anything. - return true; - } - - nsCOMPtr<nsIContentSecurityPolicy> csp; - aError = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); - if (aError.Failed()) { - return false; - } - - if (!csp) { - return true; - } - - bool allowsEval = true; - bool reportViolation = false; - aError = csp->GetAllowsEval(&reportViolation, &allowsEval); - if (aError.Failed()) { - return false; - } - - if (reportViolation) { - // Get the calling location. - uint32_t lineNum = 0; - uint32_t columnNum = 0; - nsAutoString fileNameString; - if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum, - &columnNum)) { - fileNameString.AssignLiteral("unknown"); - } - - csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, - nullptr, // triggering element - fileNameString, aExpression, lineNum, columnNum, - EmptyString(), EmptyString()); - } - - return allowsEval; -} - nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler() : mLineNo(0) , mColumn(0) { } nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx, nsGlobalWindowInner *aWindow, @@ -247,18 +201,19 @@ nsJSScriptTimeoutHandler::nsJSScriptTime { if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { // This window was already closed, or never properly initialized, // don't let a timer be scheduled on such a window. aError.Throw(NS_ERROR_NOT_INITIALIZED); return; } - *aAllowEval = CheckCSPForEval(aCx, aWindow, aExpression, aError); - if (aError.Failed() || !*aAllowEval) { + aError = CSPEvalChecker::CheckForWindow(aCx, aWindow, aExpression, + aAllowEval); + if (NS_WARN_IF(aError.Failed()) || !*aAllowEval) { return; } Init(aCx); } nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate, @@ -271,24 +226,32 @@ nsJSScriptTimeoutHandler::nsJSScriptTime MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); Init(aCx, std::move(aArguments)); } nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate, - const nsAString& aExpression) + const nsAString& aExpression, + bool* aAllowEval, + ErrorResult& aError) : mLineNo(0) , mColumn(0) , mExpr(aExpression) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); + aError = CSPEvalChecker::CheckForWorker(aCx, aWorkerPrivate, aExpression, + aAllowEval); + if (NS_WARN_IF(aError.Failed()) || !*aAllowEval) { + return; + } + Init(aCx); } nsJSScriptTimeoutHandler::~nsJSScriptTimeoutHandler() { ReleaseJSObjects(); } @@ -371,14 +334,20 @@ NS_CreateJSTimeoutHandler(JSContext *aCx RefPtr<nsJSScriptTimeoutHandler> handler = new nsJSScriptTimeoutHandler(aCx, aWorkerPrivate, aFunction, std::move(args)); return handler.forget(); } already_AddRefed<nsIScriptTimeoutHandler> NS_CreateJSTimeoutHandler(JSContext* aCx, WorkerPrivate* aWorkerPrivate, - const nsAString& aExpression) + const nsAString& aExpression, ErrorResult& aRv) { + bool allowEval = false; RefPtr<nsJSScriptTimeoutHandler> handler = - new nsJSScriptTimeoutHandler(aCx, aWorkerPrivate, aExpression); + new nsJSScriptTimeoutHandler(aCx, aWorkerPrivate, aExpression, &allowEval, + aRv); + if (aRv.Failed() || !allowEval) { + return nullptr; + } + return handler.forget(); }
--- a/dom/base/nsNodeInfoManager.cpp +++ b/dom/base/nsNodeInfoManager.cpp @@ -42,17 +42,17 @@ static const uint32_t kInitialNodeInfoHa nsNodeInfoManager::nsNodeInfoManager() : mNodeInfoHash(kInitialNodeInfoHashSize), mDocument(nullptr), mNonDocumentNodeInfos(0), mTextNodeInfo(nullptr), mCommentNodeInfo(nullptr), mDocumentNodeInfo(nullptr), - mRecentlyUsedNodeInfos{}, + mRecentlyUsedNodeInfos(), mSVGEnabled(eTriUnset), mMathMLEnabled(eTriUnset) { nsLayoutStatics::AddRef(); if (gNodeInfoManagerLeakPRLog) MOZ_LOG(gNodeInfoManagerLeakPRLog, LogLevel::Debug, ("NODEINFOMANAGER %p created", this)); @@ -149,20 +149,19 @@ nsNodeInfoManager::GetNodeInfo(nsAtom *a int32_t aNamespaceID, uint16_t aNodeType, nsAtom* aExtraName /* = nullptr */) { CheckValidNodeInfo(aNodeType, aName, aNamespaceID, aExtraName); NodeInfo::NodeInfoInner tmpKey(aName, aPrefix, aNamespaceID, aNodeType, aExtraName); - uint32_t index = tmpKey.Hash() % RECENTLY_USED_NODEINFOS_SIZE; - NodeInfo* ni = mRecentlyUsedNodeInfos[index]; - if (ni && tmpKey == ni->mInner) { - RefPtr<NodeInfo> nodeInfo = ni; + auto p = mRecentlyUsedNodeInfos.Lookup(tmpKey); + if (p) { + RefPtr<NodeInfo> nodeInfo = p.Data(); return nodeInfo.forget(); } // We don't use LookupForAdd here as that would end up storing the temporary // key instead of using `mInner`. RefPtr<NodeInfo> nodeInfo = mNodeInfoHash.Get(&tmpKey); if (!nodeInfo) { ++mNonDocumentNodeInfos; @@ -171,17 +170,17 @@ nsNodeInfoManager::GetNodeInfo(nsAtom *a } nodeInfo = new NodeInfo(aName, aPrefix, aNamespaceID, aNodeType, aExtraName, this); mNodeInfoHash.Put(&nodeInfo->mInner, nodeInfo); } // Have to do the swap thing, because already_AddRefed<nsNodeInfo> // doesn't cast to already_AddRefed<mozilla::dom::NodeInfo> - mRecentlyUsedNodeInfos[index] = nodeInfo; + p.Set(nodeInfo); return nodeInfo.forget(); } nsresult nsNodeInfoManager::GetNodeInfo(const nsAString& aName, nsAtom *aPrefix, int32_t aNamespaceID, uint16_t aNodeType, NodeInfo** aNodeInfo) @@ -191,37 +190,36 @@ nsNodeInfoManager::GetNodeInfo(const nsA { RefPtr<nsAtom> nameAtom = NS_Atomize(aName); CheckValidNodeInfo(aNodeType, nameAtom, aNamespaceID, nullptr); } #endif NodeInfo::NodeInfoInner tmpKey(aName, aPrefix, aNamespaceID, aNodeType); - uint32_t index = tmpKey.Hash() % RECENTLY_USED_NODEINFOS_SIZE; - NodeInfo* ni = mRecentlyUsedNodeInfos[index]; - if (ni && ni->mInner == tmpKey) { - RefPtr<NodeInfo> nodeInfo = ni; + auto p = mRecentlyUsedNodeInfos.Lookup(tmpKey); + if (p) { + RefPtr<NodeInfo> nodeInfo = p.Data(); nodeInfo.forget(aNodeInfo); return NS_OK; } RefPtr<NodeInfo> nodeInfo = mNodeInfoHash.Get(&tmpKey); if (!nodeInfo) { ++mNonDocumentNodeInfos; if (mNonDocumentNodeInfos == 1) { NS_IF_ADDREF(mDocument); } RefPtr<nsAtom> nameAtom = NS_Atomize(aName); nodeInfo = new NodeInfo(nameAtom, aPrefix, aNamespaceID, aNodeType, nullptr, this); mNodeInfoHash.Put(&nodeInfo->mInner, nodeInfo); } - mRecentlyUsedNodeInfos[index] = nodeInfo; + p.Set(nodeInfo); nodeInfo.forget(aNodeInfo); return NS_OK; } nsresult nsNodeInfoManager::GetNodeInfo(const nsAString& aName, nsAtom *aPrefix, @@ -336,21 +334,17 @@ nsNodeInfoManager::RemoveNodeInfo(NodeIn if (aNodeInfo == mTextNodeInfo) { mTextNodeInfo = nullptr; } else if (aNodeInfo == mCommentNodeInfo) { mCommentNodeInfo = nullptr; } } - uint32_t index = aNodeInfo->mInner.Hash() % RECENTLY_USED_NODEINFOS_SIZE; - if (mRecentlyUsedNodeInfos[index] == aNodeInfo) { - mRecentlyUsedNodeInfos[index] = nullptr; - } - + mRecentlyUsedNodeInfos.Remove(aNodeInfo->mInner); DebugOnly<bool> ret = mNodeInfoHash.Remove(&aNodeInfo->mInner); MOZ_ASSERT(ret, "Can't find mozilla::dom::NodeInfo to remove!!!"); } bool nsNodeInfoManager::InternalSVGEnabled() { // If the svg.disabled pref. is true, convert all SVG nodes into
--- a/dom/base/nsNodeInfoManager.h +++ b/dom/base/nsNodeInfoManager.h @@ -8,30 +8,29 @@ * A class for handing out nodeinfos and ensuring sharing of them as needed. */ #ifndef nsNodeInfoManager_h___ #define nsNodeInfoManager_h___ #include "mozilla/Attributes.h" // for final #include "mozilla/dom/NodeInfo.h" +#include "mozilla/MruCache.h" #include "nsCOMPtr.h" // for member #include "nsCycleCollectionParticipant.h" // for NS_DECL_CYCLE_* #include "nsDataHashtable.h" #include "nsStringFwd.h" class nsBindingManager; class nsAtom; class nsIDocument; class nsIPrincipal; class nsWindowSizes; template<class T> struct already_AddRefed; -#define RECENTLY_USED_NODEINFOS_SIZE 31 - class nsNodeInfoManager final { private: ~nsNodeInfoManager(); public: nsNodeInfoManager(); @@ -149,23 +148,40 @@ private: { public: explicit NodeInfoInnerKey(KeyTypePointer aKey) : nsPtrHashKey(aKey) {} ~NodeInfoInnerKey() = default; bool KeyEquals(KeyTypePointer aKey) const { return *mKey == *aKey; } static PLDHashNumber HashKey(KeyTypePointer aKey) { return aKey->Hash(); } }; + struct NodeInfoCache : public mozilla::MruCache< + mozilla::dom::NodeInfo::NodeInfoInner, + mozilla::dom::NodeInfo*, + NodeInfoCache> + { + static mozilla::HashNumber Hash( + const mozilla::dom::NodeInfo::NodeInfoInner& aKey) + { + return aKey.Hash(); + } + static bool Match(const mozilla::dom::NodeInfo::NodeInfoInner& aKey, + const mozilla::dom::NodeInfo* aVal) + { + return aKey == aVal->mInner; + } + }; + nsDataHashtable<NodeInfoInnerKey, mozilla::dom::NodeInfo*> mNodeInfoHash; nsIDocument * MOZ_NON_OWNING_REF mDocument; // WEAK uint32_t mNonDocumentNodeInfos; nsCOMPtr<nsIPrincipal> mPrincipal; // Never null after Init() succeeds. nsCOMPtr<nsIPrincipal> mDefaultPrincipal; // Never null after Init() succeeds mozilla::dom::NodeInfo * MOZ_NON_OWNING_REF mTextNodeInfo; // WEAK to avoid circular ownership mozilla::dom::NodeInfo * MOZ_NON_OWNING_REF mCommentNodeInfo; // WEAK to avoid circular ownership mozilla::dom::NodeInfo * MOZ_NON_OWNING_REF mDocumentNodeInfo; // WEAK to avoid circular ownership RefPtr<nsBindingManager> mBindingManager; - mozilla::dom::NodeInfo* mRecentlyUsedNodeInfos[RECENTLY_USED_NODEINFOS_SIZE]; + NodeInfoCache mRecentlyUsedNodeInfos; Tri mSVGEnabled; Tri mMathMLEnabled; }; #endif /* nsNodeInfoManager_h___ */
--- a/dom/base/nsSandboxFlags.h +++ b/dom/base/nsSandboxFlags.h @@ -107,10 +107,15 @@ const unsigned long SANDBOX_PROPAGATES_T */ const unsigned long SANDBOXED_ORIENTATION_LOCK = 0x2000; /** * This flag disables the Presentation API. */ const unsigned long SANDBOXED_PRESENTATION = 0x4000; -const unsigned long SANDBOX_ALL_FLAGS = 0x7FFF; +/** + * This flag disables access to the first-party storage area by user activation. + */ +const unsigned long SANDBOXED_STORAGE_ACCESS = 0x8000; + +const unsigned long SANDBOX_ALL_FLAGS = 0xFFFF; #endif
--- a/dom/commandhandler/moz.build +++ b/dom/commandhandler/moz.build @@ -22,17 +22,16 @@ XPIDL_SOURCES += [ 'nsIControllerContext.idl', 'nsPICommandUpdater.idl', ] XPIDL_MODULE = 'commandhandler' UNIFIED_SOURCES += [ 'nsBaseCommandController.cpp', - 'nsCommandGroup.cpp', 'nsCommandManager.cpp', 'nsCommandParams.cpp', 'nsControllerCommandTable.cpp', ] LOCAL_INCLUDES += [ '/dom/base', ]
deleted file mode 100644 --- a/dom/commandhandler/nsCommandGroup.cpp +++ /dev/null @@ -1,296 +0,0 @@ -/* -*- 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 "nsString.h" -#include "nsReadableUtils.h" -#include "nsTArray.h" -#include "nsSimpleEnumerator.h" -#include "nsXPCOM.h" -#include "nsSupportsPrimitives.h" -#include "nsIComponentManager.h" -#include "nsCommandGroup.h" -#include "nsIControllerCommand.h" -#include "nsCRT.h" - -class nsGroupsEnumerator : public nsSimpleEnumerator -{ -public: - explicit nsGroupsEnumerator( - nsControllerCommandGroup::GroupsHashtable& aInHashTable); - - NS_DECL_NSISIMPLEENUMERATOR - - const nsID& DefaultInterface() override - { - return NS_GET_IID(nsISupportsCString); - } - -protected: - ~nsGroupsEnumerator() override; - - nsresult Initialize(); - -protected: - nsControllerCommandGroup::GroupsHashtable& mHashTable; - int32_t mIndex; - const char** mGroupNames; // array of pointers to char16_t* in the hash table - bool mInitted; -}; - -nsGroupsEnumerator::nsGroupsEnumerator( - nsControllerCommandGroup::GroupsHashtable& aInHashTable) - : mHashTable(aInHashTable) - , mIndex(-1) - , mGroupNames(nullptr) - , mInitted(false) -{ -} - -nsGroupsEnumerator::~nsGroupsEnumerator() -{ - delete[] mGroupNames; -} - -NS_IMETHODIMP -nsGroupsEnumerator::HasMoreElements(bool* aResult) -{ - nsresult rv = NS_OK; - - NS_ENSURE_ARG_POINTER(aResult); - - if (!mInitted) { - rv = Initialize(); - if (NS_FAILED(rv)) { - return rv; - } - } - - *aResult = (mIndex < static_cast<int32_t>(mHashTable.Count()) - 1); - return NS_OK; -} - -NS_IMETHODIMP -nsGroupsEnumerator::GetNext(nsISupports** aResult) -{ - nsresult rv = NS_OK; - - NS_ENSURE_ARG_POINTER(aResult); - - if (!mInitted) { - rv = Initialize(); - if (NS_FAILED(rv)) { - return rv; - } - } - - mIndex++; - if (mIndex >= static_cast<int32_t>(mHashTable.Count())) { - return NS_ERROR_FAILURE; - } - - const char* thisGroupName = mGroupNames[mIndex]; - - nsCOMPtr<nsISupportsCString> supportsString = - do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv); - if (NS_FAILED(rv)) { - return rv; - } - - supportsString->SetData(nsDependentCString(thisGroupName)); - return CallQueryInterface(supportsString, aResult); -} - -nsresult -nsGroupsEnumerator::Initialize() -{ - if (mInitted) { - return NS_OK; - } - - mGroupNames = new const char*[mHashTable.Count()]; - if (!mGroupNames) { - return NS_ERROR_OUT_OF_MEMORY; - } - - mIndex = 0; - for (auto iter = mHashTable.Iter(); !iter.Done(); iter.Next()) { - mGroupNames[mIndex] = iter.Key().Data(); - mIndex++; - } - - mIndex = -1; - mInitted = true; - return NS_OK; -} - -class nsNamedGroupEnumerator : public nsSimpleEnumerator -{ -public: - explicit nsNamedGroupEnumerator(nsTArray<nsCString>* aInArray); - - NS_DECL_NSISIMPLEENUMERATOR - - const nsID& DefaultInterface() override - { - return NS_GET_IID(nsISupportsCString); - } - -protected: - ~nsNamedGroupEnumerator() override; - - nsTArray<nsCString>* mGroupArray; - int32_t mIndex; -}; - -nsNamedGroupEnumerator::nsNamedGroupEnumerator(nsTArray<nsCString>* aInArray) - : mGroupArray(aInArray) - , mIndex(-1) -{ -} - -nsNamedGroupEnumerator::~nsNamedGroupEnumerator() -{ -} - -NS_IMETHODIMP -nsNamedGroupEnumerator::HasMoreElements(bool* aResult) -{ - NS_ENSURE_ARG_POINTER(aResult); - - int32_t arrayLen = mGroupArray ? mGroupArray->Length() : 0; - *aResult = (mIndex < arrayLen - 1); - return NS_OK; -} - -NS_IMETHODIMP -nsNamedGroupEnumerator::GetNext(nsISupports** aResult) -{ - NS_ENSURE_ARG_POINTER(aResult); - - if (!mGroupArray) { - return NS_ERROR_FAILURE; - } - - mIndex++; - if (mIndex >= int32_t(mGroupArray->Length())) { - return NS_ERROR_FAILURE; - } - - const nsCString& thisGroupName = mGroupArray->ElementAt(mIndex); - - nsresult rv; - nsCOMPtr<nsISupportsCString> supportsString = - do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv); - if (NS_FAILED(rv)) { - return rv; - } - - supportsString->SetData(thisGroupName); - return CallQueryInterface(supportsString, aResult); -} - -NS_IMPL_ISUPPORTS(nsControllerCommandGroup, nsIControllerCommandGroup) - -nsControllerCommandGroup::nsControllerCommandGroup() -{ -} - -nsControllerCommandGroup::~nsControllerCommandGroup() -{ - ClearGroupsHash(); -} - -void -nsControllerCommandGroup::ClearGroupsHash() -{ - mGroupsHash.Clear(); -} - -NS_IMETHODIMP -nsControllerCommandGroup::AddCommandToGroup(const char* aCommand, - const char* aGroup) -{ - nsDependentCString groupKey(aGroup); - auto commandList = mGroupsHash.LookupForAdd(groupKey).OrInsert([]() { - return new AutoTArray<nsCString, 8>(); - }); - -#ifdef DEBUG - nsCString* appended = -#endif - commandList->AppendElement(aCommand); - NS_ASSERTION(appended, "Append failed"); - - return NS_OK; -} - -NS_IMETHODIMP -nsControllerCommandGroup::RemoveCommandFromGroup(const char* aCommand, - const char* aGroup) -{ - nsDependentCString groupKey(aGroup); - nsTArray<nsCString>* commandList = mGroupsHash.Get(groupKey); - if (!commandList) { - return NS_OK; // no group - } - - uint32_t numEntries = commandList->Length(); - for (uint32_t i = 0; i < numEntries; i++) { - nsCString commandString = commandList->ElementAt(i); - if (nsDependentCString(aCommand) != commandString) { - commandList->RemoveElementAt(i); - break; - } - } - return NS_OK; -} - -NS_IMETHODIMP -nsControllerCommandGroup::IsCommandInGroup(const char* aCommand, - const char* aGroup, bool* aResult) -{ - NS_ENSURE_ARG_POINTER(aResult); - *aResult = false; - - nsDependentCString groupKey(aGroup); - nsTArray<nsCString>* commandList = mGroupsHash.Get(groupKey); - if (!commandList) { - return NS_OK; // no group - } - - uint32_t numEntries = commandList->Length(); - for (uint32_t i = 0; i < numEntries; i++) { - nsCString commandString = commandList->ElementAt(i); - if (nsDependentCString(aCommand) != commandString) { - *aResult = true; - break; - } - } - return NS_OK; -} - -NS_IMETHODIMP -nsControllerCommandGroup::GetGroupsEnumerator(nsISimpleEnumerator** aResult) -{ - RefPtr<nsGroupsEnumerator> groupsEnum = new nsGroupsEnumerator(mGroupsHash); - - groupsEnum.forget(aResult); - return NS_OK; -} - -NS_IMETHODIMP -nsControllerCommandGroup::GetEnumeratorForGroup(const char* aGroup, - nsISimpleEnumerator** aResult) -{ - nsDependentCString groupKey(aGroup); - nsTArray<nsCString>* commandList = mGroupsHash.Get(groupKey); // may be null - - RefPtr<nsNamedGroupEnumerator> theGroupEnum = - new nsNamedGroupEnumerator(commandList); - - theGroupEnum.forget(aResult); - return NS_OK; -}
deleted file mode 100644 --- a/dom/commandhandler/nsCommandGroup.h +++ /dev/null @@ -1,44 +0,0 @@ -/* -*- 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 nsCommandGroup_h__ -#define nsCommandGroup_h__ - -#include "nsIController.h" -#include "nsClassHashtable.h" -#include "nsHashKeys.h" - -// {ecd55a01-2780-11d5-a73c-ca641a6813bc} -#define NS_CONTROLLER_COMMAND_GROUP_CID \ - { 0xecd55a01, 0x2780, 0x11d5, { 0xa7, 0x3c, 0xca, 0x64, 0x1a, 0x68, 0x13, 0xbc } } - -#define NS_CONTROLLER_COMMAND_GROUP_CONTRACTID \ - "@mozilla.org/embedcomp/controller-command-group;1" - -class nsControllerCommandGroup : public nsIControllerCommandGroup -{ -public: - nsControllerCommandGroup(); - - NS_DECL_ISUPPORTS - NS_DECL_NSICONTROLLERCOMMANDGROUP - -public: - typedef nsClassHashtable<nsCStringHashKey, nsTArray<nsCString>> - GroupsHashtable; - -protected: - virtual ~nsControllerCommandGroup(); - - void ClearGroupsHash(); - -protected: - // Hash keyed on command group. This could be made more space-efficient, - // maybe with atoms. - GroupsHashtable mGroupsHash; -}; - -#endif // nsCommandGroup_h__
--- a/dom/commandhandler/nsICommandParams.idl +++ b/dom/commandhandler/nsICommandParams.idl @@ -86,14 +86,8 @@ interface nsICommandParams : nsISupports * In order to avoid circular dependency issues, these methods are defined * in nsCommandParams.h. Consumers need to #include that header. */ inline nsCommandParams* AsCommandParams(); inline const nsCommandParams* AsCommandParams() const; %} }; -// {f7fa4581-238e-11d5-a73c-ab64fb68f2bc} -%{C++ -#define NS_COMMAND_PARAMS_CID { 0xf7fa4581, 0x238e, 0x11d5, { 0xa7, 0x3c, 0xab, 0x64, 0xfb, 0x68, 0xf2, 0xbc } } -#define NS_COMMAND_PARAMS_CONTRACTID "@mozilla.org/embedcomp/command-params;1" -%} -
--- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -5792,17 +5792,17 @@ HTMLInputElement::GetFiles(bool aRecursi nsIControllers* HTMLInputElement::GetControllers(ErrorResult& aRv) { //XXX: what about type "file"? if (IsSingleLineTextControl(false)) { if (!mControllers) { - mControllers = NS_NewXULControllers(); + mControllers = new nsXULControllers(); if (!mControllers) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } nsCOMPtr<nsIController> controller = nsBaseCommandController::CreateEditorController(); if (!controller) {
--- a/dom/html/HTMLTextAreaElement.cpp +++ b/dom/html/HTMLTextAreaElement.cpp @@ -618,17 +618,17 @@ HTMLTextAreaElement::IsDoneAddingChildre // Controllers Methods nsIControllers* HTMLTextAreaElement::GetControllers(ErrorResult& aError) { if (!mControllers) { - mControllers = NS_NewXULControllers(); + mControllers = new nsXULControllers(); if (!mControllers) { aError.Throw(NS_ERROR_FAILURE); return nullptr; } nsCOMPtr<nsIController> controller = nsBaseCommandController::CreateEditorController(); if (!controller) {
new file mode 100644 --- /dev/null +++ b/dom/security/CSPEvalChecker.cpp @@ -0,0 +1,185 @@ +/* -*- 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 "mozilla/dom/CSPEvalChecker.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/ErrorResult.h" +#include "nsGlobalWindowInner.h" +#include "nsIDocument.h" +#include "nsCOMPtr.h" +#include "nsJSUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +namespace { + +nsresult +CheckInternal(nsIContentSecurityPolicy* aCSP, + const nsAString& aExpression, + const nsAString& aFileNameString, + uint32_t aLineNum, + uint32_t aColumnNum, + bool* aAllowed) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aAllowed); + + // The value is set at any "return", but better to have a default value here. + *aAllowed = false; + + if (!aCSP) { + *aAllowed = true; + return NS_OK; + } + + bool reportViolation = false; + nsresult rv = aCSP->GetAllowsEval(&reportViolation, aAllowed); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowed = false; + return rv; + } + + if (reportViolation) { + aCSP->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, + nullptr, // triggering element + aFileNameString, aExpression, aLineNum, + aColumnNum, EmptyString(), EmptyString()); + } + + return NS_OK; +} + +class WorkerCSPCheckRunnable final : public WorkerMainThreadRunnable +{ +public: + WorkerCSPCheckRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression, + const nsAString& aFileNameString, + uint32_t aLineNum, + uint32_t aColumnNum) + : WorkerMainThreadRunnable(aWorkerPrivate, + NS_LITERAL_CSTRING("CSP Eval Check")) + , mExpression(aExpression) + , mFileNameString(aFileNameString) + , mLineNum(aLineNum) + , mColumnNum(aColumnNum) + , mEvalAllowed(false) + {} + + bool + MainThreadRun() override + { + mResult = CheckInternal(mWorkerPrivate->GetCSP(), mExpression, + mFileNameString, mLineNum, mColumnNum, + &mEvalAllowed); + return true; + } + + nsresult + GetResult(bool* aAllowed) + { + MOZ_ASSERT(aAllowed); + *aAllowed = mEvalAllowed; + return mResult; + } + +private: + const nsString mExpression; + const nsString mFileNameString; + const uint32_t mLineNum; + const uint32_t mColumnNum; + bool mEvalAllowed; + nsresult mResult; +}; + +} // anonymous + +/* static */ nsresult +CSPEvalChecker::CheckForWindow(JSContext* aCx, nsGlobalWindowInner* aWindow, + const nsAString& aExpression, bool* aAllowEval) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aAllowEval); + + // The value is set at any "return", but better to have a default value here. + *aAllowEval = false; + + // if CSP is enabled, and setTimeout/setInterval was called with a string, + // disable the registration and log an error + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + if (!doc) { + // if there's no document, we don't have to do anything. + *aAllowEval = true; + return NS_OK; + } + + nsCOMPtr<nsIContentSecurityPolicy> csp; + nsresult rv = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowEval = false; + return rv; + } + + // Get the calling location. + uint32_t lineNum = 0; + uint32_t columnNum = 0; + nsAutoString fileNameString; + if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum, + &columnNum)) { + fileNameString.AssignLiteral("unknown"); + } + + rv = CheckInternal(csp, aExpression, fileNameString, lineNum, columnNum, + aAllowEval); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowEval = false; + return rv; + } + + return NS_OK; +} + +/* static */ nsresult +CSPEvalChecker::CheckForWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression, bool* aAllowEval) +{ + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aAllowEval); + + // The value is set at any "return", but better to have a default value here. + *aAllowEval = false; + + // Get the calling location. + uint32_t lineNum = 0; + uint32_t columnNum = 0; + nsAutoString fileNameString; + if (!nsJSUtils::GetCallingLocation(aCx, fileNameString, &lineNum, + &columnNum)) { + fileNameString.AssignLiteral("unknown"); + } + + RefPtr<WorkerCSPCheckRunnable> r = + new WorkerCSPCheckRunnable(aWorkerPrivate, aExpression, fileNameString, + lineNum, columnNum); + ErrorResult error; + r->Dispatch(Canceling, error); + if (NS_WARN_IF(error.Failed())) { + *aAllowEval = false; + return error.StealNSResult(); + } + + nsresult rv = r->GetResult(aAllowEval); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aAllowEval = false; + return rv; + } + + return NS_OK; +}
new file mode 100644 --- /dev/null +++ b/dom/security/CSPEvalChecker.h @@ -0,0 +1,35 @@ +/* -*- 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_CSPEvalChecker_h +#define mozilla_dom_CSPEvalChecker_h + +#include "nsString.h" + +struct JSContext; +class nsGlobalWindowInner; + +namespace mozilla { +namespace dom { + +class WorkerPrivate; + +class CSPEvalChecker final +{ +public: + static nsresult + CheckForWindow(JSContext* aCx, nsGlobalWindowInner* aWindow, + const nsAString& aExpression, bool* aAllowEval); + + static nsresult + CheckForWorker(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression, bool* aAllowEval); +}; + +} // dom namespace +} // mozilla namespace + +#endif // mozilla_dom_CSPEvalChecker_h
--- a/dom/security/moz.build +++ b/dom/security/moz.build @@ -6,16 +6,17 @@ with Files('*'): BUG_COMPONENT = ('Core', 'DOM: Security') TEST_DIRS += ['test'] EXPORTS.mozilla.dom += [ 'ContentVerifier.h', + 'CSPEvalChecker.h', 'FramingChecker.h', 'nsContentSecurityManager.h', 'nsCSPContext.h', 'nsCSPService.h', 'nsCSPUtils.h', 'nsMixedContentBlocker.h', 'PolicyTokenizer.h', 'SRICheck.h', @@ -25,16 +26,17 @@ EXPORTS.mozilla.dom += [ EXPORTS += [ 'nsContentSecurityManager.h', 'nsMixedContentBlocker.h', ] UNIFIED_SOURCES += [ 'ContentVerifier.cpp', + 'CSPEvalChecker.cpp', 'FramingChecker.cpp', 'nsContentSecurityManager.cpp', 'nsCSPContext.cpp', 'nsCSPParser.cpp', 'nsCSPService.cpp', 'nsCSPUtils.cpp', 'nsMixedContentBlocker.cpp', 'PolicyTokenizer.cpp',
--- a/dom/webidl/Document.webidl +++ b/dom/webidl/Document.webidl @@ -478,16 +478,24 @@ partial interface Document { }; // http://w3c.github.io/selection-api/#extensions-to-document-interface partial interface Document { [Throws] Selection? getSelection(); }; +// https://github.com/whatwg/html/issues/3338 +partial interface Document { + [Pref="dom.storage_access.enabled", Throws] + Promise<boolean> hasStorageAccess(); + [Pref="dom.storage_access.enabled", Throws] + Promise<void> requestStorageAccess(); +}; + // Extension to give chrome JS the ability to determine whether // the user has interacted with the document or not. partial interface Document { [ChromeOnly] readonly attribute boolean userHasInteracted; }; // Extension to give chrome JS the ability to simulate activate the docuement // by user gesture.
--- a/dom/workers/ScriptLoader.cpp +++ b/dom/workers/ScriptLoader.cpp @@ -1219,17 +1219,17 @@ private: // importScripts() and the Worker constructor do not support integrity metadata // (or any fetch options). Until then, we can just block. // If we ever have those data in the future, we'll have to the check to // by using the SRICheck module MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, ("Scriptloader::Load, SRI required but not supported in workers")); nsCOMPtr<nsIContentSecurityPolicy> wcsp; chanLoadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(wcsp)); - MOZ_ASSERT(wcsp, "We sould have a CSP for the worker here"); + MOZ_ASSERT(wcsp, "We should have a CSP for the worker here"); if (wcsp) { wcsp->LogViolationDetails( nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT, nullptr, // triggering element aLoadInfo.mURL, EmptyString(), 0, 0, EmptyString(), EmptyString()); } return NS_ERROR_SRI_CORRUPT; }
--- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -3445,16 +3445,20 @@ WorkerPrivate::EnsureClientSource() bool WorkerPrivate::EnsureCSPEventListener() { mCSPEventListener = WorkerCSPEventListener::Create(this); if (NS_WARN_IF(!mCSPEventListener)) { return false; } + if (mLoadInfo.mCSP) { + mLoadInfo.mCSP->SetEventListener(mCSPEventListener); + } + return true; } void WorkerPrivate::EnsurePerformanceStorage() { AssertIsOnWorkerThread();
--- a/dom/workers/WorkerScope.cpp +++ b/dom/workers/WorkerScope.cpp @@ -62,17 +62,18 @@ NS_CreateJSTimeoutHandler(JSContext* aCx mozilla::dom::WorkerPrivate* aWorkerPrivate, mozilla::dom::Function& aFunction, const mozilla::dom::Sequence<JS::Value>& aArguments, mozilla::ErrorResult& aError); extern already_AddRefed<nsIScriptTimeoutHandler> NS_CreateJSTimeoutHandler(JSContext* aCx, mozilla::dom::WorkerPrivate* aWorkerPrivate, - const nsAString& aExpression); + const nsAString& aExpression, + mozilla::ErrorResult& aRv); namespace mozilla { namespace dom { using mozilla::dom::cache::CacheStorage; using mozilla::ipc::PrincipalInfo; WorkerGlobalScope::WorkerGlobalScope(WorkerPrivate* aWorkerPrivate) @@ -271,34 +272,38 @@ WorkerGlobalScope::SetTimeout(JSContext* const int32_t aTimeout, const Sequence<JS::Value>& aArguments, ErrorResult& aRv) { mWorkerPrivate->AssertIsOnWorkerThread(); nsCOMPtr<nsIScriptTimeoutHandler> handler = NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aArguments, aRv); - if (NS_WARN_IF(aRv.Failed())) { + if (!handler) { return 0; } return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, false, aRv); } int32_t WorkerGlobalScope::SetTimeout(JSContext* aCx, const nsAString& aHandler, const int32_t aTimeout, const Sequence<JS::Value>& /* unused */, ErrorResult& aRv) { mWorkerPrivate->AssertIsOnWorkerThread(); nsCOMPtr<nsIScriptTimeoutHandler> handler = - NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler); + NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aRv); + if (!handler) { + return 0; + } + return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, false, aRv); } void WorkerGlobalScope::ClearTimeout(int32_t aHandle) { mWorkerPrivate->AssertIsOnWorkerThread(); mWorkerPrivate->ClearTimeout(aHandle); @@ -329,17 +334,21 @@ WorkerGlobalScope::SetInterval(JSContext const Sequence<JS::Value>& /* unused */, ErrorResult& aRv) { mWorkerPrivate->AssertIsOnWorkerThread(); Sequence<JS::Value> dummy; nsCOMPtr<nsIScriptTimeoutHandler> handler = - NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler); + NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return 0; + } + return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, true, aRv); } void WorkerGlobalScope::ClearInterval(int32_t aHandle) { mWorkerPrivate->AssertIsOnWorkerThread(); mWorkerPrivate->ClearTimeout(aHandle);
--- a/dom/xul/nsIController.idl +++ b/dom/xul/nsIController.idl @@ -31,34 +31,8 @@ interface nsICommandController : nsISupp void getCommandStateWithParams( in string command, in nsICommandParams aCommandParams); void doCommandWithParams(in string command, in nsICommandParams aCommandParams); void getSupportedCommands(out unsigned long count, [array, size_is(count), retval] out string commands); }; - - -/* - An API for registering commands in groups, to allow for - updating via nsIDOMWindow::UpdateCommands. -*/ -interface nsISimpleEnumerator; - -[scriptable, uuid(9F82C404-1C7B-11D5-A73C-ECA43CA836FC)] -interface nsIControllerCommandGroup : nsISupports -{ - - void addCommandToGroup(in string aCommand, in string aGroup); - void removeCommandFromGroup(in string aCommand, in string aGroup); - - boolean isCommandInGroup(in string aCommand, in string aGroup); - - /* - We should expose some methods that allow for enumeration. - */ - nsISimpleEnumerator getGroupsEnumerator(); - - nsISimpleEnumerator getEnumeratorForGroup(in string aGroup); - -}; -
--- a/dom/xul/nsXULControllers.cpp +++ b/dom/xul/nsXULControllers.cpp @@ -37,24 +37,16 @@ nsXULControllers::DeleteControllers() { nsXULControllerData* controllerData = mControllers.ElementAt(i); delete controllerData; // releases the nsIController } mControllers.Clear(); } - -already_AddRefed<nsIControllers> -NS_NewXULControllers() -{ - RefPtr<nsXULControllers> controllers = new nsXULControllers(); - return controllers.forget(); -} - NS_IMPL_CYCLE_COLLECTION_CLASS(nsXULControllers) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXULControllers) tmp->DeleteControllers(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsXULControllers) { uint32_t i, count = tmp->mControllers.Length();
--- a/dom/xul/nsXULControllers.h +++ b/dom/xul/nsXULControllers.h @@ -14,17 +14,17 @@ #include "nsCOMPtr.h" #include "nsTArray.h" #include "nsWeakPtr.h" #include "nsIControllers.h" #include "nsCycleCollectionParticipant.h" /* non-XPCOM class for holding controllers and their IDs */ -class nsXULControllerData +class nsXULControllerData final { public: nsXULControllerData(uint32_t inControllerID, nsIController* inController) : mControllerID(inControllerID) , mController(inController) { } @@ -37,30 +37,26 @@ public: NS_IF_ADDREF(*outController = mController); return NS_OK; } uint32_t mControllerID; nsCOMPtr<nsIController> mController; }; - -already_AddRefed<nsIControllers> NS_NewXULControllers(); - -class nsXULControllers : public nsIControllers +class nsXULControllers final : public nsIControllers { public: - friend already_AddRefed<nsIControllers> NS_NewXULControllers(); + nsXULControllers(); NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsXULControllers, nsIControllers) NS_DECL_NSICONTROLLERS protected: - nsXULControllers(); virtual ~nsXULControllers(void); void DeleteControllers(); nsTArray<nsXULControllerData*> mControllers; uint32_t mCurControllerID; };
--- a/dom/xul/nsXULElement.cpp +++ b/dom/xul/nsXULElement.cpp @@ -1133,17 +1133,17 @@ nsXULElement::IsAttributeMapped(const ns } nsIControllers* nsXULElement::GetControllers(ErrorResult& rv) { if (! Controllers()) { nsExtendedDOMSlots* slots = ExtendedDOMSlots(); - slots->mControllers = NS_NewXULControllers(); + slots->mControllers = new nsXULControllers(); } return Controllers(); } already_AddRefed<BoxObject> nsXULElement::GetBoxObject(ErrorResult& rv) {
--- a/editor/composer/ComposerCommandsUpdater.cpp +++ b/editor/composer/ComposerCommandsUpdater.cpp @@ -276,18 +276,16 @@ ComposerCommandsUpdater::UpdateDirtyStat nsresult ComposerCommandsUpdater::UpdateCommandGroup(const nsAString& aCommandGroup) { nsCOMPtr<nsPICommandUpdater> commandUpdater = GetCommandUpdater(); NS_ENSURE_TRUE(commandUpdater, NS_ERROR_FAILURE); - // This hardcoded list of commands is temporary. - // This code should use nsIControllerCommandGroup. if (aCommandGroup.EqualsLiteral("undo")) { commandUpdater->CommandStatusChanged("cmd_undo"); commandUpdater->CommandStatusChanged("cmd_redo"); return NS_OK; } if (aCommandGroup.EqualsLiteral("select") || aCommandGroup.EqualsLiteral("style")) {
--- a/gfx/layers/PaintThread.cpp +++ b/gfx/layers/PaintThread.cpp @@ -192,35 +192,35 @@ PaintThread::UpdateRenderMode() mPaintWorkers = nullptr; } else { InitPaintWorkers(); } } } void -PaintThread::QueuePaintTask(PaintTask* aTask) +PaintThread::QueuePaintTask(UniquePtr<PaintTask>&& aTask) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aTask); if (gfxPrefs::LayersOMTPDumpCapture() && aTask->mCapture) { aTask->mCapture->Dump(); } + MOZ_RELEASE_ASSERT(aTask->mCapture->hasOneRef()); + RefPtr<CompositorBridgeChild> cbc(CompositorBridgeChild::Get()); - RefPtr<PaintTask> state(aTask); - - cbc->NotifyBeginAsyncPaint(state); + cbc->NotifyBeginAsyncPaint(aTask.get()); RefPtr<PaintThread> self = this; RefPtr<Runnable> task = NS_NewRunnableFunction("PaintThread::AsyncPaintTask", - [self, cbc, state]() -> void + [self, cbc, task = std::move(aTask)]() -> void { - self->AsyncPaintTask(cbc, state); + self->AsyncPaintTask(cbc, task.get()); }); nsIEventTarget* paintThread = mPaintWorkers ? static_cast<nsIEventTarget*>(mPaintWorkers.get()) : static_cast<nsIEventTarget*>(sThread.get()); #ifndef OMTP_FORCE_SYNC paintThread->Dispatch(task.forget());
--- a/gfx/layers/PaintThread.h +++ b/gfx/layers/PaintThread.h @@ -29,28 +29,25 @@ namespace layers { // on the paint thread or paint worker pool. // // More specifically it contains: // 1. A capture command list of drawing commands // 2. A destination draw target to replay the draw commands upon // 3. A list of dependent texture clients that must be kept alive for the // task's duration, and then destroyed on the main thread class PaintTask { - NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PaintTask) public: PaintTask() {} + ~PaintTask() {} void DropTextureClients(); RefPtr<gfx::DrawTarget> mTarget; RefPtr<gfx::DrawTargetCapture> mCapture; AutoTArray<RefPtr<TextureClient>, 4> mClients; - -protected: - virtual ~PaintTask() {} }; class CompositorBridgeChild; class PaintThread final { friend void DestroyPaintThread(UniquePtr<PaintThread>&& aPaintThread); @@ -68,17 +65,17 @@ public: // This allows the paint thread to dynamically toggle between a paint worker // thread pool used with tiling, and a single paint thread used with rotated // buffer. void UpdateRenderMode(); // Must be called on the main thread. Queues an async paint // task to be completed on the paint thread. - void QueuePaintTask(PaintTask* aTask); + void QueuePaintTask(UniquePtr<PaintTask>&& aTask); // Must be called on the main thread. Signifies that the current // layer tree transaction has been finished and any async paints // for it have been queued on the paint thread. This MUST be called // at the end of a layer transaction as it will be used to do an optional // texture sync and then unblock the main thread if it is waiting to paint // a new frame. void QueueEndLayerTransaction(SyncObjectClient* aSyncObject);
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp +++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp @@ -288,18 +288,18 @@ APZCCallbackHelper::UpdateRootFrame(Fram return; } MOZ_ASSERT(aMetrics.GetUseDisplayPortMargins()); if (gfxPrefs::APZAllowZooming() && aMetrics.GetScrollOffsetUpdated()) { // If zooming is disabled then we don't really want to let APZ fiddle // with these things. In theory setting the resolution here should be a - // no-op, but setting the SPCSPS is bad because it can cause a stale value - // to be returned by window.innerWidth/innerHeight (see bug 1187792). + // no-op, but setting the visual viewport size is bad because it can cause a + // stale value to be returned by window.innerWidth/innerHeight (see bug 1187792). // // We also skip this codepath unless the metrics has a scroll offset update // type other eNone, because eNone just means that this repaint request // was triggered by APZ in response to a main-thread update. In this // scenario we don't want to update the main-thread resolution because // it can trigger unnecessary reflows. float presShellResolution = shell->GetResolution();
--- a/gfx/layers/client/ClientPaintedLayer.cpp +++ b/gfx/layers/client/ClientPaintedLayer.cpp @@ -91,17 +91,17 @@ ClientPaintedLayer::UpdatePaintRegion(Pa return true; } void ClientPaintedLayer::FinishPaintState(PaintState& aState) { if (aState.mAsyncTask && !aState.mAsyncTask->mCapture->IsEmpty()) { ClientManager()->SetQueuedAsyncPaints(); - PaintThread::Get()->QueuePaintTask(aState.mAsyncTask); + PaintThread::Get()->QueuePaintTask(std::move(aState.mAsyncTask)); } } uint32_t ClientPaintedLayer::GetPaintFlags(ReadbackProcessor* aReadback) { uint32_t flags = ContentClient::PAINT_CAN_DRAW_ROTATED; #ifndef MOZ_IGNORE_PAINT_WILL_RESAMPLE
--- a/gfx/layers/client/ContentClient.cpp +++ b/gfx/layers/client/ContentClient.cpp @@ -158,17 +158,17 @@ ContentClient::BeginPaint(PaintedLayer* OpenMode readMode = result.mAsyncPaint ? OpenMode::OPEN_READ_ASYNC : OpenMode::OPEN_READ; OpenMode writeMode = result.mAsyncPaint ? OpenMode::OPEN_READ_WRITE_ASYNC : OpenMode::OPEN_READ_WRITE; IntRect drawBounds = result.mRegionToDraw.GetBounds(); if (result.mAsyncPaint) { - result.mAsyncTask = new PaintTask(); + result.mAsyncTask.reset(new PaintTask()); } // Try to acquire the back buffer, copy over contents if we are using a new buffer, // and rotate or unrotate the buffer as necessary if (mBuffer && dest.mCanReuseBuffer) { if (mBuffer->Lock(writeMode)) { auto newParameters = mBuffer->AdjustedParameters(dest.mBufferRect);
--- a/gfx/layers/client/ContentClient.h +++ b/gfx/layers/client/ContentClient.h @@ -20,16 +20,17 @@ #include "mozilla/layers/CompositorTypes.h" // for TextureInfo, etc #include "mozilla/layers/ISurfaceAllocator.h" #include "mozilla/layers/LayersSurfaces.h" // for SurfaceDescriptor #include "mozilla/layers/LayersTypes.h" // for TextureDumpMode #include "mozilla/layers/TextureClient.h" // for TextureClient #include "mozilla/layers/PaintThread.h" // for PaintTask #include "mozilla/Maybe.h" // for Maybe #include "mozilla/mozalloc.h" // for operator delete +#include "mozilla/UniquePtr.h" // for UniquePtr #include "ReadbackProcessor.h" // For ReadbackProcessor::Update #include "nsCOMPtr.h" // for already_AddRefed #include "nsPoint.h" // for nsIntPoint #include "nsRect.h" // for mozilla::gfx::IntRect #include "nsRegion.h" // for nsIntRegion #include "nsTArray.h" // for nsTArray namespace mozilla { @@ -112,17 +113,17 @@ public: {} nsIntRegion mRegionToDraw; nsIntRegion mRegionToInvalidate; SurfaceMode mMode; DrawRegionClip mClip; gfxContentType mContentType; bool mAsyncPaint; - RefPtr<PaintTask> mAsyncTask; + UniquePtr<PaintTask> mAsyncTask; }; enum { PAINT_WILL_RESAMPLE = 0x01, PAINT_NO_ROTATION = 0x02, PAINT_CAN_DRAW_ROTATED = 0x04, PAINT_ASYNC = 0x08, };
--- a/gfx/layers/client/MultiTiledContentClient.cpp +++ b/gfx/layers/client/MultiTiledContentClient.cpp @@ -147,17 +147,17 @@ void ClientMultiTiledLayerBuffer::MaybeS AutoTArray<uint64_t, 10> syncTextureSerials; SurfaceMode mode; Unused << GetContentType(&mode); // Pre-pass through the tiles (mirroring the filter logic below) to gather // texture IDs that we need to ensure are unused by the GPU before we // continue. if (!aPaintRegion.IsEmpty()) { - MOZ_ASSERT(mPaintTasks.size() == 0); + MOZ_ASSERT(mPaintTasks.IsEmpty()); for (size_t i = 0; i < mRetainedTiles.Length(); ++i) { const TileCoordIntPoint tileCoord = aNewTiles.TileCoord(i); IntPoint tileOffset = GetTileOffset(tileCoord); nsIntRegion tileDrawRegion = IntRect(tileOffset, aScaledTileSize); tileDrawRegion.AndWith(aPaintRegion); if (tileDrawRegion.IsEmpty()) { @@ -215,17 +215,17 @@ void ClientMultiTiledLayerBuffer::Update oldRetainedTiles.Clear(); nsIntRegion paintRegion = aPaintRegion; nsIntRegion dirtyRegion = aDirtyRegion; MaybeSyncTextures(paintRegion, newTiles, scaledTileSize); if (!paintRegion.IsEmpty()) { - MOZ_ASSERT(mPaintTasks.size() == 0); + MOZ_ASSERT(mPaintTasks.IsEmpty()); for (size_t i = 0; i < newTileCount; ++i) { const TileCoordIntPoint tileCoord = newTiles.TileCoord(i); IntPoint tileOffset = GetTileOffset(tileCoord); nsIntRegion tileDrawRegion = IntRect(tileOffset, scaledTileSize); tileDrawRegion.AndWith(paintRegion); @@ -238,24 +238,24 @@ void ClientMultiTiledLayerBuffer::Update gfxCriticalError() << "ValidateTile failed"; } // Validating the tile may have required more to be painted. paintRegion.OrWith(tileDrawRegion); dirtyRegion.OrWith(tileDrawRegion); } - if (!mPaintTiles.empty()) { + if (!mPaintTiles.IsEmpty()) { // Create a tiled draw target gfx::TileSet tileset; - for (size_t i = 0; i < mPaintTiles.size(); ++i) { + for (size_t i = 0; i < mPaintTiles.Length(); ++i) { mPaintTiles[i].mTileOrigin -= mTilingOrigin; } - tileset.mTiles = &mPaintTiles[0]; - tileset.mTileCount = mPaintTiles.size(); + tileset.mTiles = mPaintTiles.Elements(); + tileset.mTileCount = mPaintTiles.Length(); RefPtr<DrawTarget> drawTarget = gfx::Factory::CreateTiledDrawTarget(tileset); if (!drawTarget || !drawTarget->IsValid()) { gfxDevCrash(LogReason::InvalidContext) << "Invalid tiled draw target"; return; } drawTarget->SetTransform(Matrix()); // Draw into the tiled draw target @@ -269,37 +269,38 @@ void ClientMultiTiledLayerBuffer::Update ctx = nullptr; // Edge padding allows us to avoid resampling artifacts if (gfxPrefs::TileEdgePaddingEnabled() && mResolution == 1) { drawTarget->PadEdges(newValidRegion.MovedBy(-mTilingOrigin)); } // Reset - mPaintTiles.clear(); + mPaintTiles.Clear(); mTilingOrigin = IntPoint(std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::max()); } // Dispatch to the paint thread if (aFlags & TilePaintFlags::Async) { bool queuedTask = false; - for (const auto& state : mPaintTasks) { - if (!state->mCapture->IsEmpty()) { - PaintThread::Get()->QueuePaintTask(state); + while (!mPaintTasks.IsEmpty()) { + UniquePtr<PaintTask> task = mPaintTasks.PopLastElement(); + if (!task->mCapture->IsEmpty()) { + PaintThread::Get()->QueuePaintTask(std::move(task)); queuedTask = true; } } if (queuedTask) { mManager->SetQueuedAsyncPaints(); } - mPaintTasks.clear(); + mPaintTasks.Clear(); } for (uint32_t i = 0; i < mRetainedTiles.Length(); ++i) { TileClient& tile = mRetainedTiles[i]; UnlockTile(tile); } } @@ -381,24 +382,24 @@ ClientMultiTiledLayerBuffer::ValidateTil iter.Get().Width(), iter.Get().Height()); backBuffer->mTarget->ClearRect(drawRect); } } gfx::Tile paintTile; paintTile.mTileOrigin = gfx::IntPoint(aTileOrigin.x, aTileOrigin.y); paintTile.mDrawTarget = backBuffer->mTarget; - mPaintTiles.push_back(paintTile); + mPaintTiles.AppendElement(paintTile); if (aFlags & TilePaintFlags::Async) { - RefPtr<PaintTask> task = new PaintTask(); + UniquePtr<PaintTask> task(new PaintTask()); task->mCapture = backBuffer->mCapture; task->mTarget = backBuffer->mBackBuffer; task->mClients = std::move(backBuffer->mTextureClients); - mPaintTasks.push_back(task); + mPaintTasks.AppendElement(std::move(task)); } else { MOZ_RELEASE_ASSERT(backBuffer->mTarget == backBuffer->mBackBuffer); MOZ_RELEASE_ASSERT(backBuffer->mCapture == nullptr); } mTilingOrigin.x = std::min(mTilingOrigin.x, paintTile.mTileOrigin.x); mTilingOrigin.y = std::min(mTilingOrigin.y, paintTile.mTileOrigin.y);
--- a/gfx/layers/client/MultiTiledContentClient.h +++ b/gfx/layers/client/MultiTiledContentClient.h @@ -9,16 +9,17 @@ #include "ClientLayerManager.h" // for ClientLayerManager #include "nsRegion.h" // for nsIntRegion #include "mozilla/gfx/2D.h" // for gfx::Tile #include "mozilla/gfx/Point.h" // for IntPoint #include "mozilla/layers/CompositableClient.h" // for CompositableClient #include "mozilla/layers/LayersMessages.h" // for TileDescriptor #include "mozilla/layers/TiledContentClient.h" // for ClientTiledPaintedLayer +#include "mozilla/UniquePtr.h" // for UniquePtr #include "TiledLayerBuffer.h" // for TiledLayerBuffer namespace mozilla { namespace layers { class ClientLayerManager; class ClientMultiTiledLayerBuffer @@ -116,18 +117,18 @@ private: // The region that will be made valid during Update(). Once Update() is // completed then this is identical to mValidRegion. nsIntRegion mNewValidRegion; SharedFrameMetricsHelper* mSharedFrameMetricsHelper; // Parameters that are collected during Update for a paint before they // are either executed or replayed on the paint thread. - std::vector<gfx::Tile> mPaintTiles; - std::vector<RefPtr<PaintTask>> mPaintTasks; + AutoTArray<gfx::Tile, 4> mPaintTiles; + AutoTArray<UniquePtr<PaintTask>, 4> mPaintTasks; /** * While we're adding tiles, this is used to keep track of the position of * the top-left of the top-left-most tile. When we come to wrap the tiles in * TiledDrawTarget we subtract the value of this member from each tile's * offset so that all the tiles have a positive offset, then add a * translation to the TiledDrawTarget to compensate. This is important so * that the mRect of the TiledDrawTarget is always at a positive x/y
--- a/gfx/layers/client/SingleTiledContentClient.cpp +++ b/gfx/layers/client/SingleTiledContentClient.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/layers/SingleTiledContentClient.h" #include "ClientTiledPaintedLayer.h" #include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" namespace mozilla { namespace layers { SingleTiledContentClient::SingleTiledContentClient(ClientTiledPaintedLayer& aPaintedLayer, ClientLayerManager* aManager) : TiledContentClient(aManager, "Single") @@ -231,21 +232,27 @@ ClientSingleTiledLayerBuffer::PaintThebe } ctx->SetMatrix(ctx->CurrentMatrix().PreTranslate(-mTilingOrigin.x, -mTilingOrigin.y)); aCallback(&mPaintedLayer, ctx, paintRegion, paintRegion, DrawRegionClip::DRAW, nsIntRegion(), aCallbackData); } if (asyncPaint) { if (!backBuffer->mCapture->IsEmpty()) { - RefPtr<PaintTask> task = new PaintTask(); + UniquePtr<PaintTask> task(new PaintTask()); task->mCapture = backBuffer->mCapture; task->mTarget = backBuffer->mBackBuffer; task->mClients = std::move(backBuffer->mTextureClients); - PaintThread::Get()->QueuePaintTask(task); + + // The target is an alias for the capture, and the paint thread expects + // to be the only one with a reference to the capture + backBuffer->mTarget = nullptr; + backBuffer->mCapture = nullptr; + + PaintThread::Get()->QueuePaintTask(std::move(task)); mManager->SetQueuedAsyncPaints(); } } else { MOZ_ASSERT(backBuffer->mTarget == backBuffer->mBackBuffer); MOZ_ASSERT(!backBuffer->mCapture); } // The new buffer is now validated, remove the dirty region from it.
--- a/gfx/thebes/gfxPrefs.h +++ b/gfx/thebes/gfxPrefs.h @@ -538,16 +538,17 @@ private: #if defined(XP_MACOSX) DECL_GFX_PREF(Live, "gl.multithreaded", GLMultithreaded, bool, false); #endif DECL_GFX_PREF(Live, "gl.require-hardware", RequireHardwareGL, bool, false); DECL_GFX_PREF(Live, "gl.use-tls-is-current", UseTLSIsCurrent, int32_t, 0); DECL_GFX_PREF(Live, "image.animated.decode-on-demand.threshold-kb", ImageAnimatedDecodeOnDemandThresholdKB, uint32_t, 20480); DECL_GFX_PREF(Live, "image.animated.decode-on-demand.batch-size", ImageAnimatedDecodeOnDemandBatchSize, uint32_t, 6); + DECL_GFX_PREF(Live, "image.animated.generate-full-frames", ImageAnimatedGenerateFullFrames, bool, false); DECL_GFX_PREF(Live, "image.animated.resume-from-last-displayed", ImageAnimatedResumeFromLastDisplayed, bool, false); DECL_GFX_PREF(Live, "image.cache.factor2.threshold-surfaces", ImageCacheFactor2ThresholdSurfaces, int32_t, -1); DECL_GFX_PREF(Once, "image.cache.size", ImageCacheSize, int32_t, 5*1024*1024); DECL_GFX_PREF(Once, "image.cache.timeweight", ImageCacheTimeWeight, int32_t, 500); DECL_GFX_PREF(Live, "image.decode-immediately.enabled", ImageDecodeImmediatelyEnabled, bool, false); DECL_GFX_PREF(Live, "image.downscale-during-decode.enabled", ImageDownscaleDuringDecodeEnabled, bool, true); DECL_GFX_PREF(Live, "image.infer-src-animation.threshold-ms", ImageInferSrcAnimationThresholdMS, uint32_t, 2000); DECL_GFX_PREF(Live, "image.layout_network_priority", ImageLayoutNetworkPriority, bool, true);
--- a/image/AnimationSurfaceProvider.cpp +++ b/image/AnimationSurfaceProvider.cpp @@ -27,20 +27,20 @@ AnimationSurfaceProvider::AnimationSurfa , mDecoder(aDecoder.get()) , mFramesMutex("AnimationSurfaceProvider::mFrames") { MOZ_ASSERT(!mDecoder->IsMetadataDecode(), "Use MetadataDecodingTask for metadata decodes"); MOZ_ASSERT(!mDecoder->IsFirstFrameDecode(), "Use DecodedSurfaceProvider for single-frame image decodes"); - // We still produce paletted surfaces for GIF which means the frames are - // smaller than one would expect for APNG. This may be removed if/when - // bug 1337111 lands and it is enabled by default. - size_t pixelSize = aDecoder->GetType() == DecoderType::GIF + // We may produce paletted surfaces for GIF which means the frames are smaller + // than one would expect. + size_t pixelSize = !aDecoder->ShouldBlendAnimation() && + aDecoder->GetType() == DecoderType::GIF ? sizeof(uint8_t) : sizeof(uint32_t); // Calculate how many frames we need to decode in this animation before we // enter decode-on-demand mode. IntSize frameSize = aSurfaceKey.Size(); size_t threshold = (size_t(gfxPrefs::ImageAnimatedDecodeOnDemandThresholdKB()) * 1024) / (pixelSize * frameSize.width * frameSize.height);
--- a/image/Decoder.cpp +++ b/image/Decoder.cpp @@ -286,17 +286,17 @@ nsresult Decoder::AllocateFrame(const gfx::IntSize& aOutputSize, const gfx::IntRect& aFrameRect, gfx::SurfaceFormat aFormat, uint8_t aPaletteDepth, const Maybe<AnimationParams>& aAnimParams) { mCurrentFrame = AllocateFrameInternal(aOutputSize, aFrameRect, aFormat, aPaletteDepth, aAnimParams, - mCurrentFrame.get()); + std::move(mCurrentFrame)); if (mCurrentFrame) { mHasFrameToTake = true; // Gather the raw pointers the decoders will use. mCurrentFrame->GetImageData(&mImageData, &mImageDataLength); mCurrentFrame->GetPaletteData(&mColormap, &mColormapSize); @@ -316,17 +316,17 @@ Decoder::AllocateFrame(const gfx::IntSiz } RawAccessFrameRef Decoder::AllocateFrameInternal(const gfx::IntSize& aOutputSize, const gfx::IntRect& aFrameRect, SurfaceFormat aFormat, uint8_t aPaletteDepth, const Maybe<AnimationParams>& aAnimParams, - imgFrame* aPreviousFrame) + RawAccessFrameRef&& aPreviousFrame) { if (HasError()) { return RawAccessFrameRef(); } uint32_t frameNum = aAnimParams ? aAnimParams->mFrameNum : 0; if (frameNum != mFrameCount) { MOZ_ASSERT_UNREACHABLE("Allocating frames out of order"); @@ -338,17 +338,17 @@ Decoder::AllocateFrameInternal(const gfx NS_WARNING("Trying to add frame with zero or negative size"); return RawAccessFrameRef(); } auto frame = MakeNotNull<RefPtr<imgFrame>>(); bool nonPremult = bool(mSurfaceFlags & SurfaceFlags::NO_PREMULTIPLY_ALPHA); if (NS_FAILED(frame->InitForDecoder(aOutputSize, aFrameRect, aFormat, aPaletteDepth, nonPremult, - aAnimParams))) { + aAnimParams, ShouldBlendAnimation()))) { NS_WARNING("imgFrame::Init should succeed"); return RawAccessFrameRef(); } RawAccessFrameRef ref = frame->RawAccessRef(); if (!ref) { frame->Abort(); return RawAccessFrameRef(); @@ -369,17 +369,37 @@ Decoder::AllocateFrameInternal(const gfx } } if (frameNum > 0) { ref->SetRawAccessOnly(); // Some GIFs are huge but only have a small area that they animate. We only // need to refresh that small area when frame 0 comes around again. - mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, frame->GetRect()); + mFirstFrameRefreshArea.UnionRect(mFirstFrameRefreshArea, + ref->GetBoundedBlendRect()); + + if (ShouldBlendAnimation()) { + if (aPreviousFrame->GetDisposalMethod() != + DisposalMethod::RESTORE_PREVIOUS) { + // If the new restore frame is the direct previous frame, then we know + // the dirty rect is composed only of the current frame's blend rect and + // the restore frame's clear rect (if applicable) which are handled in + // filters. + mRestoreFrame = std::move(aPreviousFrame); + mRestoreDirtyRect.SetBox(0, 0, 0, 0); + } else { + // We only need the previous frame's dirty rect, because while there may + // have been several frames between us and mRestoreFrame, the only areas + // that changed are the restore frame's clear rect, the current frame + // blending rect, and the previous frame's blending rect. All else is + // forgotten due to us restoring the same frame again. + mRestoreDirtyRect = aPreviousFrame->GetBoundedBlendRect(); + } + } } mFrameCount++; return ref; } /*
--- a/image/Decoder.h +++ b/image/Decoder.h @@ -264,16 +264,25 @@ public: * Should we stop decoding after the first frame? */ bool IsFirstFrameDecode() const { return bool(mDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY); } /** + * Should blend the current frame with the previous frames to produce a + * complete frame instead of a partial frame for animated images. + */ + bool ShouldBlendAnimation() const + { + return bool(mDecoderFlags & DecoderFlags::BLEND_ANIMATION); + } + + /** * @return the number of complete animation frames which have been decoded so * far, if it has changed since the last call to TakeCompleteFrameCount(); * otherwise, returns Nothing(). */ Maybe<uint32_t> TakeCompleteFrameCount(); // The number of frames we have, including anything in-progress. Thus, this // is only 0 if we haven't begun any frames. @@ -405,24 +414,51 @@ public: RasterImage* GetImageMaybeNull() const { return mImage.get(); } RawAccessFrameRef GetCurrentFrameRef() { return mCurrentFrame ? mCurrentFrame->RawAccessRef() : RawAccessFrameRef(); } + /** + * For use during decoding only. Allows the BlendAnimationFilter to get the + * current frame we are producing for its animation parameters. + */ + imgFrame* GetCurrentFrame() + { + MOZ_ASSERT(ShouldBlendAnimation()); + return mCurrentFrame.get(); + } + + /** + * For use during decoding only. Allows the BlendAnimationFilter to get the + * frame it should be pulling the previous frame data from. + */ + const RawAccessFrameRef& GetRestoreFrameRef() const + { + MOZ_ASSERT(ShouldBlendAnimation()); + return mRestoreFrame; + } + + const gfx::IntRect& GetRestoreDirtyRect() const + { + MOZ_ASSERT(ShouldBlendAnimation()); + return mRestoreDirtyRect; + } + bool HasFrameToTake() const { return mHasFrameToTake; } void ClearHasFrameToTake() { MOZ_ASSERT(mHasFrameToTake); mHasFrameToTake = false; } protected: friend class AutoRecordDecoderTelemetry; + friend class DecoderTestHelper; friend class nsICODecoder; friend class PalettedSurfaceSink; friend class SurfaceSink; virtual ~Decoder(); /* * Internal hooks. Decoder implementations may override these and @@ -539,32 +575,42 @@ private: return mInFrame ? mFrameCount - 1 : mFrameCount; } RawAccessFrameRef AllocateFrameInternal(const gfx::IntSize& aOutputSize, const gfx::IntRect& aFrameRect, gfx::SurfaceFormat aFormat, uint8_t aPaletteDepth, const Maybe<AnimationParams>& aAnimParams, - imgFrame* aPreviousFrame); + RawAccessFrameRef&& aPreviousFrame); protected: Maybe<Downscaler> mDownscaler; uint8_t* mImageData; // Pointer to image data in either Cairo or 8bit format uint32_t mImageDataLength; uint32_t* mColormap; // Current colormap to be used in Cairo format uint32_t mColormapSize; private: RefPtr<RasterImage> mImage; Maybe<SourceBufferIterator> mIterator; + + // The current frame the decoder is producing. RawAccessFrameRef mCurrentFrame; + + // The complete frame to combine with the current partial frame to produce + // a complete current frame. + RawAccessFrameRef mRestoreFrame; + ImageMetadata mImageMetadata; - gfx::IntRect mInvalidRect; // Tracks an invalidation region in the current frame. + + gfx::IntRect mInvalidRect; // Tracks new rows as the current frame is decoded. + gfx::IntRect mRestoreDirtyRect; // Tracks an invalidation region between the + // restore frame and the previous frame. Maybe<gfx::IntSize> mOutputSize; // The size of our output surface. Maybe<gfx::IntSize> mExpectedSize; // The expected size of the image. Progress mProgress; uint32_t mFrameCount; // Number of frames, including anything in-progress FrameTimeout mLoopLength; // Length of a single loop of this image. gfx::IntRect mFirstFrameRefreshArea; // The area of the image that needs to // be invalidated when the animation loops.
--- a/image/DecoderFactory.cpp +++ b/image/DecoderFactory.cpp @@ -327,16 +327,17 @@ DecoderFactory::CreateDecoderForICOResou return decoder.forget(); } /* static */ already_AddRefed<Decoder> DecoderFactory::CreateAnonymousDecoder(DecoderType aType, NotNull<SourceBuffer*> aSourceBuffer, const Maybe<IntSize>& aOutputSize, + DecoderFlags aDecoderFlags, SurfaceFlags aSurfaceFlags) { if (aType == DecoderType::UNKNOWN) { return nullptr; } RefPtr<Decoder> decoder = GetDecoder(aType, /* aImage = */ nullptr, /* aIsRedecode = */ false); @@ -345,24 +346,17 @@ DecoderFactory::CreateAnonymousDecoder(D // Initialize the decoder. decoder->SetMetadataDecode(false); decoder->SetIterator(aSourceBuffer->Iterator()); // Anonymous decoders are always transient; we don't want to optimize surfaces // or do any other expensive work that might be wasted. DecoderFlags decoderFlags = DecoderFlags::IMAGE_IS_TRANSIENT; - // Without an image, the decoder can't store anything in the SurfaceCache, so - // callers will only be able to retrieve the most recent frame via - // Decoder::GetCurrentFrame(). That means that anonymous decoders should - // always be first-frame-only decoders, because nobody ever wants the *last* - // frame. - decoderFlags |= DecoderFlags::FIRST_FRAME_ONLY; - - decoder->SetDecoderFlags(decoderFlags); + decoder->SetDecoderFlags(aDecoderFlags | decoderFlags); decoder->SetSurfaceFlags(aSurfaceFlags); // Set an output size for downscale-during-decode if requested. if (aOutputSize) { decoder->SetOutputSize(*aOutputSize); } if (NS_FAILED(decoder->Init())) {
--- a/image/DecoderFactory.h +++ b/image/DecoderFactory.h @@ -172,23 +172,25 @@ public: * * @param aType Which type of decoder to create - JPEG, PNG, etc. * @param aSourceBuffer The SourceBuffer which the decoder will read its data * from. * @param aOutputSize If Some(), the output size for the decoder. If this is * smaller than the intrinsic size, the decoder will * downscale the image. If Nothing(), the output size will * be the intrinsic size. + * @param aDecoderFlags Flags specifying the behavior of this decoder. * @param aSurfaceFlags Flags specifying the type of output this decoder * should produce. */ static already_AddRefed<Decoder> CreateAnonymousDecoder(DecoderType aType, NotNull<SourceBuffer*> aSourceBuffer, const Maybe<gfx::IntSize>& aOutputSize, + DecoderFlags aDecoderFlags, SurfaceFlags aSurfaceFlags); /** * Creates and initializes an anonymous metadata decoder (one which isn't * associated with an Image object). This decoder will only decode the image's * header, extracting metadata like the size of the image. No actual image * data will be decoded and no surfaces will be allocated. *
--- a/image/DecoderFlags.h +++ b/image/DecoderFlags.h @@ -26,17 +26,25 @@ enum class DecoderFlags : uint8_t ASYNC_NOTIFY = 1 << 3, /** * By default, a surface is considered substitutable. That means callers are * willing to accept a less than ideal match to display. If a caller requires * a specific size and won't accept alternatives, then this flag should be * set. */ - CANNOT_SUBSTITUTE = 1 << 4 + CANNOT_SUBSTITUTE = 1 << 4, + + /** + * By default, an animation decoder will produce partial frames that need to + * be combined with the previously displayed/composited frame by FrameAnimator + * to produce a complete frame. If this flag is set, the decoder will perform + * this blending at decode time, and the frames produced are complete. + */ + BLEND_ANIMATION = 1 << 5 }; MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(DecoderFlags) /** * @return the default set of decode flags. */ inline DecoderFlags DefaultDecoderFlags()
--- a/image/FrameAnimator.cpp +++ b/image/FrameAnimator.cpp @@ -311,36 +311,38 @@ FrameAnimator::AdvanceFrame(AnimationSta return ret; } if (nextFrame->GetTimeout() == FrameTimeout::Forever()) { ret.mAnimationFinished = true; } if (nextFrameIndex == 0) { + MOZ_ASSERT(nextFrame->IsFullFrame()); ret.mDirtyRect = aState.FirstFrameRefreshArea(); - } else { + } else if (!nextFrame->IsFullFrame()) { MOZ_ASSERT(nextFrameIndex == currentFrameIndex + 1); - // Change frame if (!DoBlend(aCurrentFrame, nextFrame, nextFrameIndex, &ret.mDirtyRect)) { // something went wrong, move on to next NS_WARNING("FrameAnimator::AdvanceFrame(): Compositing of frame failed"); nextFrame->SetCompositingFailed(true); aState.mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime(aState, aCurrentFrame->GetTimeout()); aState.mCurrentAnimationFrameIndex = nextFrameIndex; aState.mCompositedFrameRequested = false; aCurrentFrame = std::move(nextFrame); aFrames.Advance(nextFrameIndex); return ret; } nextFrame->SetCompositingFailed(false); + } else { + ret.mDirtyRect = nextFrame->GetDirtyRect(); } aState.mCurrentAnimationFrameTime = GetCurrentImgFrameEndTime(aState, aCurrentFrame->GetTimeout()); // If we can get closer to the current time by a multiple of the image's loop // time, we should. We can only do this if we're done decoding; otherwise, we // don't know the full loop length, and LoopLength() will have to return @@ -478,18 +480,23 @@ FrameAnimator::RequestRefresh(AnimationS // If we didn't advance a frame, and our frame end time didn't change, // then we need to break out of this loop & wait for the frame(s) // to finish downloading. if (!frameRes.mFrameAdvanced && currentFrameEndTime == oldFrameEndTime) { break; } } - // Advanced to the correct frame, the composited frame is now valid to be drawn. - if (currentFrameEndTime > aTime) { + // We should only mark the composited frame as valid and reset the dirty rect + // if we advanced (meaning the next frame was actually produced somehow), the + // composited frame was previously invalid (so we may need to repaint + // everything) and the frame index is valid (to know we were doing blending + // on the main thread, instead of on the decoder threads in advance). + if (currentFrameEndTime > aTime && aState.mCompositedFrameInvalid && + mLastCompositedFrameIndex >= 0) { aState.mCompositedFrameInvalid = false; ret.mDirtyRect = IntRect(IntPoint(0,0), mSize); } MOZ_ASSERT(!aState.mIsCurrentlyDecoded || !aState.mCompositedFrameInvalid); return ret; }
--- a/image/ImageOps.cpp +++ b/image/ImageOps.cpp @@ -220,17 +220,19 @@ ImageOps::DecodeToSurface(ImageBuffer* a } // Create a decoder. DecoderType decoderType = DecoderFactory::GetDecoderType(PromiseFlatCString(aMimeType).get()); RefPtr<Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(decoderType, WrapNotNull(sourceBuffer), - aSize, ToSurfaceFlags(aFlags)); + aSize, + DecoderFlags::FIRST_FRAME_ONLY, + ToSurfaceFlags(aFlags)); if (!decoder) { return nullptr; } // Run the decoder synchronously. RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder)); task->Run(); if (!decoder->GetDecodeDone() || decoder->HasError()) {
--- a/image/RasterImage.cpp +++ b/image/RasterImage.cpp @@ -1281,16 +1281,20 @@ RasterImage::Decode(const IntSize& aSize surfaceFlags &= ~SurfaceFlags::NO_PREMULTIPLY_ALPHA; } // Create a decoder. RefPtr<IDecodingTask> task; nsresult rv; bool animated = mAnimationState && aPlaybackType == PlaybackType::eAnimated; if (animated) { + if (gfxPrefs::ImageAnimatedGenerateFullFrames()) { + decoderFlags |= DecoderFlags::BLEND_ANIMATION; + } + size_t currentFrame = mAnimationState->GetCurrentAnimationFrameIndex(); rv = DecoderFactory::CreateAnimationDecoder(mDecoderType, WrapNotNull(this), mSourceBuffer, mSize, decoderFlags, surfaceFlags, currentFrame, getter_AddRefs(task)); } else { rv = DecoderFactory::CreateDecoder(mDecoderType, WrapNotNull(this),
--- a/image/SurfaceFilters.h +++ b/image/SurfaceFilters.h @@ -15,16 +15,17 @@ #include <algorithm> #include <stdint.h> #include <string.h> #include "mozilla/Likely.h" #include "mozilla/Maybe.h" #include "mozilla/UniquePtr.h" #include "mozilla/gfx/2D.h" +#include "skia/src/core/SkBlitRow.h" #include "DownscalingFilter.h" #include "SurfaceCache.h" #include "SurfacePipe.h" namespace mozilla { namespace image { @@ -323,16 +324,389 @@ private: UniquePtr<uint8_t[]> mBuffer; /// The buffer used to store reordered rows. int32_t mInputRow; /// The current row we're reading. (0-indexed) int32_t mOutputRow; /// The current row we're writing. (0-indexed) uint8_t mPass; /// Which pass we're on. (0-indexed) bool mProgressiveDisplay; /// If true, duplicate rows to optimize for /// progressive display. }; +////////////////////////////////////////////////////////////////////////////// +// BlendAnimationFilter +////////////////////////////////////////////////////////////////////////////// + +template <typename Next> class BlendAnimationFilter; + +/** + * A configuration struct for BlendAnimationFilter. + */ +struct BlendAnimationConfig +{ + template <typename Next> using Filter = BlendAnimationFilter<Next>; + Decoder* mDecoder; /// The decoder producing the animation. +}; + +/** + * BlendAnimationFilter turns a partial image as part of an animation into a + * complete frame given its frame rect, blend method, and the base frame's + * data buffer, frame rect and disposal method. Any excess data caused by a + * frame rect not being contained by the output size will be discarded. + * + * The base frame is an already produced complete frame from the animation. + * It may be any previous frame depending on the disposal method, although + * most often it will be the immediate previous frame to the current we are + * generating. + * + * The 'Next' template parameter specifies the next filter in the chain. + */ +template <typename Next> +class BlendAnimationFilter final : public SurfaceFilter +{ +public: + BlendAnimationFilter() + : mRow(0) + , mRowLength(0) + , mOverProc(nullptr) + , mBaseFrameStartPtr(nullptr) + , mBaseFrameRowPtr(nullptr) + { } + + template <typename... Rest> + nsresult Configure(const BlendAnimationConfig& aConfig, const Rest&... aRest) + { + nsresult rv = mNext.Configure(aRest...); + if (NS_FAILED(rv)) { + return rv; + } + + if (!aConfig.mDecoder || !aConfig.mDecoder->ShouldBlendAnimation()) { + MOZ_ASSERT_UNREACHABLE("Expected image decoder that is blending!"); + return NS_ERROR_INVALID_ARG; + } + + imgFrame* currentFrame = aConfig.mDecoder->GetCurrentFrame(); + if (!currentFrame) { + MOZ_ASSERT_UNREACHABLE("Decoder must have current frame!"); + return NS_ERROR_FAILURE; + } + + mFrameRect = mUnclampedFrameRect = currentFrame->GetBlendRect(); + gfx::IntSize outputSize = mNext.InputSize(); + mRowLength = outputSize.width * sizeof(uint32_t); + + // Forbid frame rects with negative size. + if (mUnclampedFrameRect.width < 0 || mUnclampedFrameRect.height < 0) { + return NS_ERROR_FAILURE; + } + + // Clamp mFrameRect to the output size. + gfx::IntRect outputRect(0, 0, outputSize.width, outputSize.height); + mFrameRect = mFrameRect.Intersect(outputRect); + bool fullFrame = outputRect.IsEqualEdges(mFrameRect); + + // If there's no intersection, |mFrameRect| will be an empty rect positioned + // at the maximum of |inputRect|'s and |aFrameRect|'s coordinates, which is + // not what we want. Force it to (0, 0) in that case. + if (mFrameRect.IsEmpty()) { + mFrameRect.MoveTo(0, 0); + } + + BlendMethod blendMethod = currentFrame->GetBlendMethod(); + switch (blendMethod) { + default: + blendMethod = BlendMethod::SOURCE; + MOZ_FALLTHROUGH_ASSERT("Unexpected blend method!"); + case BlendMethod::SOURCE: + // Default, overwrites base frame data (if any) with new. + break; + case BlendMethod::OVER: + // OVER only has an impact on the output if we have new data to blend + // with. + if (mFrameRect.IsEmpty()) { + blendMethod = BlendMethod::SOURCE; + } + break; + } + + // Determine what we need to clear and what we need to copy. If this frame + // is a full frame and uses source blending, there is no need to consider + // the disposal method of the previous frame. + gfx::IntRect dirtyRect(outputRect); + if (!fullFrame || blendMethod != BlendMethod::SOURCE) { + const RawAccessFrameRef& restoreFrame = + aConfig.mDecoder->GetRestoreFrameRef(); + if (restoreFrame) { + MOZ_ASSERT(restoreFrame->GetImageSize() == outputSize); + MOZ_ASSERT(restoreFrame->IsFinished()); + + // We can safely use this pointer without holding a RawAccessFrameRef + // because the decoder will keep it alive for us. + mBaseFrameStartPtr = restoreFrame.Data(); + MOZ_ASSERT(mBaseFrameStartPtr); + + gfx::IntRect restoreBlendRect = restoreFrame->GetBoundedBlendRect(); + gfx::IntRect restoreDirtyRect = aConfig.mDecoder->GetRestoreDirtyRect(); + switch (restoreFrame->GetDisposalMethod()) { + default: + case DisposalMethod::RESTORE_PREVIOUS: + MOZ_FALLTHROUGH_ASSERT("Unexpected DisposalMethod"); + case DisposalMethod::NOT_SPECIFIED: + case DisposalMethod::KEEP: + dirtyRect = mFrameRect.Union(restoreDirtyRect); + break; + case DisposalMethod::CLEAR: + // We only need to clear if the rect is outside the frame rect (i.e. + // overwrites a non-overlapping area) or the blend method may cause + // us to combine old data and new. + if (!mFrameRect.Contains(restoreBlendRect) || + blendMethod == BlendMethod::OVER) { + mClearRect = restoreBlendRect; + } + + // If we are clearing the whole frame, we do not need to retain a + // reference to the base frame buffer. + if (outputRect.IsEqualEdges(mClearRect)) { + mBaseFrameStartPtr = nullptr; + } else { + dirtyRect = mFrameRect.Union(restoreDirtyRect).Union(mClearRect); + } + break; + } + } else if (!fullFrame) { + // This must be the first frame, clear everything. + mClearRect = outputRect; + } + } + + // The dirty rect, or delta between the current frame and the previous frame + // (chronologically, not necessarily the restore frame) is the last + // animation parameter we need to initialize the new frame with. + currentFrame->SetDirtyRect(dirtyRect); + + if (!mBaseFrameStartPtr) { + // Switch to SOURCE if no base frame to ensure we don't allocate an + // intermediate buffer below. OVER does nothing without the base frame + // data. + blendMethod = BlendMethod::SOURCE; + } + + // Skia provides arch-specific accelerated methods to perform blending. + // Note that this is an internal Skia API and may be prone to change, + // but we avoid the overhead of setting up Skia objects. + if (blendMethod == BlendMethod::OVER) { + mOverProc = SkBlitRow::Factory32(SkBlitRow::kSrcPixelAlpha_Flag32); + MOZ_ASSERT(mOverProc); + } + + // We don't need an intermediate buffer unless the unclamped frame rect + // width is larger than the clamped frame rect width. In that case, the + // caller will end up writing data that won't end up in the final image at + // all, and we'll need a buffer to give that data a place to go. + if (mFrameRect.width < mUnclampedFrameRect.width || mOverProc) { + mBuffer.reset(new (fallible) uint8_t[mUnclampedFrameRect.width * + sizeof(uint32_t)]); + if (MOZ_UNLIKELY(!mBuffer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memset(mBuffer.get(), 0, mUnclampedFrameRect.width * sizeof(uint32_t)); + } + + ConfigureFilter(mUnclampedFrameRect.Size(), sizeof(uint32_t)); + return NS_OK; + } + + Maybe<SurfaceInvalidRect> TakeInvalidRect() override + { + return mNext.TakeInvalidRect(); + } + +protected: + uint8_t* DoResetToFirstRow() override + { + uint8_t* rowPtr = mNext.ResetToFirstRow(); + if (rowPtr == nullptr) { + mRow = mFrameRect.YMost(); + return nullptr; + } + + mRow = 0; + mBaseFrameRowPtr = mBaseFrameStartPtr; + + while (mRow < mFrameRect.y) { + WriteBaseFrameRow(); + AdvanceRowOutsideFrameRect(); + } + + // We're at the beginning of the frame rect now, so return if we're either + // ready for input or we're already done. + rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer(); + if (!mFrameRect.IsEmpty() || rowPtr == nullptr) { + // Note that the pointer we're returning is for the next row we're + // actually going to write to, but we may discard writes before that point + // if mRow < mFrameRect.y. + mRow = mUnclampedFrameRect.y; + WriteBaseFrameRow(); + return AdjustRowPointer(rowPtr); + } + + // We've finished the region specified by the frame rect, but the frame rect + // is empty, so we need to output the rest of the image immediately. Advance + // to the end of the next pipeline stage's buffer, outputting rows that are + // copied from the base frame and/or cleared. + WriteBaseFrameRowsUntilComplete(); + + mRow = mFrameRect.YMost(); + return nullptr; // We're done. + } + + uint8_t* DoAdvanceRow() override + { + uint8_t* rowPtr = nullptr; + + const int32_t currentRow = mRow; + mRow++; + + // The unclamped frame rect has a negative offset which means -y rows from + // the decoder need to be discarded before we advance properly. + if (currentRow >= 0 && mBaseFrameRowPtr) { + mBaseFrameRowPtr += mRowLength; + } + + if (currentRow < mFrameRect.y) { + // This row is outside of the frame rect, so just drop it on the floor. + rowPtr = mBuffer ? mBuffer.get() : mNext.CurrentRowPointer(); + return AdjustRowPointer(rowPtr); + } else if (NS_WARN_IF(currentRow >= mFrameRect.YMost())) { + return nullptr; + } + + // If we had to buffer, merge the data into the row. Otherwise we had the + // decoder write directly to the next stage's buffer. + if (mBuffer) { + int32_t width = mFrameRect.width; + uint32_t* dst = reinterpret_cast<uint32_t*>(mNext.CurrentRowPointer()); + uint32_t* src = reinterpret_cast<uint32_t*>(mBuffer.get()) - + std::min(mUnclampedFrameRect.x, 0); + dst += mFrameRect.x; + if (mOverProc) { + mOverProc(dst, src, width, 0xFF); + } else { + memcpy(dst, src, width * sizeof(uint32_t)); + } + rowPtr = mNext.AdvanceRow() ? mBuffer.get() : nullptr; + } else { + MOZ_ASSERT(!mOverProc); + rowPtr = mNext.AdvanceRow(); + } + + // If there's still more data coming or we're already done, just adjust the + // pointer and return. + if (mRow < mFrameRect.YMost() || rowPtr == nullptr) { + WriteBaseFrameRow(); + return AdjustRowPointer(rowPtr); + } + + // We've finished the region specified by the frame rect. Advance to the end + // of the next pipeline stage's buffer, outputting rows that are copied from + // the base frame and/or cleared. + WriteBaseFrameRowsUntilComplete(); + + return nullptr; // We're done. + } + +private: + void WriteBaseFrameRowsUntilComplete() + { + do { + WriteBaseFrameRow(); + } while (AdvanceRowOutsideFrameRect()); + } + + void WriteBaseFrameRow() + { + uint8_t* dest = mNext.CurrentRowPointer(); + if (!dest) { + return; + } + + if (!mBaseFrameRowPtr) { + // No base frame, so we are clearing everything. + memset(dest, 0, mRowLength); + } else if (mClearRect.height > 0 && + mClearRect.y <= mRow && + mClearRect.YMost() > mRow) { + // We have a base frame, but we are inside the area to be cleared. + // Only copy the data we need from the source. + size_t prefixLength = mClearRect.x * sizeof(uint32_t); + size_t clearLength = mClearRect.width * sizeof(uint32_t); + size_t postfixOffset = prefixLength + clearLength; + size_t postfixLength = mRowLength - postfixOffset; + MOZ_ASSERT(prefixLength + clearLength + postfixLength == mRowLength); + memcpy(dest, mBaseFrameRowPtr, prefixLength); + memset(dest + prefixLength, 0, clearLength); + memcpy(dest + postfixOffset, mBaseFrameRowPtr + postfixOffset, postfixLength); + } else { + memcpy(dest, mBaseFrameRowPtr, mRowLength); + } + } + + bool AdvanceRowOutsideFrameRect() + { + // The unclamped frame rect may have a negative offset however we should + // never be advancing the row via this path (otherwise mBaseFrameRowPtr + // will be wrong. + MOZ_ASSERT(mRow >= 0); + MOZ_ASSERT(mRow < mFrameRect.y || mRow >= mFrameRect.YMost()); + + mRow++; + if (mBaseFrameRowPtr) { + mBaseFrameRowPtr += mRowLength; + } + + return mNext.AdvanceRow() != nullptr; + } + + uint8_t* AdjustRowPointer(uint8_t* aNextRowPointer) const + { + if (mBuffer) { + MOZ_ASSERT(aNextRowPointer == mBuffer.get() || aNextRowPointer == nullptr); + return aNextRowPointer; // No adjustment needed for an intermediate buffer. + } + + if (mFrameRect.IsEmpty() || + mRow >= mFrameRect.YMost() || + aNextRowPointer == nullptr) { + return nullptr; // Nothing left to write. + } + + MOZ_ASSERT(!mOverProc); + return aNextRowPointer + mFrameRect.x * sizeof(uint32_t); + } + + Next mNext; /// The next SurfaceFilter in the chain. + + gfx::IntRect mFrameRect; /// The surface subrect which contains data, + /// clamped to the image size. + gfx::IntRect mUnclampedFrameRect; /// The frame rect before clamping. + UniquePtr<uint8_t[]> mBuffer; /// The intermediate buffer, if one is + /// necessary because the frame rect width + /// is larger than the image's logical width. + int32_t mRow; /// The row in unclamped frame rect space + /// that we're currently writing. + size_t mRowLength; /// Length in bytes of a row that is the input + /// for the next filter. + SkBlitRow::Proc32 mOverProc; /// Function pointer to perform over blending. + const uint8_t* mBaseFrameStartPtr; /// Starting row pointer to the base frame + /// data from which we copy pixel data from. + const uint8_t* mBaseFrameRowPtr; /// Current row pointer to the base frame + /// data. + gfx::IntRect mClearRect; /// The frame area to clear before blending + /// the current frame. +}; ////////////////////////////////////////////////////////////////////////////// // RemoveFrameRectFilter ////////////////////////////////////////////////////////////////////////////// template <typename Next> class RemoveFrameRectFilter; /**
--- a/image/SurfacePipeFactory.h +++ b/image/SurfacePipeFactory.h @@ -49,21 +49,25 @@ enum class SurfacePipeFlags DEINTERLACE = 1 << 0, // If set, deinterlace the image. ADAM7_INTERPOLATE = 1 << 1, // If set, the caller is deinterlacing the // image using ADAM7, and we may want to // interpolate it for better intermediate results. FLIP_VERTICALLY = 1 << 2, // If set, flip the image vertically. - PROGRESSIVE_DISPLAY = 1 << 3 // If set, we expect the image to be displayed + PROGRESSIVE_DISPLAY = 1 << 3, // If set, we expect the image to be displayed // progressively. This enables features that // result in a better user experience for // progressive display but which may be more // computationally expensive. + + BLEND_ANIMATION = 1 << 4 // If set, produce the next full frame of an + // animation instead of a partial frame to be + // blended later. }; MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SurfacePipeFlags) class SurfacePipeFactory { public: /** * Creates and initializes a normal (i.e., non-paletted) SurfacePipe. @@ -96,45 +100,50 @@ public: SurfacePipeFlags aFlags) { const bool deinterlace = bool(aFlags & SurfacePipeFlags::DEINTERLACE); const bool flipVertically = bool(aFlags & SurfacePipeFlags::FLIP_VERTICALLY); const bool progressiveDisplay = bool(aFlags & SurfacePipeFlags::PROGRESSIVE_DISPLAY); const bool downscale = aInputSize != aOutputSize; const bool removeFrameRect = !aFrameRect.IsEqualEdges(nsIntRect(0, 0, aInputSize.width, aInputSize.height)); + const bool blendAnimation = bool(aFlags & SurfacePipeFlags::BLEND_ANIMATION); // Don't interpolate if we're sure we won't show this surface to the user // until it's completely decoded. The final pass of an ADAM7 image doesn't // need interpolation, so we only need to interpolate if we'll be displaying // the image while it's still being decoded. const bool adam7Interpolate = bool(aFlags & SurfacePipeFlags::ADAM7_INTERPOLATE) && progressiveDisplay; if (deinterlace && adam7Interpolate) { MOZ_ASSERT_UNREACHABLE("ADAM7 deinterlacing is handled by libpng"); return Nothing(); } + MOZ_ASSERT_IF(blendAnimation, aAnimParams); + // Construct configurations for the SurfaceFilters. Note that the order of // these filters is significant. We want to deinterlace or interpolate raw // input rows, before any other transformations, and we want to remove the // frame rect (which may involve adding blank rows or columns to the image) // before any downscaling, so that the new rows and columns are taken into // account. DeinterlacingConfig<uint32_t> deinterlacingConfig { progressiveDisplay }; ADAM7InterpolatingConfig interpolatingConfig; RemoveFrameRectConfig removeFrameRectConfig { aFrameRect }; + BlendAnimationConfig blendAnimationConfig { aDecoder }; DownscalingConfig downscalingConfig { aInputSize, aFormat }; SurfaceConfig surfaceConfig { aDecoder, aOutputSize, aFormat, flipVertically, aAnimParams }; Maybe<SurfacePipe> pipe; if (downscale) { + MOZ_ASSERT(!blendAnimation); if (removeFrameRect) { if (deinterlace) { pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig, downscalingConfig, surfaceConfig); } else if (adam7Interpolate) { pipe = MakePipe(interpolatingConfig, removeFrameRectConfig, downscalingConfig, surfaceConfig); } else { // (deinterlace and adam7Interpolate are false) @@ -145,25 +154,33 @@ public: pipe = MakePipe(deinterlacingConfig, downscalingConfig, surfaceConfig); } else if (adam7Interpolate) { pipe = MakePipe(interpolatingConfig, downscalingConfig, surfaceConfig); } else { // (deinterlace and adam7Interpolate are false) pipe = MakePipe(downscalingConfig, surfaceConfig); } } } else { // (downscale is false) - if (removeFrameRect) { + if (blendAnimation) { + if (deinterlace) { + pipe = MakePipe(deinterlacingConfig, blendAnimationConfig, surfaceConfig); + } else if (adam7Interpolate) { + pipe = MakePipe(interpolatingConfig, blendAnimationConfig, surfaceConfig); + } else { // (deinterlace and adam7Interpolate are false) + pipe = MakePipe(blendAnimationConfig, surfaceConfig); + } + } else if (removeFrameRect) { if (deinterlace) { pipe = MakePipe(deinterlacingConfig, removeFrameRectConfig, surfaceConfig); } else if (adam7Interpolate) { pipe = MakePipe(interpolatingConfig, removeFrameRectConfig, surfaceConfig); } else { // (deinterlace and adam7Interpolate are false) pipe = MakePipe(removeFrameRectConfig, surfaceConfig); } - } else { // (removeFrameRect is false) + } else { // (blendAnimation and removeFrameRect is false) if (deinterlace) { pipe = MakePipe(deinterlacingConfig, surfaceConfig); } else if (adam7Interpolate) { pipe = MakePipe(interpolatingConfig, surfaceConfig); } else { // (deinterlace and adam7Interpolate are false) pipe = MakePipe(surfaceConfig); } }
--- a/image/decoders/GIF2.h +++ b/image/decoders/GIF2.h @@ -32,16 +32,18 @@ typedef struct gif_struct { // Output state machine int64_t pixels_remaining; // Pixels remaining to be output. // Parameters for image frame currently being decoded int tpixel; // Index of transparent pixel int32_t disposal_method; // Restore to background, leave in place, etc. uint32_t* local_colormap; // Per-image colormap + uint32_t local_colormap_buffer_size; // Size of the buffer containing the + // local colormap. int local_colormap_size; // Size of local colormap array. uint32_t delay_time; // Display time, in milliseconds, // for this image in a multi-image GIF // Global (multi-image) state int version; // Either 89 for GIF89 or 87 for GIF87 int32_t screen_width; // Logical screen width & height int32_t screen_height;
--- a/image/decoders/nsGIFDecoder2.cpp +++ b/image/decoders/nsGIFDecoder2.cpp @@ -174,40 +174,50 @@ nsGIFDecoder2::CheckForTransparency(cons nsresult nsGIFDecoder2::BeginImageFrame(const IntRect& aFrameRect, uint16_t aDepth, bool aIsInterlaced) { MOZ_ASSERT(HasSize()); bool hasTransparency = CheckForTransparency(aFrameRect); + bool blendAnimation = ShouldBlendAnimation(); // Make sure there's no animation if we're downscaling. MOZ_ASSERT_IF(Size() != OutputSize(), !GetImageMetadata().HasAnimation()); AnimationParams animParams { aFrameRect, FrameTimeout::FromRawMilliseconds(mGIFStruct.delay_time), uint32_t(mGIFStruct.images_decoded), BlendMethod::OVER, DisposalMethod(mGIFStruct.disposal_method) }; SurfacePipeFlags pipeFlags = aIsInterlaced ? SurfacePipeFlags::DEINTERLACE : SurfacePipeFlags(); - Maybe<SurfacePipe> pipe; + gfx::SurfaceFormat format; if (mGIFStruct.images_decoded == 0) { - gfx::SurfaceFormat format = hasTransparency ? SurfaceFormat::B8G8R8A8 - : SurfaceFormat::B8G8R8X8; - // The first frame may be displayed progressively. pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; + format = hasTransparency ? SurfaceFormat::B8G8R8A8 + : SurfaceFormat::B8G8R8X8; + } else { + format = SurfaceFormat::B8G8R8A8; + } + + if (blendAnimation) { + pipeFlags |= SurfacePipeFlags::BLEND_ANIMATION; + } + + Maybe<SurfacePipe> pipe; + if (mGIFStruct.images_decoded == 0 || blendAnimation) { // The first frame is always decoded into an RGB surface. pipe = SurfacePipeFactory::CreateSurfacePipe(this, Size(), OutputSize(), aFrameRect, format, Some(animParams), pipeFlags); } else { // This is an animation frame (and not the first). To minimize the memory // usage of animations, the image data is stored in paletted form. @@ -215,18 +225,18 @@ nsGIFDecoder2::BeginImageFrame(const Int // We should never use paletted surfaces with a draw target directly, so // the only practical difference between B8G8R8A8 and B8G8R8X8 is the // cleared pixel value if we get truncated. We want 0 in that case to // ensure it is an acceptable value for the color map as was the case // historically. MOZ_ASSERT(Size() == OutputSize()); pipe = SurfacePipeFactory::CreatePalettedSurfacePipe(this, Size(), aFrameRect, - SurfaceFormat::B8G8R8A8, - aDepth, Some(animParams), + format, aDepth, + Some(animParams), pipeFlags); } mCurrentFrameIndex = mGIFStruct.images_decoded; if (!pipe) { mPipe = SurfacePipe(); return NS_ERROR_FAILURE; @@ -903,28 +913,40 @@ nsGIFDecoder2::FinishImageDescriptor(con mGIFStruct.pixels_remaining = int64_t(frameRect.Width()) * int64_t(frameRect.Height()); if (haveLocalColorTable) { // We have a local color table, so prepare to read it into the palette of // the current frame. mGIFStruct.local_colormap_size = 1 << depth; - if (mGIFStruct.images_decoded == 0) { - // The first frame has a local color table. Allocate space for it as we - // use a BGRA or BGRX surface for the first frame; such surfaces don't - // have their own palettes internally. + if (!mColormap) { + // Allocate a buffer to store the local color tables. This could be if the + // first frame has a local color table, or for subsequent frames when + // blending the animation during decoding. + MOZ_ASSERT(mGIFStruct.images_decoded == 0 || ShouldBlendAnimation()); + + // Ensure our current colormap buffer is large enough to hold the new one. mColormapSize = sizeof(uint32_t) << realDepth; - if (!mGIFStruct.local_colormap) { + if (mGIFStruct.local_colormap_buffer_size < mColormapSize) { + if (mGIFStruct.local_colormap) { + free(mGIFStruct.local_colormap); + } + mGIFStruct.local_colormap_buffer_size = mColormapSize; mGIFStruct.local_colormap = static_cast<uint32_t*>(moz_xmalloc(mColormapSize)); + } else { + mColormapSize = mGIFStruct.local_colormap_buffer_size; } + mColormap = mGIFStruct.local_colormap; } + MOZ_ASSERT(mColormap); + const size_t size = 3 << depth; if (mColormapSize > size) { // Clear the part of the colormap which will be unused with this palette. // If a GIF references an invalid palette entry, ensure the entry is opaque white. // This is needed for Skia as if it isn't, RGBX surfaces will cause blending issues // with Skia. memset(reinterpret_cast<uint8_t*>(mColormap) + size, 0xFF, mColormapSize - size); @@ -936,17 +958,17 @@ nsGIFDecoder2::FinishImageDescriptor(con // large and it'd be preferable to avoid unnecessary copies. return Transition::ToUnbuffered(State::FINISHED_LOCAL_COLOR_TABLE, State::LOCAL_COLOR_TABLE, size); } // There's no local color table; copy the global color table into the palette // of the current frame. - if (mGIFStruct.images_decoded > 0) { + if (mColormap) { memcpy(mColormap, mGIFStruct.global_colormap, mColormapSize); } else { mColormap = mGIFStruct.global_colormap; } return Transition::To(State::IMAGE_DATA_BLOCK, BLOCK_HEADER_LEN); } @@ -1046,17 +1068,17 @@ nsGIFDecoder2::ReadLZWData(const char* a { const uint8_t* data = reinterpret_cast<const uint8_t*>(aData); size_t length = aLength; while (mGIFStruct.pixels_remaining > 0 && (length > 0 || mGIFStruct.bits >= mGIFStruct.codesize)) { size_t bytesRead = 0; - auto result = mGIFStruct.images_decoded == 0 + auto result = mGIFStruct.images_decoded == 0 || ShouldBlendAnimation() ? mPipe.WritePixelBlocks<uint32_t>([&](uint32_t* aPixelBlock, int32_t aBlockSize) { return YieldPixels<uint32_t>(data, length, &bytesRead, aPixelBlock, aBlockSize); }) : mPipe.WritePixelBlocks<uint8_t>([&](uint8_t* aPixelBlock, int32_t aBlockSize) { return YieldPixels<uint8_t>(data, length, &bytesRead, aPixelBlock, aBlockSize); }); if (MOZ_UNLIKELY(bytesRead > length)) {
--- a/image/decoders/nsPNGDecoder.cpp +++ b/image/decoders/nsPNGDecoder.cpp @@ -228,16 +228,20 @@ nsPNGDecoder::CreateFrame(const FrameInf ? SurfacePipeFlags::ADAM7_INTERPOLATE : SurfacePipeFlags(); if (mNumFrames == 0) { // The first frame may be displayed progressively. pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY; } + if (ShouldBlendAnimation()) { + pipeFlags |= SurfacePipeFlags::BLEND_ANIMATION; + } + Maybe<SurfacePipe> pipe = SurfacePipeFactory::CreateSurfacePipe(this, Size(), OutputSize(), aFrameInfo.mFrameRect, mFormat, animParams, pipeFlags); if (!pipe) { mPipe = SurfacePipe(); return NS_ERROR_FAILURE;
--- a/image/imgFrame.cpp +++ b/image/imgFrame.cpp @@ -103,34 +103,33 @@ ShouldUseHeap(const IntSize& aSize, } return false; } static already_AddRefed<DataSourceSurface> AllocateBufferForImage(const IntSize& size, SurfaceFormat format, - bool aIsAnimated = false) + bool aIsAnimated = false, + bool aIsFullFrame = true) { int32_t stride = VolatileSurfaceStride(size, format); - if (ShouldUseHeap(size, stride, aIsAnimated)) { + if (gfxVars::GetUseWebRenderOrDefault() && + gfxPrefs::ImageMemShared() && aIsFullFrame) { + RefPtr<SourceSurfaceSharedData> newSurf = new SourceSurfaceSharedData(); + if (newSurf->Init(size, stride, format)) { + return newSurf.forget(); + } + } else if (ShouldUseHeap(size, stride, aIsAnimated)) { RefPtr<SourceSurfaceAlignedRawData> newSurf = new SourceSurfaceAlignedRawData(); if (newSurf->Init(size, format, false, 0, stride)) { return newSurf.forget(); } - } - - if (!aIsAnimated && gfxVars::GetUseWebRenderOrDefault() - && gfxPrefs::ImageMemShared()) { - RefPtr<SourceSurfaceSharedData> newSurf = new SourceSurfaceSharedData(); - if (newSurf->Init(size, stride, format)) { - return newSurf.forget(); - } } else { RefPtr<SourceSurfaceVolatileData> newSurf= new SourceSurfaceVolatileData(); if (newSurf->Init(size, stride, format)) { return newSurf.forget(); } } return nullptr; } @@ -208,16 +207,17 @@ imgFrame::imgFrame() , mOptimizable(false) , mTimeout(FrameTimeout::FromRawMilliseconds(100)) , mDisposalMethod(DisposalMethod::NOT_SPECIFIED) , mBlendMethod(BlendMethod::OVER) , mFormat(SurfaceFormat::UNKNOWN) , mPalettedImageData(nullptr) , mPaletteDepth(0) , mNonPremult(false) + , mIsFullFrame(false) , mCompositingFailed(false) { } imgFrame::~imgFrame() { #ifdef DEBUG MonitorAutoLock lock(mMonitor); @@ -230,36 +230,44 @@ imgFrame::~imgFrame() } nsresult imgFrame::InitForDecoder(const nsIntSize& aImageSize, const nsIntRect& aRect, SurfaceFormat aFormat, uint8_t aPaletteDepth /* = 0 */, bool aNonPremult /* = false */, - const Maybe<AnimationParams>& aAnimParams /* = Nothing() */) + const Maybe<AnimationParams>& aAnimParams /* = Nothing() */, + bool aIsFullFrame /* = false */) { // Assert for properties that should be verified by decoders, // warn for properties related to bad content. if (!AllowedImageAndFrameDimensions(aImageSize, aRect)) { NS_WARNING("Should have legal image size"); mAborted = true; return NS_ERROR_FAILURE; } mImageSize = aImageSize; mFrameRect = aRect; + // May be updated shortly after InitForDecoder by BlendAnimationFilter + // because it needs to take into consideration the previous frames to + // properly calculate. We start with the whole frame as dirty. + mDirtyRect = aRect; + if (aAnimParams) { mBlendRect = aAnimParams->mBlendRect; mTimeout = aAnimParams->mTimeout; mBlendMethod = aAnimParams->mBlendMethod; mDisposalMethod = aAnimParams->mDisposalMethod; + mIsFullFrame = aAnimParams->mFrameNum == 0 || aIsFullFrame; } else { mBlendRect = aRect; + mIsFullFrame = true; } // We only allow a non-trivial frame rect (i.e., a frame rect that doesn't // cover the entire image) for paletted animation frames. We never draw those // frames directly; we just use FrameAnimator to composite them and produce a // BGRA surface that we actually draw. We enforce this here to make sure that // imgFrame::Draw(), which is responsible for drawing all other kinds of // frames, never has to deal with a non-trivial frame rect. @@ -290,17 +298,18 @@ imgFrame::InitForDecoder(const nsIntSize if (!mPalettedImageData) { NS_WARNING("Call to calloc for paletted image data should succeed"); } NS_ENSURE_TRUE(mPalettedImageData, NS_ERROR_OUT_OF_MEMORY); } else { MOZ_ASSERT(!mLockedSurface, "Called imgFrame::InitForDecoder() twice?"); bool postFirstFrame = aAnimParams && aAnimParams->mFrameNum > 0; - mRawSurface = AllocateBufferForImage(mFrameRect.Size(), mFormat, postFirstFrame); + mRawSurface = AllocateBufferForImage(mFrameRect.Size(), mFormat, + postFirstFrame, mIsFullFrame); if (!mRawSurface) { mAborted = true; return NS_ERROR_OUT_OF_MEMORY; } mLockedSurface = CreateLockedSurface(mRawSurface, mFrameRect.Size(), mFormat); if (!mLockedSurface) { NS_WARNING("Failed to create LockedSurface");
--- a/image/imgFrame.h +++ b/image/imgFrame.h @@ -54,27 +54,32 @@ public: * when drawing content into an imgFrame, as it may use a different graphics * backend than normal content drawing. */ nsresult InitForDecoder(const nsIntSize& aImageSize, const nsIntRect& aRect, SurfaceFormat aFormat, uint8_t aPaletteDepth = 0, bool aNonPremult = false, - const Maybe<AnimationParams>& aAnimParams = Nothing()); + const Maybe<AnimationParams>& aAnimParams = Nothing(), + bool aIsFullFrame = false); nsresult InitForAnimator(const nsIntSize& aSize, SurfaceFormat aFormat) { nsIntRect frameRect(0, 0, aSize.width, aSize.height); AnimationParams animParams { frameRect, FrameTimeout::Forever(), /* aFrameNum */ 1, BlendMethod::OVER, DisposalMethod::NOT_SPECIFIED }; - return InitForDecoder(aSize, frameRect, - aFormat, 0, false, Some(animParams)); + // We set aIsFullFrame to false because we don't want the compositing frame + // to be allocated into shared memory for WebRender. mIsFullFrame is only + // otherwise used for frames produced by Decoder, so it isn't relevant. + return InitForDecoder(aSize, frameRect, aFormat, /* aPaletteDepth */ 0, + /* aNonPremult */ false, Some(animParams), + /* aIsFullFrame */ false); } /** * Initialize this imgFrame with a new surface and draw the provided * gfxDrawable into it. * * This is appropriate to use when drawing content into an imgFrame, as it @@ -183,16 +188,21 @@ public: void GetImageData(uint8_t** aData, uint32_t* length) const; uint8_t* GetImageData() const; bool GetIsPaletted() const; void GetPaletteData(uint32_t** aPalette, uint32_t* length) const; uint32_t* GetPaletteData() const; uint8_t GetPaletteDepth() const { return mPaletteDepth; } + const IntRect& GetDirtyRect() const { return mDirtyRect; } + void SetDirtyRect(const IntRect& aDirtyRect) { mDirtyRect = aDirtyRect; } + + bool IsFullFrame() const { return mIsFullFrame; } + bool GetCompositingFailed() const; void SetCompositingFailed(bool val); void SetOptimizable(); void FinalizeSurface(); already_AddRefed<SourceSurface> GetSourceSurface(); @@ -290,36 +300,61 @@ private: // data bool mFinished; bool mOptimizable; ////////////////////////////////////////////////////////////////////////////// // Effectively const data, only mutated in the Init methods. ////////////////////////////////////////////////////////////////////////////// + //! The size of the buffer we are decoding to. IntSize mImageSize; + + //! XXX(aosmond): This means something different depending on the context. We + //! should correct this. + //! + //! There are several different contexts for mFrameRect: + //! - If for non-animated image, it will be originate at (0, 0) and matches + //! the dimensions of mImageSize. + //! - If for an APNG, it also matches the above. + //! - If for a GIF which is producing full frames, it matches the above. + //! - If for a GIF which is producing partial frames, it matches mBlendRect. IntRect mFrameRect; + + //! The contents for the frame, as represented in the encoded image. This may + //! differ from mImageSize because it may be a partial frame. For the first + //! frame, this means we need to shift the data in place, and for animated + //! frames, it likely need to combine with a previous frame to get the full + //! contents. IntRect mBlendRect; + //! This is the region that has changed between this frame and the previous + //! frame of an animation. For the first frame, this will be the same as + //! mFrameRect. + IntRect mDirtyRect; + //! The timeout for this frame. FrameTimeout mTimeout; DisposalMethod mDisposalMethod; BlendMethod mBlendMethod; SurfaceFormat mFormat; // The palette and image data for images that are paletted, since Cairo // doesn't support these images. // The paletted data comes first, then the image data itself. // Total length is PaletteDataLength() + GetImageDataLength(). uint8_t* mPalettedImageData; uint8_t mPaletteDepth; bool mNonPremult; + //! True if the frame has all of the data stored in it, false if it needs to + //! be combined with another frame (e.g. the previous frame) to be complete. + bool mIsFullFrame; ////////////////////////////////////////////////////////////////////////////// // Main-thread-only mutable data. ////////////////////////////////////////////////////////////////////////////// bool mCompositingFailed; };
--- a/image/test/gtest/Common.cpp +++ b/image/test/gtest/Common.cpp @@ -190,30 +190,31 @@ RectIsSolidColor(SourceSurface* aSurface DataSourceSurface::ScopedMap mapping(dataSurface, DataSourceSurface::MapType::READ); ASSERT_TRUE_OR_RETURN(mapping.IsMapped(), false); ASSERT_EQ_OR_RETURN(mapping.GetStride(), surfaceSize.width * 4, false); uint8_t* data = mapping.GetData(); ASSERT_TRUE_OR_RETURN(data != nullptr, false); + BGRAColor pmColor = aColor.Premultiply(); int32_t rowLength = mapping.GetStride(); for (int32_t row = rect.Y(); row < rect.YMost(); ++row) { for (int32_t col = rect.X(); col < rect.XMost(); ++col) { int32_t i = row * rowLength + col * 4; if (aFuzz != 0) { - ASSERT_LE_OR_RETURN(abs(aColor.mBlue - data[i + 0]), aFuzz, false); - ASSERT_LE_OR_RETURN(abs(aColor.mGreen - data[i + 1]), aFuzz, false); - ASSERT_LE_OR_RETURN(abs(aColor.mRed - data[i + 2]), aFuzz, false); - ASSERT_LE_OR_RETURN(abs(aColor.mAlpha - data[i + 3]), aFuzz, false); + ASSERT_LE_OR_RETURN(abs(pmColor.mBlue - data[i + 0]), aFuzz, false); + ASSERT_LE_OR_RETURN(abs(pmColor.mGreen - data[i + 1]), aFuzz, false); + ASSERT_LE_OR_RETURN(abs(pmColor.mRed - data[i + 2]), aFuzz, false); + ASSERT_LE_OR_RETURN(abs(pmColor.mAlpha - data[i + 3]), aFuzz, false); } else { - ASSERT_EQ_OR_RETURN(aColor.mBlue, data[i + 0], false); - ASSERT_EQ_OR_RETURN(aColor.mGreen, data[i + 1], false); - ASSERT_EQ_OR_RETURN(aColor.mRed, data[i + 2], false); - ASSERT_EQ_OR_RETURN(aColor.mAlpha, data[i + 3], false); + ASSERT_EQ_OR_RETURN(pmColor.mBlue, data[i + 0], false); + ASSERT_EQ_OR_RETURN(pmColor.mGreen, data[i + 1], false); + ASSERT_EQ_OR_RETURN(pmColor.mRed, data[i + 2], false); + ASSERT_EQ_OR_RETURN(pmColor.mAlpha, data[i + 3], false); } } } return true; } bool @@ -295,16 +296,17 @@ RowHasPixels(SourceSurface* aSurface, already_AddRefed<Decoder> CreateTrivialDecoder() { gfxPrefs::GetSingleton(); DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif"); auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>(); RefPtr<Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(), + DefaultDecoderFlags(), DefaultSurfaceFlags()); return decoder.forget(); } void AssertCorrectPipelineFinalState(SurfaceFilter* aFilter, const gfx::IntRect& aInputSpaceRect, const gfx::IntRect& aOutputSpaceRect)
--- a/image/test/gtest/Common.h +++ b/image/test/gtest/Common.h @@ -5,16 +5,17 @@ #ifndef mozilla_image_test_gtest_Common_h #define mozilla_image_test_gtest_Common_h #include <vector> #include "gtest/gtest.h" +#include "mozilla/Attributes.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" @@ -70,34 +71,53 @@ struct ImageTestCase gfx::IntSize mOutputSize; uint32_t mFlags; }; struct BGRAColor { BGRAColor() : BGRAColor(0, 0, 0, 0) { } - BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha) + BGRAColor(uint8_t aBlue, uint8_t aGreen, uint8_t aRed, uint8_t aAlpha, bool aPremultiplied = false) : mBlue(aBlue) , mGreen(aGreen) , mRed(aRed) , mAlpha(aAlpha) + , mPremultiplied(aPremultiplied) { } static BGRAColor Green() { return BGRAColor(0x00, 0xFF, 0x00, 0xFF); } static BGRAColor Red() { return BGRAColor(0x00, 0x00, 0xFF, 0xFF); } static BGRAColor Blue() { return BGRAColor(0xFF, 0x00, 0x00, 0xFF); } static BGRAColor Transparent() { return BGRAColor(0x00, 0x00, 0x00, 0x00); } - uint32_t AsPixel() const { return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue); } + BGRAColor Premultiply() const + { + if (!mPremultiplied) { + return BGRAColor(gfxPreMultiply(mBlue, mAlpha), + gfxPreMultiply(mGreen, mAlpha), + gfxPreMultiply(mRed, mAlpha), + mAlpha, + true); + } + return *this; + } + + uint32_t AsPixel() const { + if (!mPremultiplied) { + return gfxPackedPixel(mAlpha, mRed, mGreen, mBlue); + } + return gfxPackedPixelNoPreMultiply(mAlpha, mRed, mGreen, mBlue); + } uint8_t mBlue; uint8_t mGreen; uint8_t mRed; uint8_t mAlpha; + bool mPremultiplied; }; /////////////////////////////////////////////////////////////////////////////// // General Helpers /////////////////////////////////////////////////////////////////////////////// /** @@ -236,30 +256,38 @@ already_AddRefed<Decoder> CreateTrivialD * it to the provided lambda @aFunc. Assertions that the pipeline is constructly * correctly and cleanup of any allocated surfaces is handled automatically. * * @param aDecoder The decoder to use for allocating surfaces. * @param aFunc The lambda function to pass the filter pipeline to. * @param aConfigs The configuration for the pipeline. */ template <typename Func, typename... Configs> -void WithFilterPipeline(Decoder* aDecoder, Func aFunc, const Configs&... aConfigs) +void WithFilterPipeline(Decoder* aDecoder, Func aFunc, bool aFinish, const Configs&... aConfigs) { auto pipe = MakeUnique<typename detail::FilterPipeline<Configs...>::Type>(); nsresult rv = pipe->Configure(aConfigs...); ASSERT_TRUE(NS_SUCCEEDED(rv)); aFunc(aDecoder, pipe.get()); - RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); - if (currentFrame) { - currentFrame->Finish(); + if (aFinish) { + RawAccessFrameRef currentFrame = aDecoder->GetCurrentFrameRef(); + if (currentFrame) { + currentFrame->Finish(); + } } } +template <typename Func, typename... Configs> +void WithFilterPipeline(Decoder* aDecoder, Func aFunc, const Configs&... aConfigs) +{ + WithFilterPipeline(aDecoder, aFunc, true, aConfigs...); +} + /** * Creates a pipeline of SurfaceFilters from a list of Config structs and * asserts that configuring it fails. Cleanup of any allocated surfaces is * handled automatically. * * @param aDecoder The decoder to use for allocating surfaces. * @param aConfigs The configuration for the pipeline. */ @@ -364,16 +392,41 @@ void CheckWritePixels(Decoder* aDecoder, void CheckPalettedWritePixels(Decoder* aDecoder, SurfaceFilter* aFilter, const Maybe<gfx::IntRect>& aOutputRect = Nothing(), const Maybe<gfx::IntRect>& aInputRect = Nothing(), const Maybe<gfx::IntRect>& aInputWriteRect = Nothing(), const Maybe<gfx::IntRect>& aOutputWriteRect = Nothing(), uint8_t aFuzz = 0); +/////////////////////////////////////////////////////////////////////////////// +// Decoder Helpers +/////////////////////////////////////////////////////////////////////////////// + +// Friend class of Decoder to access internals for tests. +class MOZ_STACK_CLASS DecoderTestHelper final +{ +public: + explicit DecoderTestHelper(Decoder* aDecoder) + : mDecoder(aDecoder) + { } + + void PostIsAnimated(FrameTimeout aTimeout) + { + mDecoder->PostIsAnimated(aTimeout); + } + + void PostFrameStop(Opacity aOpacity) + { + mDecoder->PostFrameStop(aOpacity); + } + +private: + Decoder* mDecoder; +}; /////////////////////////////////////////////////////////////////////////////// // Test Data /////////////////////////////////////////////////////////////////////////////// ImageTestCase GreenPNGTestCase(); ImageTestCase GreenGIFTestCase(); ImageTestCase GreenJPGTestCase();
new file mode 100644 --- /dev/null +++ b/image/test/gtest/TestBlendAnimationFilter.cpp @@ -0,0 +1,504 @@ +/* -*- 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 "gtest/gtest.h" + +#include "mozilla/gfx/2D.h" +#include "skia/include/core/SkColorPriv.h" // for SkPMSrcOver +#include "Common.h" +#include "Decoder.h" +#include "DecoderFactory.h" +#include "SourceBuffer.h" +#include "SurfaceFilters.h" +#include "SurfacePipe.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::image; + +static already_AddRefed<Decoder> +CreateTrivialBlendingDecoder() +{ + gfxPrefs::GetSingleton(); + DecoderType decoderType = DecoderFactory::GetDecoderType("image/gif"); + DecoderFlags decoderFlags = DecoderFlags::BLEND_ANIMATION; + SurfaceFlags surfaceFlags = DefaultSurfaceFlags(); + auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>(); + return DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, + Nothing(), decoderFlags, + surfaceFlags); +} + +template <typename Func> RawAccessFrameRef +WithBlendAnimationFilter(Decoder* aDecoder, + const AnimationParams& aAnimParams, + const IntSize& aOutputSize, + Func aFunc) +{ + DecoderTestHelper decoderHelper(aDecoder); + + if (!aDecoder->HasAnimation()) { + decoderHelper.PostIsAnimated(aAnimParams.mTimeout); + } + + BlendAnimationConfig blendAnim { aDecoder }; + SurfaceConfig surfaceSink { aDecoder, aOutputSize, SurfaceFormat::B8G8R8A8, + false, Some(aAnimParams) }; + + auto func = [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + aFunc(aDecoder, aFilter); + }; + + WithFilterPipeline(aDecoder, func, false, blendAnim, surfaceSink); + + RawAccessFrameRef current = aDecoder->GetCurrentFrameRef(); + if (current) { + decoderHelper.PostFrameStop(Opacity::SOME_TRANSPARENCY); + } + + return current; +} + +void +AssertConfiguringBlendAnimationFilterFails(const IntRect& aFrameRect, + const IntSize& aOutputSize) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams animParams { aFrameRect, FrameTimeout::FromRawMilliseconds(0), + 0, BlendMethod::SOURCE, DisposalMethod::KEEP }; + BlendAnimationConfig blendAnim { decoder }; + SurfaceConfig surfaceSink { decoder, aOutputSize, + SurfaceFormat::B8G8R8A8, false, + Some(animParams) }; + AssertConfiguringPipelineFails(decoder, blendAnim, surfaceSink); +} + +TEST(ImageBlendAnimationFilter, BlendFailsForNegativeFrameRect) +{ + // A negative frame rect size is disallowed. + AssertConfiguringBlendAnimationFilterFails(IntRect(IntPoint(0, 0), IntSize(-1, -1)), + IntSize(100, 100)); +} + +TEST(ImageBlendAnimationFilter, WriteFullFirstFrame) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100))); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); +} + +TEST(ImageBlendAnimationFilter, WritePartialFirstFrame) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params { IntRect(25, 50, 50, 25), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + CheckWritePixels(aDecoder, aFilter, Some(IntRect(0, 0, 100, 100)), + Nothing(), + Some(IntRect(25, 50, 50, 25)), + Some(IntRect(25, 50, 50, 25))); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); +} + +static void +TestWithBlendAnimationFilterClear(BlendMethod aBlendMethod) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1 { IntRect(0, 40, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, + DisposalMethod::CLEAR }; + RawAccessFrameRef frame1 = + WithBlendAnimationFilter(decoder, params1, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(BGRAColor::Red().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect()); + + ASSERT_TRUE(frame1.get() != nullptr); + + RefPtr<SourceSurface> surface = frame1->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green())); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red())); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green())); + + AnimationParams params2 { IntRect(0, 50, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, aBlendMethod, + DisposalMethod::KEEP }; + RawAccessFrameRef frame2 = + WithBlendAnimationFilter(decoder, params2, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(BGRAColor::Blue().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + + ASSERT_TRUE(frame2.get() != nullptr); + + surface = frame2->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green())); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 10, BGRAColor::Transparent())); + EXPECT_TRUE(RowsAreSolidColor(surface, 50, 20, BGRAColor::Blue())); + EXPECT_TRUE(RowsAreSolidColor(surface, 70, 30, BGRAColor::Green())); +} + +TEST(ImageBlendAnimationFilter, ClearWithOver) +{ + TestWithBlendAnimationFilterClear(BlendMethod::OVER); +} + +TEST(ImageBlendAnimationFilter, ClearWithSource) +{ + TestWithBlendAnimationFilterClear(BlendMethod::SOURCE); +} + +TEST(ImageBlendAnimationFilter, KeepWithSource) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(BGRAColor::Green().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1 { IntRect(0, 40, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + RawAccessFrameRef frame1 = + WithBlendAnimationFilter(decoder, params1, IntSize(100, 100), + [](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(BGRAColor::Red().AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect()); + + ASSERT_TRUE(frame1.get() != nullptr); + + RefPtr<SourceSurface> surface = frame1->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, BGRAColor::Green())); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, BGRAColor::Red())); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, BGRAColor::Green())); +} + +TEST(ImageBlendAnimationFilter, KeepWithOver) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor0(0, 0xFF, 0, 0x40); + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor0.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1 { IntRect(0, 40, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::OVER, + DisposalMethod::KEEP }; + BGRAColor frameColor1(0, 0, 0xFF, 0x80); + RawAccessFrameRef frame1 = + WithBlendAnimationFilter(decoder, params1, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor1.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 40, 100, 20), frame1->GetDirtyRect()); + + ASSERT_TRUE(frame1.get() != nullptr); + + BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied + EXPECT_EQ(SkPMSrcOver(frameColor1.AsPixel(), frameColor0.AsPixel()), + blendedColor.AsPixel()); + + RefPtr<SourceSurface> surface = frame1->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, RestorePreviousWithOver) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor0(0, 0xFF, 0, 0x40); + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor0.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1 { IntRect(0, 10, 100, 80), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, + DisposalMethod::RESTORE_PREVIOUS }; + BGRAColor frameColor1 = BGRAColor::Green(); + RawAccessFrameRef frame1 = + WithBlendAnimationFilter(decoder, params1, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor1.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect()); + + AnimationParams params2 { IntRect(0, 40, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, BlendMethod::OVER, + DisposalMethod::KEEP }; + BGRAColor frameColor2(0, 0, 0xFF, 0x80); + RawAccessFrameRef frame2 = + WithBlendAnimationFilter(decoder, params2, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor2.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect()); + + ASSERT_TRUE(frame2.get() != nullptr); + + BGRAColor blendedColor(0, 0x20, 0x80, 0xA0, true); // already premultiplied + EXPECT_EQ(SkPMSrcOver(frameColor2.AsPixel(), frameColor0.AsPixel()), + blendedColor.AsPixel()); + + RefPtr<SourceSurface> surface = frame2->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, blendedColor)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, RestorePreviousWithSource) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor0(0, 0xFF, 0, 0x40); + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor0.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1 { IntRect(0, 10, 100, 80), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, + DisposalMethod::RESTORE_PREVIOUS }; + BGRAColor frameColor1 = BGRAColor::Green(); + RawAccessFrameRef frame1 = + WithBlendAnimationFilter(decoder, params1, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor1.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame1->GetDirtyRect()); + + AnimationParams params2 { IntRect(0, 40, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor2(0, 0, 0xFF, 0x80); + RawAccessFrameRef frame2 = + WithBlendAnimationFilter(decoder, params2, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor2.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 10, 100, 80), frame2->GetDirtyRect()); + + ASSERT_TRUE(frame2.get() != nullptr); + + RefPtr<SourceSurface> surface = frame2->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 40, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor2)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, RestorePreviousClearWithSource) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(0, 0, 100, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor0 = BGRAColor::Red(); + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor0.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + AnimationParams params1 { IntRect(0, 0, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 1, BlendMethod::SOURCE, + DisposalMethod::CLEAR }; + BGRAColor frameColor1 = BGRAColor::Blue(); + RawAccessFrameRef frame1 = + WithBlendAnimationFilter(decoder, params1, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor1.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 20), frame1->GetDirtyRect()); + + AnimationParams params2 { IntRect(0, 10, 100, 80), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 2, BlendMethod::SOURCE, + DisposalMethod::RESTORE_PREVIOUS }; + BGRAColor frameColor2 = BGRAColor::Green(); + RawAccessFrameRef frame2 = + WithBlendAnimationFilter(decoder, params2, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor2.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 90), frame2->GetDirtyRect()); + + AnimationParams params3 { IntRect(0, 40, 100, 20), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 3, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor3 = BGRAColor::Blue(); + RawAccessFrameRef frame3 = + WithBlendAnimationFilter(decoder, params3, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor3.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 90), frame3->GetDirtyRect()); + + ASSERT_TRUE(frame3.get() != nullptr); + + RefPtr<SourceSurface> surface = frame3->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 20, BGRAColor::Transparent())); + EXPECT_TRUE(RowsAreSolidColor(surface, 20, 20, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 40, 20, frameColor3)); + EXPECT_TRUE(RowsAreSolidColor(surface, 60, 40, frameColor0)); +} + +TEST(ImageBlendAnimationFilter, PartialOverlapFrameRect) +{ + RefPtr<Decoder> decoder = CreateTrivialBlendingDecoder(); + ASSERT_TRUE(decoder != nullptr); + + AnimationParams params0 { IntRect(-10, -20, 110, 100), + FrameTimeout::FromRawMilliseconds(0), + /* aFrameNum */ 0, BlendMethod::SOURCE, + DisposalMethod::KEEP }; + BGRAColor frameColor0 = BGRAColor::Red(); + RawAccessFrameRef frame0 = + WithBlendAnimationFilter(decoder, params0, IntSize(100, 100), + [&](Decoder* aDecoder, SurfaceFilter* aFilter) { + auto result = aFilter->WritePixels<uint32_t>([&] { + return AsVariant(frameColor0.AsPixel()); + }); + EXPECT_EQ(WriteState::FINISHED, result); + }); + EXPECT_EQ(IntRect(0, 0, 100, 100), frame0->GetDirtyRect()); + + RefPtr<SourceSurface> surface = frame0->GetSourceSurface(); + EXPECT_TRUE(RowsAreSolidColor(surface, 0, 80, frameColor0)); + EXPECT_TRUE(RowsAreSolidColor(surface, 80, 20, BGRAColor::Transparent())); +} +
--- a/image/test/gtest/TestDecoders.cpp +++ b/image/test/gtest/TestDecoders.cpp @@ -1,15 +1,16 @@ /* 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 "Common.h" +#include "AnimationSurfaceProvider.h" #include "Decoder.h" #include "DecoderFactory.h" #include "decoders/nsBMPDecoder.h" #include "IDecodingTask.h" #include "ImageOps.h" #include "imgIContainer.h" #include "imgITools.h" #include "ImageFactory.h" @@ -110,16 +111,17 @@ void WithSingleChunkDecode(const ImageTe ASSERT_TRUE(NS_SUCCEEDED(rv)); sourceBuffer->Complete(NS_OK); // Create a decoder. DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); RefPtr<Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, aOutputSize, + DecoderFlags::FIRST_FRAME_ONLY, DefaultSurfaceFlags()); ASSERT_TRUE(decoder != nullptr); RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder)); // Run the full decoder synchronously. task->Run(); // Call the lambda to verify the expected results. @@ -147,16 +149,17 @@ CheckDecoderMultiChunk(const ImageTestCa // Create a SourceBuffer and a decoder. auto sourceBuffer = MakeNotNull<RefPtr<SourceBuffer>>(); sourceBuffer->ExpectLength(length); DecoderType decoderType = DecoderFactory::GetDecoderType(aTestCase.mMimeType); RefPtr<Decoder> decoder = DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(), + DecoderFlags::FIRST_FRAME_ONLY, DefaultSurfaceFlags()); ASSERT_TRUE(decoder != nullptr); RefPtr<IDecodingTask> task = new AnonymousDecodingTask(WrapNotNull(decoder)); for (uint64_t read = 0; read < length ; ++read) { uint64_t available = 0; rv = inputStream->Available(&available); ASSERT_TRUE(available > 0); @@ -199,16 +202,138 @@ CheckDownscaleDuringDecode(const ImageTe // small amount of fuzz; this is just the nature of Lanczos downscaling. EXPECT_TRUE(RowsAreSolidColor(surface, 0, 4, BGRAColor::Green(), /* aFuzz = */ 47)); 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)); }); } +static void +CheckAnimationDecoderResults(const ImageTestCase& aTestCase, + AnimationSurfaceProvider* aProvider, + Decoder* aDecoder) +{ + EXPECT_TRUE(aDecoder->GetDecodeDone()); + EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_HAS_ERROR), + aDecoder->HasError()); + + if (aTestCase.mFlags & TEST_CASE_HAS_ERROR) { + return; // That's all we can check for bad images. + } + + // The decoder should get the correct size. + IntSize size = aDecoder->Size(); + EXPECT_EQ(aTestCase.mSize.width, size.width); + EXPECT_EQ(aTestCase.mSize.height, size.height); + + if (aTestCase.mFlags & TEST_CASE_IGNORE_OUTPUT) { + return; + } + + // Check the output. + AutoTArray<BGRAColor, 2> framePixels; + framePixels.AppendElement(BGRAColor::Green()); + framePixels.AppendElement(BGRAColor(0x7F, 0x7F, 0x7F, 0xFF)); + + DrawableSurface drawableSurface(WrapNotNull(aProvider)); + for (size_t i = 0; i < framePixels.Length(); ++i) { + nsresult rv = drawableSurface.Seek(i); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + // Check the first frame, all green. + RawAccessFrameRef rawFrame = drawableSurface->RawAccessRef(); + RefPtr<SourceSurface> surface = rawFrame->GetSourceSurface(); + + // Verify that the resulting surfaces matches our expectations. + EXPECT_TRUE(surface->IsDataSourceSurface()); + EXPECT_TRUE(surface->GetFormat() == SurfaceFormat::B8G8R8X8 || + surface->GetFormat() == SurfaceFormat::B8G8R8A8); + EXPECT_EQ(aTestCase.mOutputSize, surface->GetSize()); + EXPECT_TRUE(IsSolidColor(surface, framePixels[i], + aTestCase.mFlags & TEST_CASE_IS_FUZZY ? 1 : 0)); + } + + // Should be no more frames. + nsresult rv = drawableSurface.Seek(framePixels.Length()); + EXPECT_TRUE(NS_FAILED(rv)); +} + +template <typename Func> +static void +WithSingleChunkAnimationDecode(const ImageTestCase& aTestCase, + Func aResultChecker) +{ + // Create an image. + RefPtr<Image> image = + ImageFactory::CreateAnonymousImage(nsDependentCString(aTestCase.mMimeType)); + ASSERT_TRUE(!image->HasError()); + + NotNull<RefPtr<RasterImage>> rasterImage = + WrapNotNull(static_cast<RasterImage*>(image.get())); + + nsCOMPtr<nsIInputStream> inputStream = LoadFile(aTestCase.mPath); + ASSERT_TRUE(inputStream != nullptr); + + // Figure out how much data we have. + uint64_t length; + nsresult rv = inputStream->Available(&length); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + // Write the data into a SourceBuffer. + NotNull<RefPtr<SourceBuffer>> sourceBuffer = WrapNotNull(new SourceBuffer()); + sourceBuffer->ExpectLength(length); + rv = sourceBuffer->AppendFromInputStream(inputStream, length); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + sourceBuffer->Complete(NS_OK); + + // Create a metadata decoder first, because otherwise RasterImage will get + // unhappy about finding out the image is animated during a full decode. + DecoderType decoderType = + DecoderFactory::GetDecoderType(aTestCase.mMimeType); + RefPtr<IDecodingTask> task = + DecoderFactory::CreateMetadataDecoder(decoderType, rasterImage, sourceBuffer); + ASSERT_TRUE(task != nullptr); + + // Run the metadata decoder synchronously. + task->Run(); + + // Create a decoder. + DecoderFlags decoderFlags = DecoderFlags::BLEND_ANIMATION; + SurfaceFlags surfaceFlags = DefaultSurfaceFlags(); + RefPtr<Decoder> decoder = + DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(), + decoderFlags, surfaceFlags); + ASSERT_TRUE(decoder != nullptr); + + // Create an AnimationSurfaceProvider which will manage the decoding process + // and make this decoder's output available in the surface cache. + SurfaceKey surfaceKey = + RasterSurfaceKey(aTestCase.mOutputSize, surfaceFlags, PlaybackType::eAnimated); + RefPtr<AnimationSurfaceProvider> provider = + new AnimationSurfaceProvider(rasterImage, + surfaceKey, + WrapNotNull(decoder), + /* aCurrentFrame */ 0); + + // Run the full decoder synchronously. + provider->Run(); + + // Call the lambda to verify the expected results. + aResultChecker(provider, decoder); +} + +static void +CheckAnimationDecoderSingleChunk(const ImageTestCase& aTestCase) +{ + WithSingleChunkAnimationDecode(aTestCase, [&](AnimationSurfaceProvider* aProvider, Decoder* aDecoder) { + CheckAnimationDecoderResults(aTestCase, aProvider, aDecoder); + }); +} + class ImageDecoders : public ::testing::Test { protected: AutoInitializeImageLib mInit; }; TEST_F(ImageDecoders, PNGSingleChunk) { @@ -310,26 +435,36 @@ TEST_F(ImageDecoders, AnimatedGIFSingleC CheckDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase()); } TEST_F(ImageDecoders, AnimatedGIFMultiChunk) { CheckDecoderMultiChunk(GreenFirstFrameAnimatedGIFTestCase()); } +TEST_F(ImageDecoders, AnimatedGIFWithBlendedFrames) +{ + CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedGIFTestCase()); +} + TEST_F(ImageDecoders, AnimatedPNGSingleChunk) { CheckDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase()); } TEST_F(ImageDecoders, AnimatedPNGMultiChunk) { CheckDecoderMultiChunk(GreenFirstFrameAnimatedPNGTestCase()); } +TEST_F(ImageDecoders, AnimatedPNGWithBlendedFrames) +{ + CheckAnimationDecoderSingleChunk(GreenFirstFrameAnimatedPNGTestCase()); +} + TEST_F(ImageDecoders, CorruptSingleChunk) { CheckDecoderSingleChunk(CorruptTestCase()); } TEST_F(ImageDecoders, CorruptMultiChunk) { CheckDecoderMultiChunk(CorruptTestCase());
--- a/image/test/gtest/TestMetadata.cpp +++ b/image/test/gtest/TestMetadata.cpp @@ -103,16 +103,17 @@ CheckMetadata(const ImageTestCase& aTest EXPECT_EQ(expectTransparency, bool(metadataProgress & FLAG_HAS_TRANSPARENCY)); EXPECT_EQ(bool(aTestCase.mFlags & TEST_CASE_IS_ANIMATED), bool(metadataProgress & FLAG_IS_ANIMATED)); // Create a full decoder, so we can compare the result. decoder = DecoderFactory::CreateAnonymousDecoder(decoderType, sourceBuffer, Nothing(), + DecoderFlags::FIRST_FRAME_ONLY, DefaultSurfaceFlags()); ASSERT_TRUE(decoder != nullptr); task = new AnonymousDecodingTask(WrapNotNull(decoder)); if (aBMPWithinICO == BMPWithinICO::YES) { static_cast<nsBMPDecoder*>(decoder.get())->SetIsWithinICO(); }
--- a/image/test/gtest/moz.build +++ b/image/test/gtest/moz.build @@ -5,16 +5,17 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. Library('imagetest') UNIFIED_SOURCES = [ 'Common.cpp', 'TestADAM7InterpolatingFilter.cpp', 'TestAnimationFrameBuffer.cpp', + 'TestBlendAnimationFilter.cpp', 'TestContainers.cpp', 'TestCopyOnWrite.cpp', 'TestDecoders.cpp', 'TestDecodeToSurface.cpp', 'TestDeinterlacingFilter.cpp', 'TestLoader.cpp', 'TestMetadata.cpp', 'TestRemoveFrameRectFilter.cpp',
--- a/js/src/jit-test/tests/cacheir/binaryarith.js +++ b/js/src/jit-test/tests/cacheir/binaryarith.js @@ -40,16 +40,21 @@ obj1 = new D('A'); var funAdd4 = (a, b) => { return a + b; } warmup(funAdd4, [["x", obj1, "xA"], [obj1, "bba", "Abba"]]); // Add: Int32 Boolean var funAdd5 = (a, b) => { return a + b; } warmup(funAdd5, [[true, 10, 11], [false, 1, 1], [10, true, 11], [1, false, 1], [2147483647, true, 2147483648],[true, 2147483647, 2147483648]]); +// Add: String Number Concat +var funAdd6 = (a, b) => { return a + b; } +warmup(funAdd6, [["x", 10, "x10"], [10, "bba", "10bba"], ["x", 1.2, "x1.2"], + [1.2, "bba", "1.2bba"]]); + // Sub Int32 var funSub1 = (a, b) => { return a - b; } warmup(funSub1, [[7, 0, 7], [7, 8, -1], [4294967295, 2, 4294967293], [0,0,0]]); // Sub Double var funSub2 = (a, b) => { return a - b; } warmup(funSub2, [[7.5, 0, 7.5], [7, 8.125, -1.125], [4294967295.3125, 2, 4294967293.3125], [NaN,10,NaN]]); @@ -205,9 +210,40 @@ warmup(funURsh4, [[54772703898, 11, 1578 for (var k=0; k < 30; k++) { A="01234567"; res ="" for (var i = 0; i < 8; ++i) { var v = A[7 - i]; res+=v; } assertEq(res, "76543210"); -} \ No newline at end of file +} + +// Begin OOM testing: +if (!('oomTest' in this)) + quit(); + +// Add: String Number Concat OOM test +var addOom = (a, b) => { return a + b; } + +function generate_digits(prefix, start) { + digits = [] + number = ""+start+".25"; + for (var i = 1; i < 7; i++) { + number = i + number; + digits.push([prefix, Number(number), prefix + number]); + } + return digits; +} + +// Trying to defeat dtoacache: Warm the IC with one set of digits, then actually oomTest +// using another set. +var warmup_digits = generate_digits("x", 1); +var test_digits = generate_digits("x", 2); + +function ot(digits) { + warmup(addOom, digits); +} + +// Ensure ICs are warmed +ot(warmup_digits); + +oomTest(() => { ot(test_digits); }); \ No newline at end of file
--- a/js/src/jit/BaselineCacheIRCompiler.cpp +++ b/js/src/jit/BaselineCacheIRCompiler.cpp @@ -2370,16 +2370,18 @@ ICCacheIR_Updated::Clone(JSContext* cx, bool BaselineCacheIRCompiler::emitCallStringConcatResult() { AutoOutputRegister output(*this); Register lhs = allocator.useRegister(masm, reader.stringOperandId()); Register rhs = allocator.useRegister(masm, reader.stringOperandId()); AutoScratchRegisterMaybeOutput scratch(allocator, masm, output); + allocator.discardStack(masm); + AutoStubFrame stubFrame(*this); stubFrame.enter(masm, scratch); masm.push(rhs); masm.push(lhs); if (!callVM(masm, ConcatStringsInfo)) { return false;
--- a/js/src/jit/BaselineInspector.cpp +++ b/js/src/jit/BaselineInspector.cpp @@ -387,16 +387,18 @@ ParseCacheIRStub(ICStub* stub) switch (reader.readOp()) { case CacheOp::LoadUndefinedResult: return MIRType::Undefined; case CacheOp::LoadBooleanResult: return MIRType::Boolean; case CacheOp::LoadStringResult: case CacheOp::CallStringConcatResult: case CacheOp::CallStringObjectConcatResult: + case CacheOp::CallInt32ToString: + case CacheOp::CallNumberToString: return MIRType::String; case CacheOp::DoubleAddResult: case CacheOp::DoubleSubResult: case CacheOp::DoubleMulResult: case CacheOp::DoubleDivResult: case CacheOp::DoubleModResult: case CacheOp::DoubleNegationResult: return MIRType::Double;
--- a/js/src/jit/CacheIR.cpp +++ b/js/src/jit/CacheIR.cpp @@ -5820,16 +5820,19 @@ BinaryArithIRGenerator::tryAttachStub() return true; } // String x Object if (tryAttachStringObjectConcat()) { return true; } + if (tryAttachStringNumberConcat()) + return true; + trackAttached(IRGenerator::NotAttached); return false; } bool BinaryArithIRGenerator::tryAttachBitwise() { @@ -6016,16 +6019,58 @@ BinaryArithIRGenerator::tryAttachInt32() default: MOZ_CRASH("Unhandled op in tryAttachInt32"); } writer.returnFromIC(); return true; } + +bool +BinaryArithIRGenerator::tryAttachStringNumberConcat() +{ + // Only Addition + if (op_ != JSOP_ADD) + return false; + + if (!(lhs_.isString() && rhs_.isNumber()) && + !(lhs_.isNumber() && rhs_.isString())) + { + return false; + } + + ValOperandId lhsId(writer.setInputOperandId(0)); + ValOperandId rhsId(writer.setInputOperandId(1)); + + auto guardToString = [&](ValOperandId id, HandleValue v) { + if (v.isString()) { + return writer.guardIsString(id); + } + if (v.isInt32()) { + Int32OperandId intId = writer.guardIsInt32(id); + return writer.callInt32ToString(intId); + } + // At this point we are creating an IC that will handle + // both Int32 and Double cases. + MOZ_ASSERT(v.isNumber()); + writer.guardIsNumber(id); + return writer.callNumberToString(id); + }; + + StringOperandId lhsStrId = guardToString(lhsId, lhs_); + StringOperandId rhsStrId = guardToString(rhsId, rhs_); + + writer.callStringConcatResult(lhsStrId, rhsStrId); + + writer.returnFromIC(); + trackAttached("BinaryArith.StringNumberConcat"); + return true; +} + bool BinaryArithIRGenerator::tryAttachStringConcat() { // Only Addition if (op_ != JSOP_ADD) { return false; }
--- a/js/src/jit/CacheIR.h +++ b/js/src/jit/CacheIR.h @@ -261,16 +261,18 @@ extern const char* const CacheKindNames[ _(ArrayPush) \ _(ArrayJoinResult) \ _(StoreTypedElement) \ _(CallNativeSetter) \ _(CallScriptedSetter) \ _(CallSetArrayLength) \ _(CallProxySet) \ _(CallProxySetByValue) \ + _(CallInt32ToString) \ + _(CallNumberToString) \ \ /* The *Result ops load a value into the cache's result register. */ \ _(LoadFixedSlotResult) \ _(LoadDynamicSlotResult) \ _(LoadUnboxedPropertyResult) \ _(LoadTypedObjectResult) \ _(LoadDenseElementResult) \ _(LoadDenseElementHoleResult) \ @@ -1028,16 +1030,28 @@ class MOZ_RAII CacheIRWriter : public JS buffer_.writeByte(uint32_t(strict)); } void callProxySetByValue(ObjOperandId obj, ValOperandId id, ValOperandId rhs, bool strict) { writeOpWithOperandId(CacheOp::CallProxySetByValue, obj); writeOperandId(id); writeOperandId(rhs); buffer_.writeByte(uint32_t(strict)); } + StringOperandId callInt32ToString(Int32OperandId id) { + StringOperandId res(nextOperandId_++); + writeOpWithOperandId(CacheOp::CallInt32ToString, id); + writeOperandId(res); + return res; + } + StringOperandId callNumberToString(ValOperandId id) { + StringOperandId res(nextOperandId_++); + writeOpWithOperandId(CacheOp::CallNumberToString, id); + writeOperandId(res); + return res; + } void megamorphicLoadSlotResult(ObjOperandId obj, PropertyName* name, bool handleMissing) { writeOpWithOperandId(CacheOp::MegamorphicLoadSlotResult, obj); addStubField(uintptr_t(name), StubField::Type::String); buffer_.writeByte(uint32_t(handleMissing)); } void megamorphicLoadSlotByValueResult(ObjOperandId obj, ValOperandId id, bool handleMissing) { writeOpWithOperandId(CacheOp::MegamorphicLoadSlotByValueResult, obj); @@ -1970,16 +1984,17 @@ class MOZ_RAII BinaryArithIRGenerator : void trackAttached(const char* name); bool tryAttachInt32(); bool tryAttachDouble(); bool tryAttachBitwise(); bool tryAttachStringConcat(); bool tryAttachStringObjectConcat(); + bool tryAttachStringNumberConcat(); public: BinaryArithIRGenerator(JSContext* cx, HandleScript, jsbytecode* pc, ICState::Mode, JSOp op, HandleValue lhs, HandleValue rhs, HandleValue res); bool tryAttachStub(); };
--- a/js/src/jit/CacheIRCompiler.cpp +++ b/js/src/jit/CacheIRCompiler.cpp @@ -4036,16 +4036,73 @@ bool CacheIRCompiler::emitLoadObject() { Register reg = allocator.defineRegister(masm, reader.objOperandId()); StubFieldOffset obj(reader.stubOffset(), StubField::Type::JSObject); emitLoadStubField(obj, reg); return true; } +bool +CacheIRCompiler::emitCallInt32ToString() { + Register input = allocator.useRegister(masm, reader.int32OperandId()); + Register result = allocator.defineRegister(masm, reader.stringOperandId()); + + FailurePath* failure; + if (!addFailurePath(&failure)) + return false; + + LiveRegisterSet volatileRegs(GeneralRegisterSet::Volatile(), liveVolatileFloatRegs()); + volatileRegs.takeUnchecked(result); + masm.PushRegsInMask(volatileRegs); + + masm.setupUnalignedABICall(result); + masm.loadJSContext(result); + masm.passABIArg(result); + masm.passABIArg(input); + masm.callWithABI(JS_FUNC_TO_DATA_PTR(void*, (js::Int32ToStringHelper))); + + masm.mov(ReturnReg, result); + masm.PopRegsInMask(volatileRegs); + + masm.branchPtr(Assembler::Equal, result, ImmPtr(0), failure->label()); + return true; +} + +bool +CacheIRCompiler::emitCallNumberToString() { + // Float register must be preserved. The BinaryArith ICs use + // the fact that baseline has them available, as well as fixed temps on + // LBinaryCache. + allocator.ensureDoubleRegister(masm, reader.valOperandId(), FloatReg0); + Register result = allocator.defineRegister(masm, reader.stringOperandId()); + + FailurePath* failure; + if (!addFailurePath(&failure)) + return false; + + LiveRegisterSet volatileRegs(GeneralRegisterSet::Volatile(), liveVolatileFloatRegs()); + volatileRegs.takeUnchecked(result); + volatileRegs.addUnchecked(FloatReg0); + masm.PushRegsInMask(volatileRegs); + + masm.setupUnalignedABICall(result); + masm.loadJSContext(result); + masm.passABIArg(result); + masm.passABIArg(FloatReg0, MoveOp::DOUBLE); + masm.callWithABI(JS_FUNC_TO_DATA_PTR(void*, (js::NumberToStringHelper))); + + masm.mov(ReturnReg, result); + masm.PopRegsInMask(volatileRegs); + + masm.branchPtr(Assembler::Equal, result, ImmPtr(0), failure->label()); + return true; +} + + void js::jit::LoadTypedThingData(MacroAssembler& masm, TypedThingLayout layout, Register obj, Register result) { switch (layout) { case Layout_TypedArray: masm.loadPtr(Address(obj, TypedArrayObject::dataOffset()), result); break; case Layout_OutlineTypedObject:
--- a/js/src/jit/CacheIRCompiler.h +++ b/js/src/jit/CacheIRCompiler.h @@ -107,16 +107,18 @@ namespace jit { _(ArrayJoinResult) \ _(CallPrintString) \ _(Breakpoint) \ _(MegamorphicLoadSlotResult) \ _(MegamorphicLoadSlotByValueResult) \ _(MegamorphicStoreSlot) \ _(MegamorphicHasPropResult) \ _(CallObjectHasSparseElementResult) \ + _(CallInt32ToString) \ + _(CallNumberToString) \ _(WrapResult) // Represents a Value on the Baseline frame's expression stack. Slot 0 is the // value on top of the stack (the most recently pushed value), slot 1 is the // value pushed before that, etc. class BaselineFrameSlot { uint32_t slot_;
--- a/js/src/js.msg +++ b/js/src/js.msg @@ -151,17 +151,17 @@ MSG_DEF(JSMSG_CALLER_IS_STRICT, 0 MSG_DEF(JSMSG_DEPRECATED_USAGE, 1, JSEXN_REFERENCEERR, "deprecated {0} usage") MSG_DEF(JSMSG_NOT_SCRIPTED_FUNCTION, 1, JSEXN_TYPEERR, "{0} is not a scripted function") MSG_DEF(JSMSG_NO_REST_NAME, 0, JSEXN_SYNTAXERR, "no parameter name after ...") MSG_DEF(JSMSG_PARAMETER_AFTER_REST, 0, JSEXN_SYNTAXERR, "parameter after rest parameter") MSG_DEF(JSMSG_TOO_MANY_ARGUMENTS, 0, JSEXN_RANGEERR, "too many arguments provided for a function call") // CSP MSG_DEF(JSMSG_CSP_BLOCKED_EVAL, 0, JSEXN_EVALERR, "call to eval() blocked by CSP") -MSG_DEF(JSMSG_CSP_BLOCKED_FUNCTION, 0, JSEXN_ERR, "call to Function() blocked by CSP") +MSG_DEF(JSMSG_CSP_BLOCKED_FUNCTION, 0, JSEXN_EVALERR, "call to Function() blocked by CSP") // Wrappers MSG_DEF(JSMSG_ACCESSOR_DEF_DENIED, 1, JSEXN_ERR, "Permission denied to define accessor property {0}") MSG_DEF(JSMSG_DEAD_OBJECT, 0, JSEXN_TYPEERR, "can't access dead object") MSG_DEF(JSMSG_OBJECT_ACCESS_DENIED, 0, JSEXN_ERR, "Permission denied to access object") MSG_DEF(JSMSG_PROPERTY_ACCESS_DENIED, 1, JSEXN_ERR, "Permission denied to access property {0}") // JSAPI-only (Not thrown as JS exceptions)
--- a/js/src/jsnum.cpp +++ b/js/src/jsnum.cpp @@ -714,16 +714,27 @@ js::Int32ToString(JSContext* cx, int32_t } template JSFlatString* js::Int32ToString<CanGC>(JSContext* cx, int32_t si); template JSFlatString* js::Int32ToString<NoGC>(JSContext* cx, int32_t si); +JSFlatString* +js::Int32ToStringHelper(JSContext* cx, int32_t si) +{ + AutoUnsafeCallWithABI unsafe; + JSFlatString* res = Int32ToString<NoGC>(cx, si); + if (!res) { + cx->recoverFromOutOfMemory(); + } + return res; +} + JSAtom* js::Int32ToAtom(JSContext* cx, int32_t si) { if (JSFlatString* str = LookupInt32ToString(cx, si)) { return js::AtomizeString(cx, str); } char buffer[JSFatInlineString::MAX_LENGTH_TWO_BYTE + 1]; @@ -1513,16 +1524,27 @@ js::NumberToString(JSContext* cx, double } template JSString* js::NumberToString<CanGC>(JSContext* cx, double d); template JSString* js::NumberToString<NoGC>(JSContext* cx, double d); +JSString* +js::NumberToStringHelper(JSContext* cx, double d) +{ + AutoUnsafeCallWithABI unsafe; + JSString* res = NumberToString<NoGC>(cx, d); + if (!res) { + cx->recoverFromOutOfMemory(); + } + return res; +} + JSAtom* js::NumberToAtom(JSContext* cx, double d) { int32_t si; if (mozilla::NumberEqualsInt32(d, &si)) { return Int32ToAtom(cx, si); }
--- a/js/src/jsnum.h +++ b/js/src/jsnum.h @@ -55,23 +55,29 @@ InitNumberClass(JSContext* cx, Handle<Gl * When base == 10, this function implements ToString() as specified by * ECMA-262-5 section 9.8.1; but note that it handles integers specially for * performance. See also js::NumberToCString(). */ template <AllowGC allowGC> extern JSString* NumberToString(JSContext* cx, double d); +extern JSString* +NumberToStringHelper(JSContext* cx, double d); + extern JSAtom* NumberToAtom(JSContext* cx, double d); template <AllowGC allowGC> extern JSFlatString* Int32ToString(JSContext* cx, int32_t i); +extern JSFlatString* +Int32ToStringHelper(JSContext* cx, int32_t i); + extern JSAtom* Int32ToAtom(JSContext* cx, int32_t si); // ES6 15.7.3.12 extern bool IsInteger(const Value& val); /*
--- a/js/xpconnect/idl/xpccomponents.idl +++ b/js/xpconnect/idl/xpccomponents.idl @@ -6,16 +6,17 @@ #include "nsISupports.idl" %{C++ #include "jspubtd.h" %} interface xpcIJSWeakReference; interface nsIClassInfo; +interface nsICommandParams; interface nsIComponentManager; interface nsICycleCollectorListener; interface nsIEditorSpellCheck; interface nsIFile; interface nsIURI; interface nsIJSCID; interface nsIJSIID; interface nsIPrincipal; @@ -718,16 +719,19 @@ interface nsIXPCComponents_Utils : nsISu /* Give a directive to the record/replay system. */ void recordReplayDirective(in long directive); /* Create a spellchecker object. */ nsIEditorSpellCheck createSpellChecker(); /* Create a commandline object. */ nsISupports createCommandLine(); + + /* Create a command params object. */ + nsICommandParams createCommandParams(); }; /** * Interface for the 'Components' object. * * The first interface contains things that are available to non-chrome XBL code * that runs in a scope with an ExpandedPrincipal. The second interface * includes members that are only exposed to chrome.
--- a/js/xpconnect/src/Sandbox.cpp +++ b/js/xpconnect/src/Sandbox.cpp @@ -1873,23 +1873,16 @@ nsXPCComponents_utils_Sandbox::CallOrCon } rv = CreateSandboxObject(cx, args.rval(), prinOrSop, options); if (NS_FAILED(rv)) { return ThrowAndFail(rv, cx, _retval); } - // We have this crazy behavior where wantXrays=false also implies that the - // returned sandbox is implicitly waived. We've stopped advertising it, but - // keep supporting it for now. - if (!options.wantXrays && !xpc::WrapperFactory::WaiveXrayAndWrap(cx, args.rval())) { - return NS_ERROR_UNEXPECTED; - } - *_retval = true; return NS_OK; } nsresult xpc::EvalInSandbox(JSContext* cx, HandleObject sandboxArg, const nsAString& source, const nsACString& filename, int32_t lineNo, MutableHandleValue rval)
--- a/js/xpconnect/src/XPCComponents.cpp +++ b/js/xpconnect/src/XPCComponents.cpp @@ -38,16 +38,17 @@ #include "nsIScriptError.h" #include "nsISimpleEnumerator.h" #include "nsPIDOMWindow.h" #include "nsGlobalWindow.h" #include "nsScriptError.h" #include "GeckoProfiler.h" #include "mozilla/EditorSpellCheck.h" #include "nsCommandLine.h" +#include "nsCommandParams.h" using namespace mozilla; using namespace JS; using namespace js; using namespace xpc; using mozilla::dom::Exception; /***************************************************************************/ @@ -3226,16 +3227,25 @@ NS_IMETHODIMP nsXPCComponents_Utils::CreateCommandLine(nsISupports** aCommandLine) { NS_ENSURE_ARG_POINTER(aCommandLine); nsCOMPtr<nsISupports> commandLine = new nsCommandLine(); commandLine.forget(aCommandLine); return NS_OK; } +NS_IMETHODIMP +nsXPCComponents_Utils::CreateCommandParams(nsICommandParams** aCommandParams) +{ + NS_ENSURE_ARG_POINTER(aCommandParams); + nsCOMPtr<nsICommandParams> commandParams = new nsCommandParams(); + commandParams.forget(aCommandParams); + return NS_OK; +} + /***************************************************************************/ /***************************************************************************/ /***************************************************************************/ nsXPCComponentsBase::nsXPCComponentsBase(XPCWrappedNativeScope* aScope) : mScope(aScope) {
--- a/js/xpconnect/tests/chrome/test_evalInSandbox.xul +++ b/js/xpconnect/tests/chrome/test_evalInSandbox.xul @@ -72,20 +72,24 @@ https://bugzilla.mozilla.org/show_bug.cg sandbox = new Cu.Sandbox(win, { sandboxPrototype: win, wantXrays: false } ); checkCrossOriginSandbox(sandbox); ok(Cu.evalInSandbox("('foo' in this.document);", sandbox), "can see expandos"); - ok(("foo" in Cu.evalInSandbox("this.document", sandbox)), - "must see expandos in wrappers returned from the sandbox"); + ok(!("foo" in Cu.evalInSandbox("this.document", sandbox)), + "must not see expandos in wrappers returned from the sandbox"); + ok(("foo" in Cu.waiveXrays(Cu.evalInSandbox("this.document", sandbox))), + "must see expandos in waived wrappers returned from the sandbox"); - ok(("foo" in sandbox.document), + ok(!("foo" in sandbox.document), + "must not see expandos in wrappers obtained from the sandbox"); + ok("foo" in Cu.waiveXrays(sandbox.document), "must see expandos in wrappers obtained from the sandbox"); testDone(); } function checkSameOrigin(ifr) { var win = ifr.contentWindow; var sandbox =
--- a/js/xpconnect/tests/unit/test_bug845862.js +++ b/js/xpconnect/tests/unit/test_bug845862.js @@ -1,11 +1,7 @@ function run_test() { - // We rely on the crazy "wantXrays:false also causes values return from the - // sandbox to be waived" behavior, because it's the simplest way to get - // waivers out of the sandbox (which has no native objects). :-( - var sb = new Cu.Sandbox('http://www.example.com', {wantXrays: false}); + var sb = new Cu.Sandbox('http://www.example.com'); Cu.evalInSandbox("this.foo = {}; Object.defineProperty(foo, 'bar', {get: function() {return {};}});", sb); - Assert.ok(sb.foo != XPCNativeWrapper(sb.foo), "sb.foo is waived"); - var desc = Object.getOwnPropertyDescriptor(sb.foo, 'bar'); + var desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(sb.foo), 'bar'); var b = desc.get(); Assert.ok(b != XPCNativeWrapper(b), "results from accessor descriptors are waived"); }
--- a/layout/base/MobileViewportManager.cpp +++ b/layout/base/MobileViewportManager.cpp @@ -113,17 +113,17 @@ void MobileViewportManager::ResolutionUpdated() { MVM_LOG("%p: resolution updated\n", this); if (!mPainted) { // Save the value, so our default zoom calculation // can take it into account later on. SetRestoreResolution(mPresShell->GetResolution()); } - RefreshSPCSPS(); + RefreshVisualViewportSize(); } NS_IMETHODIMP MobileViewportManager::HandleEvent(dom::Event* event) { nsAutoString type; event->GetType(type); @@ -280,18 +280,18 @@ MobileViewportManager::UpdateResolution( mPresShell->SetResolutionAndScaleTo(res.scale); } return ViewTargetAs<ScreenPixel>(cssToDev * res / ParentLayerToLayerScale(1), PixelCastJustification::ScreenIsParentLayerForRoot); } void -MobileViewportManager::UpdateSPCSPS(const ScreenIntSize& aDisplaySize, - const CSSToScreenScale& aZoom) +MobileViewportManager::UpdateVisualViewportSize(const ScreenIntSize& aDisplaySize, + const CSSToScreenScale& aZoom) { ScreenSize compositionSize(aDisplaySize); ScreenMargin scrollbars = LayoutDeviceMargin::FromAppUnits( nsLayoutUtils::ScrollbarAreaToExcludeFromCompositionBoundsFor( mPresShell->GetRootScrollFrame()), mPresShell->GetPresContext()->AppUnitsPerDevPixel()) // Scrollbars are not subject to resolution scaling, so LD pixels = @@ -327,35 +327,35 @@ MobileViewportManager::UpdateDisplayPort nsLayoutUtils::SetDisplayPortBaseIfNotSet(root->GetContent(), displayportBase); nsIScrollableFrame* scrollable = do_QueryFrame(root); nsLayoutUtils::CalculateAndSetDisplayPortMargins(scrollable, nsLayoutUtils::RepaintMode::DoNotRepaint); } } void -MobileViewportManager::RefreshSPCSPS() +MobileViewportManager::RefreshVisualViewportSize() { // This function is a subset of RefreshViewportSize, and only updates the - // SPCSPS. + // visual viewport size. if (!gfxPrefs::APZAllowZooming()) { return; } ScreenIntSize displaySize = ViewAs<ScreenPixel>( mDisplaySize, PixelCastJustification::LayoutDeviceIsScreenForBounds); CSSToLayoutDeviceScale cssToDev = mPresShell->GetPresContext()->CSSToDevPixelScale(); LayoutDeviceToLayerScale res(mPresShell->GetResolution()); CSSToScreenScale zoom = ViewTargetAs<ScreenPixel>(cssToDev * res / ParentLayerToLayerScale(1), PixelCastJustification::ScreenIsParentLayerForRoot); - UpdateSPCSPS(displaySize, zoom); + UpdateVisualViewportSize(displaySize, zoom); } void MobileViewportManager::RefreshViewportSize(bool aForceAdjustResolution) { // This function gets called by the various triggers that may result in a // change of the CSS viewport. In some of these cases (e.g. the meta-viewport // tag changes) we want to update the resolution and in others (e.g. the full @@ -409,17 +409,17 @@ MobileViewportManager::RefreshViewportSi // various APZ properties (the zoom and some things that might depend on it) MVM_LOG("%p: Updating properties because %d || %d\n", this, mIsFirstPaint, mMobileViewportSize != viewport); if (gfxPrefs::APZAllowZooming()) { CSSToScreenScale zoom = UpdateResolution(viewportInfo, displaySize, viewport, displayWidthChangeRatio); MVM_LOG("%p: New zoom is %f\n", this, zoom.scale); - UpdateSPCSPS(displaySize, zoom); + UpdateVisualViewportSize(displaySize, zoom); } if (gfxPlatform::AsyncPanZoomEnabled()) { UpdateDisplayPortMargins(); } CSSSize oldSize = mMobileViewportSize; // Update internal state.
--- a/layout/base/MobileViewportManager.h +++ b/layout/base/MobileViewportManager.h @@ -48,32 +48,32 @@ private: void SetRestoreResolution(float aResolution); public: /* Notify the MobileViewportManager that a reflow was requested in the * presShell.*/ void RequestReflow(); /* Notify the MobileViewportManager that the resolution on the presShell was - * updated, and the SPCSPS needs to be updated. */ + * updated, and the visual viewport size needs to be updated. */ void ResolutionUpdated(); private: ~MobileViewportManager(); /* Called to compute the initial viewport on page load or before-first-paint, * whichever happens first. */ void SetInitialViewport(); /* Main helper method to update the CSS viewport and any other properties that * need updating. */ void RefreshViewportSize(bool aForceAdjustResolution); - /* Secondary main helper method to update just the SPCSPS. */ - void RefreshSPCSPS(); + /* Secondary main helper method to update just the visual viewport size. */ + void RefreshVisualViewportSize(); /* Helper to clamp the given zoom by the min/max in the viewport info. */ mozilla::CSSToScreenScale ClampZoom(const mozilla::CSSToScreenScale& aZoom, const nsViewportInfo& aViewportInfo); /* Helper to update the given resolution according to changed display and viewport widths. */ mozilla::LayoutDeviceToLayerScale ScaleResolutionWithDisplayWidth(const mozilla::LayoutDeviceToLayerScale& aRes, @@ -82,19 +82,18 @@ private: const mozilla::CSSSize& aOldViewport); /* Updates the presShell resolution and returns the new zoom. */ mozilla::CSSToScreenScale UpdateResolution(const nsViewportInfo& aViewportInfo, const mozilla::ScreenIntSize& aDisplaySize, const mozilla::CSSSize& aViewport, const mozilla::Maybe<float>& aDisplayWidthChangeRatio); - /* Updates the scroll-position-clamping scrollport size */ - void UpdateSPCSPS(const mozilla::ScreenIntSize& aDisplaySize, - const mozilla::CSSToScreenScale& aZoom); + void UpdateVisualViewportSize(const mozilla::ScreenIntSize& aDisplaySize, + const mozilla::CSSToScreenScale& aZoom); /* Updates the displayport margins for the presShell's root scrollable frame */ void UpdateDisplayPortMargins(); nsCOMPtr<nsIDocument> mDocument; nsIPresShell* MOZ_NON_OWNING_REF mPresShell; // raw ref since the presShell owns this nsCOMPtr<mozilla::dom::EventTarget> mEventTarget; bool mIsFirstPaint;
--- a/layout/generic/nsBlockFrame.cpp +++ b/layout/generic/nsBlockFrame.cpp @@ -281,16 +281,22 @@ RecordReflowStatus(bool aChildIsBlock, c // Log updates to the status that yield different values if (record[index] != newS) { record[index] = newS; printf("record(%d): %02x %02x\n", index, record[0], record[1]); } } #endif +static nscoord +ResolveTextIndent(const nsStyleCoord& aStyle, nscoord aPercentageBasis) +{ + return nsLayoutUtils::ResolveToLength<false>(aStyle, aPercentageBasis); +} + NS_DECLARE_FRAME_PROPERTY_WITH_DTOR_NEVER_CALLED(OverflowLinesProperty, nsBlockFrame::FrameLines) NS_DECLARE_FRAME_PROPERTY_FRAMELIST(OverflowOutOfFlowsProperty) NS_DECLARE_FRAME_PROPERTY_FRAMELIST(PushedFloatProperty) NS_DECLARE_FRAME_PROPERTY_FRAMELIST(OutsideBulletProperty) NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(InsideBulletProperty, nsBulletFrame) NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(BlockEndEdgeOfChildrenProperty, nscoord) @@ -802,25 +808,18 @@ nsBlockFrame::GetMinISize(gfxContext *aR if (line->IsBlock()) { data.ForceBreak(); data.mCurrentLine = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, line->mFirstChild, nsLayoutUtils::MIN_ISIZE); data.ForceBreak(); } else { if (!curFrame->GetPrevContinuation() && line == curFrame->LinesBegin()) { - // Only add text-indent if it has no percentages; using a - // percentage basis of 0 unconditionally would give strange - // behavior for calc(10%-3px). - const nsStyleCoord &indent = StyleText()->mTextIndent; - if (indent.ConvertsToLength()) - data.mCurrentLine += indent.ComputeCoordPercentCalc(0); + data.mCurrentLine += ::ResolveTextIndent(StyleText()->mTextIndent, 0); } - // XXX Bug NNNNNN Should probably handle percentage text-indent. - data.mLine = &line; data.SetLineContainer(curFrame); nsIFrame *kid = line->mFirstChild; for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end; ++i, kid = kid->GetNextSibling()) { kid->AddInlineMinISize(aRenderingContext, &data); } } @@ -901,30 +900,23 @@ nsBlockFrame::GetPrefISize(gfxContext *a } data.ForceBreak(breakType); data.mCurrentLine = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, line->mFirstChild, nsLayoutUtils::PREF_ISIZE); data.ForceBreak(); } else { if (!curFrame->GetPrevContinuation() && line == curFrame->LinesBegin()) { - // Only add text-indent if it has no percentages; using a - // percentage basis of 0 unconditionally would give strange - // behavior for calc(10%-3px). - const nsStyleCoord &indent = StyleText()->mTextIndent; - if (indent.ConvertsToLength()) { - nscoord length = indent.ToLength(); - if (length != 0) { - data.mCurrentLine += length; - data.mLineIsEmpty = false; - } + nscoord indent = ::ResolveTextIndent(StyleText()->mTextIndent, 0); + data.mCurrentLine += indent; + // XXXmats should the test below be indent > 0? + if (indent != nscoord(0)) { + data.mLineIsEmpty = false; } } - // XXX Bug NNNNNN Should probably handle percentage text-indent. - data.mLine = &line; data.SetLineContainer(curFrame); nsIFrame *kid = line->mFirstChild; for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end; ++i, kid = kid->GetNextSibling()) { kid->AddInlinePrefISize(aRenderingContext, &data); } } @@ -979,26 +971,18 @@ nsBlockFrame::GetPrefWidthTightBounds(gf rv = line->mFirstChild->GetPrefWidthTightBounds(aRenderingContext, &childX, &childXMost); NS_ENSURE_SUCCESS(rv, rv); *aX = std::min(*aX, childX); *aXMost = std::max(*aXMost, childXMost); } else { if (!curFrame->GetPrevContinuation() && line == curFrame->LinesBegin()) { - // Only add text-indent if it has no percentages; using a - // percentage basis of 0 unconditionally would give strange - // behavior for calc(10%-3px). - const nsStyleCoord &indent = StyleText()->mTextIndent; - if (indent.ConvertsToLength()) { - data.mCurrentLine += indent.ComputeCoordPercentCalc(0); - } + data.mCurrentLine += ::ResolveTextIndent(StyleText()->mTextIndent, 0); } - // XXX Bug NNNNNN Should probably handle percentage text-indent. - data.mLine = &line; data.SetLineContainer(curFrame); nsIFrame *kid = line->mFirstChild; for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end; ++i, kid = kid->GetNextSibling()) { rv = kid->GetPrefWidthTightBounds(aRenderingContext, &childX, &childXMost); NS_ENSURE_SUCCESS(rv, rv);
--- a/layout/reftests/w3c-css/submitted/values3/calc-text-indent-intrinsic-1-ref.html +++ b/layout/reftests/w3c-css/submitted/values3/calc-text-indent-intrinsic-1-ref.html @@ -7,16 +7,16 @@ <style type="text/css"> body > div { margin: 0 0 1px 0; background: blue; color: white; height: 5px } </style> </head> <body> -<div style="width: 10px"></div> +<div style="width: 7px"></div> <div style="width: 57px"></div> -<div style="width: 10px"></div> +<div style="width: 60px"></div> <div style="width: 10px"></div> <div style="width: 60px"></div> <div style="width: 10px"></div> </body> </html>
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4603,16 +4603,20 @@ pref("toolkit.zoomManager.zoomValues", " // before it starts to discard already displayed frames and redecode them as // necessary. pref("image.animated.decode-on-demand.threshold-kb", 20480); // The minimum number of frames we want to have buffered ahead of an // animation's currently displayed frame. pref("image.animated.decode-on-demand.batch-size", 6); +// Whether we should generate full frames at decode time or partial frames which +// are combined at display time (historical behavior and default). +pref("image.animated.generate-full-frames", false); + // Resume an animated image from the last displayed frame rather than // advancing when out of view. pref("image.animated.resume-from-last-displayed", true); // Maximum number of surfaces for an image before entering "factor of 2" mode. // This in addition to the number of "native" sizes of an image. A native size // is a size for which we can decode a frame without up or downscaling. Most // images only have 1, but some (i.e. ICOs) may have multiple frames for the
--- a/netwerk/dns/nsEffectiveTLDService.cpp +++ b/netwerk/dns/nsEffectiveTLDService.cpp @@ -5,16 +5,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // This service reads a file of rules describing TLD-like domain names. For a // complete description of the expected file format and parsing rules, see // http://wiki.mozilla.org/Gecko:Effective_TLD_Service #include "mozilla/ArrayUtils.h" #include "mozilla/HashFunctions.h" +#include "mozilla/Maybe.h" #include "mozilla/MemoryReporting.h" #include "MainThreadUtils.h" #include "nsEffectiveTLDService.h" #include "nsIIDNService.h" #include "nsNetUtil.h" #include "prnetdb.h" #include "nsIURI.h" @@ -209,27 +210,30 @@ nsEffectiveTLDService::GetBaseDomainInte // Check if we're dealing with an IPv4/IPv6 hostname, and return PRNetAddr addr; PRStatus result = PR_StringToNetAddr(aHostname.get(), &addr); if (result == PR_SUCCESS) return NS_ERROR_HOST_IS_IP_ADDRESS; // Lookup in the cache if this is a normal query. This is restricted to // main thread-only as the cache is not thread-safe. - TLDCacheEntry* entry = nullptr; + Maybe<TldCache::Entry> entry; if (aAdditionalParts == 1 && NS_IsMainThread()) { - if (LookupForAdd(aHostname, &entry)) { + auto p = mMruTable.Lookup(aHostname); + if (p) { // There was a match, just return the cached value. - aBaseDomain = entry->mBaseDomain; + aBaseDomain = p.Data().mBaseDomain; if (trailingDot) { aBaseDomain.Append('.'); } return NS_OK; } + + entry = Some(p); } // Walk up the domain tree, most specific to least specific, // looking for matches at each level. Note that a given level may // have multiple attributes (e.g. IsWild() and IsNormal()). const char *prevDomain = nullptr; const char *currDomain = aHostname.get(); const char *nextDot = strchr(currDomain, '.'); @@ -305,18 +309,17 @@ nsEffectiveTLDService::GetBaseDomainInte if (aAdditionalParts != 0) return NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS; aBaseDomain = Substring(iter, end); // Update the MRU table if in use. if (entry) { - entry->mHost = aHostname; - entry->mBaseDomain = aBaseDomain; + entry->Set(TLDCacheEntry{aHostname, nsCString(aBaseDomain)}); } // add on the trailing dot, if applicable if (trailingDot) aBaseDomain.Append('.'); return NS_OK; } @@ -332,26 +335,16 @@ nsEffectiveTLDService::NormalizeHostname if (NS_FAILED(rv)) return rv; } ToLowerCase(aHostname); return NS_OK; } -bool -nsEffectiveTLDService::LookupForAdd(const nsACString& aHost, TLDCacheEntry** aEntry) -{ - MOZ_ASSERT(NS_IsMainThread()); - - const uint32_t hash = HashString(aHost.BeginReading(), aHost.Length()); - *aEntry = &mMruTable[hash % kTableSize]; - return (*aEntry)->mHost == aHost; -} - NS_IMETHODIMP nsEffectiveTLDService::HasRootDomain(const nsACString& aInput, const nsACString& aHost, bool* aResult) { if (NS_WARN_IF(!aResult)) { return NS_ERROR_FAILURE; }
--- a/netwerk/dns/nsEffectiveTLDService.h +++ b/netwerk/dns/nsEffectiveTLDService.h @@ -3,22 +3,24 @@ * 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 EffectiveTLDService_h #define EffectiveTLDService_h #include "nsIEffectiveTLDService.h" +#include "nsHashKeys.h" #include "nsIMemoryReporter.h" #include "nsString.h" #include "nsCOMPtr.h" #include "mozilla/Attributes.h" #include "mozilla/Dafsa.h" #include "mozilla/MemoryReporting.h" +#include "mozilla/MruCache.h" class nsIIDNService; class nsEffectiveTLDService final : public nsIEffectiveTLDService , public nsIMemoryReporter { public: @@ -50,27 +52,25 @@ private: // We use a small most recently used cache to compensate for DAFSA lookups // being slightly slower than a binary search on a larger table of strings. // // We first check the cache for a matching result and avoid a DAFSA lookup // if a match is found. Otherwise we lookup the domain in the DAFSA and then // cache the result. During standard browsing the same domains are repeatedly // fed into |GetBaseDomainInternal| so this ends up being an effective // mitigation getting about a 99% hit rate with four tabs open. - // - // A size of 31 is used rather than a more logical power-of-two such as 32 - // since it is a prime number and provides fewer collisions when when used - // with our hash algorithms. - static const uint32_t kTableSize = 31; - TLDCacheEntry mMruTable[kTableSize]; + struct TldCache : + public mozilla::MruCache<nsACString, TLDCacheEntry, TldCache> + { + static mozilla::HashNumber Hash(const nsACString& aKey) + { + return mozilla::HashString(aKey); + } + static bool Match(const nsACString& aKey, const TLDCacheEntry& aVal) + { + return aKey == aVal.mHost; + } + }; - /** - * Performs a lookup on the MRU table and provides a pointer to the hash - * entry that matched or should be used for adding this host. - * - * @param aHost The host to lookup. - * @param aEntry Out param, the entry in the MRU table to use. - * @return True if a match was found, false if there was a miss. - */ - inline bool LookupForAdd(const nsACString& aHost, TLDCacheEntry** aEntry); + TldCache mMruTable; }; #endif // EffectiveTLDService_h
--- a/security/certverifier/moz.build +++ b/security/certverifier/moz.build @@ -55,23 +55,21 @@ DIRS += [ ] TEST_DIRS += [ 'tests/gtest', ] if CONFIG['CC_TYPE'] == 'clang-cl': # -Wall on clang-cl maps to -Weverything, which turns on way too - # much, so we're using -W4 instead, which is mapped to clang's - # -Wall -Wextra. - CXXFLAGS += ['-W4'] -else: - CXXFLAGS += ['-Wall'] + # much, so we're passing through -Wall using -Xclang. + CXXFLAGS += ['-Xclang'] +CXXFLAGS += ['-Wall'] -if CONFIG['CC_TYPE'] in ('msvc', 'clang-cl'): +if CONFIG['CC_TYPE'] == 'msvc': # -Wall with Visual C++ enables too many problematic warnings CXXFLAGS += [ '-wd4324', # structure was padded due to __declspec(align()) '-wd4355', # 'this' used in base member initializer list '-wd4464', # relative include path contains '..' '-wd4480', # nonstandard extension used: specifying underlying type for # enum 'enum' '-wd4481', # nonstandard extension used: override specifier 'keyword' @@ -93,16 +91,23 @@ if CONFIG['CC_TYPE'] in ('msvc', 'clang- # interpreted as a digraph anyway, we can disable the # warning.) '-wd4640', # construction of local static object is not thread-safe '-wd4710', # 'function': function not inlined '-wd4711', # function 'function' selected for inline expansion '-wd4820', # 'bytes' bytes padding added after construct 'member_name' ] + # Disable Spectre diagnostics only if optimization is disabled. + if not CONFIG['MOZ_OPTIMIZE']: + CXXFLAGS += [ + '-wd5045', # Compiler will insert Spectre mitigation for memory + # load if /Qspectre switch specified + ] + # MSVC 2010's headers trigger these CXXFLAGS += [ '-wd4548', # expression before comma has no effect; ... '-wd4668', # 'symbol' is not defined as a preprocessor macro... '-wd4987', # nonstandard extension used ] # MSVC 2015 triggers these @@ -124,17 +129,17 @@ if CONFIG['CC_TYPE'] in ('msvc', 'clang- # Gecko headers aren't warning-free enough for us to enable these warnings CXXFLAGS += [ '-wd4100', # 'symbol' : unreferenced formal parameter '-wd4127', # conditional expression is constant '-wd4946', # reinterpret_cast used between related types ] -if CONFIG['CC_TYPE'] in ('clang', 'gcc'): +if CONFIG['CC_TYPE'] in ('clang', 'clang-cl', 'gcc'): CXXFLAGS += [ '-Wextra', '-Wunreachable-code', ] # Gecko headers aren't warning-free enough for us to enable these warnings. CXXFLAGS += [ '-Wno-unused-parameter',
--- a/security/pkix/test/gtest/moz.build +++ b/security/pkix/test/gtest/moz.build @@ -65,11 +65,17 @@ elif CONFIG['CC_TYPE'] == 'msvc': '-wd4625', # copy constructor could not be generated. '-wd4626', # assugment operator could not be generated. '-wd4640', # construction of local static object is not thread safe. # This is intended as a temporary hack to support building with VS2015. # declaration of '*' hides class member '-wd4458', ] + # Disable Spectre diagnostics only if optimization is disabled. + if not CONFIG['MOZ_OPTIMIZE']: + CXXFLAGS += [ + '-wd5045', # Compiler will insert Spectre mitigation for memory load if + # /Qspectre switch specified + ] if CONFIG['CC_TYPE'] == 'clang-cl': AllowCompilerWarnings() # workaround for bug 1090497
--- a/testing/marionette/evaluate.js +++ b/testing/marionette/evaluate.js @@ -448,17 +448,18 @@ sandbox.create = function(window, princi * @return {Sandbox} * The created sandbox. */ sandbox.createMutable = function(window) { let opts = { wantComponents: false, wantXrays: false, }; - return sandbox.create(window, null, opts); + // Note: We waive Xrays here to match potentially-accidental old behavior. + return Cu.waiveXrays(sandbox.create(window, null, opts)); }; sandbox.createSystemPrincipal = function(window) { let principal = Cc["@mozilla.org/systemprincipal;1"] .createInstance(Ci.nsIPrincipal); return sandbox.create(window, principal); };
--- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py +++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py @@ -61,17 +61,19 @@ def get_windows_version(): # get system information info = {'os': unknown, 'processor': unknown, 'version': unknown, 'os_version': unknown, 'bits': unknown, 'has_sandbox': unknown, - 'webrender': bool(os.environ.get("MOZ_WEBRENDER", False))} + 'webrender': bool(os.environ.get("MOZ_WEBRENDER", False)), + 'automation': bool(os.environ.get("MOZ_AUTOMATION", False)), + } (system, node, release, version, machine, processor) = platform.uname() (bits, linkage) = platform.architecture() # get os information and related data if system in ["Microsoft", "Windows"]: info['os'] = 'win' # There is a Python bug on Windows to determine platform values # http://bugs.python.org/issue7860
--- a/testing/mozbase/mozrunner/tests/manifest.ini +++ b/testing/mozbase/mozrunner/tests/manifest.ini @@ -1,10 +1,12 @@ [DEFAULT] subsuite = mozbase, os == "linux" -skip-if = python == 3 +# We skip these tests in automated Windows builds because they trigger crashes +# in sh.exe; see bug 1489277. +skip-if = python == 3 || (automation && os == "win") [test_crash.py] [test_interactive.py] [test_start.py] [test_states.py] [test_stop.py] [test_threads.py] [test_wait.py]
--- a/testing/web-platform/meta/content-security-policy/inside-worker/dedicated-script.html.ini +++ b/testing/web-platform/meta/content-security-policy/inside-worker/dedicated-script.html.ini @@ -1,35 +1,18 @@ [dedicated-script.html] expected: ERROR - [`eval()` blocked in blob:] - expected: FAIL - - [`setTimeout([string\])` blocked in blob:] - expected: FAIL [Cross-origin `importScripts()` blocked in http:] expected: FAIL [Cross-origin `importScripts()` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20*)] expected: FAIL [Cross-origin `importScripts()` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20*)] expected: FAIL [`eval()` blocked in http:] expected: FAIL [`setTimeout([string\])` blocked in http:] expected: FAIL - [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20*)] - expected: FAIL - - [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20*)] - expected: FAIL - - [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20*)] - expected: FAIL - - [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20*)] - expected: FAIL -
deleted file mode 100644 --- a/testing/web-platform/meta/content-security-policy/inside-worker/shared-script.html.ini +++ /dev/null @@ -1,13 +0,0 @@ -[shared-script.html] - [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20%27self%27] - expected: FAIL - - [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,script-src%20%27self%27] - expected: FAIL - - [`eval()` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20%27self%27] - expected: FAIL - - [`setTimeout([string\])` blocked in http:?pipe=sub|header(Content-Security-Policy,default-src%20%27self%27] - expected: FAIL -
deleted file mode 100644 --- a/testing/web-platform/meta/content-security-policy/script-src/script-src-1_4_2.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[script-src-1_4_2.html] - [Unsafe eval ran in Function() constructor.] - expected: FAIL -
deleted file mode 100644 --- a/testing/web-platform/meta/content-security-policy/script-src/worker-set-timeout-blocked.sub.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[worker-set-timeout-blocked.sub.html] - [Expecting alerts: ["setTimeout blocked"\]] - expected: FAIL -
new file mode 100644 --- /dev/null +++ b/testing/web-platform/meta/css/vendor-imports/mozilla/mozilla-central-reftests/values3/calc-text-indent-intrinsic-1.html.ini @@ -0,0 +1,2 @@ +[calc-text-indent-intrinsic-1.html] + expected: FAIL
--- a/toolkit/actors/ControllersChild.jsm +++ b/toolkit/actors/ControllersChild.jsm @@ -14,18 +14,17 @@ class ControllersChild extends ActorChil case "ControllerCommands:Do": if (this.docShell.isCommandEnabled(message.data)) this.docShell.doCommand(message.data); break; case "ControllerCommands:DoWithParams": var data = message.data; if (this.docShell.isCommandEnabled(data.cmd)) { - var params = Cc["@mozilla.org/embedcomp/command-params;1"]. - createInstance(Ci.nsICommandParams); + var params = Cu.createCommandParams(); for (var name in data.params) { var value = data.params[name]; if (value.type == "long") { params.setLongValue(name, parseInt(value.value)); } else { throw Cr.NS_ERROR_NOT_IMPLEMENTED; } }
--- a/toolkit/components/antitracking/AntiTrackingCommon.cpp +++ b/toolkit/components/antitracking/AntiTrackingCommon.cpp @@ -25,16 +25,17 @@ #include "nsIPrincipal.h" #include "nsIScriptError.h" #include "nsIURI.h" #include "nsIURL.h" #include "nsIWebProgressListener.h" #include "nsNetUtil.h" #include "nsPIDOMWindow.h" #include "nsScriptSecurityManager.h" +#include "nsSandboxFlags.h" #include "prtime.h" #define ANTITRACKING_PERM_KEY "3rdPartyStorage" using namespace mozilla; using mozilla::dom::ContentChild; static LazyLogModule gAntiTrackingLog("AntiTracking"); @@ -59,16 +60,23 @@ namespace { bool GetParentPrincipalAndTrackingOrigin(nsGlobalWindowInner* a3rdPartyTrackingWindow, nsIPrincipal** aTopLevelStoragePrincipal, nsACString& aTrackingOrigin) { MOZ_ASSERT(nsContentUtils::IsTrackingResourceWindow(a3rdPartyTrackingWindow)); + nsIDocument* doc = a3rdPartyTrackingWindow->GetDocument(); + // Make sure storage access isn't disabled + if (doc && ((doc->GetSandboxFlags() & SANDBOXED_STORAGE_ACCESS) != 0 || + nsContentUtils::IsInPrivateBrowsing(doc))) { + return false; + } + // Now we need the principal and the origin of the parent window. nsCOMPtr<nsIPrincipal> topLevelStoragePrincipal = a3rdPartyTrackingWindow->GetTopLevelStorageAreaPrincipal(); if (NS_WARN_IF(!topLevelStoragePrincipal)) { return false; } // Let's take the principal and the origin of the tracker.
--- a/toolkit/components/antitracking/test/browser/browser.ini +++ b/toolkit/components/antitracking/test/browser/browser.ini @@ -33,8 +33,11 @@ support-files = server.sjs [browser_onBeforeRequestNotificationForTrackingResources.js] [browser_onModifyRequestNotificationForTrackingResources.js] [browser_permissionInNormalWindows.js] [browser_permissionInPrivateWindows.js] [browser_subResources.js] support-files = subResources.sjs [browser_script.js] support-files = tracker.js +[browser_storageAccessPrivateWindow.js] +[browser_storageAccessSandboxed.js] +[browser_storageAccessWithHeuristics.js]
--- a/toolkit/components/antitracking/test/browser/browser_blockingCookies.js +++ b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js @@ -38,8 +38,96 @@ AntiTracking.runTest("Set/Get Cookies", }, // Cleanup callback async _ => { await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); }); + +AntiTracking.runTest("Cookies and Storage Access API", + // Blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + await fetch("server.sjs").then(r => r.text()).then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs").then(r => r.text()).then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + is(document.cookie, "", "Still no cookies for me"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "I have the cookies!"); + }, + + // Non blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + is(document.cookie, "", "No cookies for me"); + + await fetch("server.sjs").then(r => r.text()).then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + await fetch("server.sjs").then(r => r.text()).then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + + ok(document.cookie.length, "Some Cookies for me"); + + hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + // For non-tracking windows, calling the API is a no-op + ok(document.cookie.length, "Still some Cookies for me"); + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js @@ -36,40 +36,245 @@ AntiTracking.runTest("IndexedDB in worke ok(blob, "Blob has been created"); let blobURL = URL.createObjectURL(blob); ok(blobURL, "Blob URL has been created"); let worker = new Worker(blobURL); ok(worker, "Worker has been created"); - await new Promise(resolve => { + await new Promise((resolve, reject) => { worker.onmessage = function(e) { - resolve(); + if (e) { + resolve(); + } else { + reject(); + } }; }); }, async _ => { function nonBlockCode() { indexedDB.open("test", "1"); - postMessage(false); + postMessage(true); } let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); ok(blob, "Blob has been created"); let blobURL = URL.createObjectURL(blob); ok(blobURL, "Blob URL has been created"); let worker = new Worker(blobURL); ok(worker, "Worker has been created"); - await new Promise(resolve => { + await new Promise((resolve, reject) => { worker.onmessage = function(e) { - resolve(); + if (e) { + resolve(); + } else { + reject(); + } }; }); }, async _ => { await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); }); + +AntiTracking.runTest("IndexedDB and Storage Access API", + // blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + try { + indexedDB.open("test", "1"); + ok(false, "IDB should be blocked"); + } catch (e) { + ok(true, "IDB should be blocked"); + is(e.name, "SecurityError", "We want a security error message."); + } + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + // non-blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false); + +AntiTracking.runTest("IndexedDB in workers and Storage Access API", + async _ => { + function blockCode() { + try { + indexedDB.open("test", "1"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + let blob = new Blob([blockCode.toString() + "; blockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + }, + async _ => { + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js +++ b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js @@ -33,19 +33,23 @@ AntiTracking.runTest("BroadcastChannel i ok(blob, "Blob has been created"); let blobURL = URL.createObjectURL(blob); ok(blobURL, "Blob URL has been created"); let worker = new Worker(blobURL); ok(worker, "Worker has been created"); - await new Promise(resolve => { + await new Promise((resolve, reject) => { worker.onmessage = function(e) { - resolve(); + if (e) { + resolve(); + } else { + reject(); + } }; }); }, async _ => { function nonBlockingCode() { new BroadcastChannel("hello"); postMessage(true); } @@ -54,19 +58,214 @@ AntiTracking.runTest("BroadcastChannel i ok(blob, "Blob has been created"); let blobURL = URL.createObjectURL(blob); ok(blobURL, "Blob URL has been created"); let worker = new Worker(blobURL); ok(worker, "Worker has been created"); - await new Promise(resolve => { + await new Promise((resolve, reject) => { worker.onmessage = function(e) { - resolve(); + if (e) { + resolve(); + } else { + reject(); + } }; }); }, async _ => { await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); }); + +AntiTracking.runTest("BroadcastChannel and Storage Access API", + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel can be used"); + }, + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + new BroadcastChannel("hello"); + ok(true, "BroadcastChanneli can be used"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel can be used"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false); + +AntiTracking.runTest("BroadcastChannel in workers and Storage Access API", + async _ => { + function blockingCode() { + try { + new BroadcastChannel("hello"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + let blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + ok(blob, "Blob has been created"); + + blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + }, + async _ => { + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function(e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingStorage.js +++ b/toolkit/components/antitracking/test/browser/browser_blockingStorage.js @@ -31,8 +31,127 @@ AntiTracking.runTest("sessionStorage", await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); }, [], true, true, false); + +AntiTracking.runTest("localStorage and Storage Access API", + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false); + +AntiTracking.runTest("sessionStorage and Storage Access API", + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + sessionStorage.foo = 42; + ok(true, "SessionStorage is always allowed"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + sessionStorage.foo = 42; + ok(true, "SessionStorage is allowed after calling the storage access API too"); + }, + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + sessionStorage.foo = 42; + ok(true, "SessionStorage is always allowed"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + sessionStorage.foo = 42; + ok(true, "SessionStorage is allowed after calling the storage access API too"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false);
--- a/toolkit/components/antitracking/test/browser/browser_blockingWorkers.js +++ b/toolkit/components/antitracking/test/browser/browser_blockingWorkers.js @@ -1,8 +1,10 @@ +requestLongerTimeout(4); + AntiTracking.runTest("SharedWorkers", async _ => { try { new SharedWorker("a.js", "foo"); ok(false, "SharedWorker cannot be used!"); } catch (e) { ok(true, "SharedWorker cannot be used!"); is(e.name, "SecurityError", "We want a security error message."); @@ -15,17 +17,17 @@ AntiTracking.runTest("SharedWorkers", async _ => { await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); }); AntiTracking.runTest("ServiceWorkers", async _ => { - await navigator.serviceWorker.register("empty.js", { scope: "/" }).then( + await navigator.serviceWorker.register("empty.js").then( _ => { ok(false, "ServiceWorker cannot be used!"); }, _ => { ok(true, "ServiceWorker cannot be used!"); }); }, null, async _ => { await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); @@ -45,8 +47,213 @@ AntiTracking.runTest("DOM Cache", _ => { ok(true, "DOM Cache can be used!"); }, _ => { ok(false, "DOM Cache can be used!"); }); }, async _ => { await new Promise(resolve => { Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); }); }); + +AntiTracking.runTest("SharedWorkers and Storage Access API", + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + }, + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false); + +AntiTracking.runTest("ServiceWorkers and Storage Access API", + async _ => { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); + + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + await navigator.serviceWorker.register("empty.js").then( + _ => { ok(false, "ServiceWorker cannot be used!"); }, + _ => { ok(true, "ServiceWorker cannot be used!"); }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + await navigator.serviceWorker.register("empty.js").then( + reg => { ok(true, "ServiceWorker can be used!"); return reg; }, + _ => { ok(false, "ServiceWorker cannot be used! " + _); }).then( + reg => reg.unregister(), + _ => { ok(false, "unregister failed"); }); + }, + async _ => { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); + + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + await navigator.serviceWorker.register("empty.js").then( + reg => { ok(true, "ServiceWorker can be used!"); return reg; }, + _ => { ok(false, "ServiceWorker cannot be used!"); }).then( + reg => reg.unregister(), + _ => { ok(false, "unregister failed"); }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + await navigator.serviceWorker.register("empty.js").then( + reg => { ok(true, "ServiceWorker can be used!"); return reg; }, + _ => { ok(false, "ServiceWorker cannot be used!"); }).then( + reg => reg.unregister(), + _ => { ok(false, "unregister failed"); }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + [["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true]], + false, false); + +AntiTracking.runTest("DOM Cache and Storage Access API", + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + await caches.open("wow").then( + _ => { ok(false, "DOM Cache cannot be used!"); }, + _ => { ok(true, "DOM Cache cannot be used!"); }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + await caches.open("wow").then( + _ => { ok(true, "DOM Cache can be used!"); }, + _ => { ok(false, "DOM Cache can be used!"); }); + }, + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + await caches.open("wow").then( + _ => { ok(true, "DOM Cache can be used!"); }, + _ => { ok(false, "DOM Cache can be used!"); }); + + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + try { + p = document.requestStorageAccess(); + } finally { + helper.destruct(); + } + await p; + + hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now has storage access"); + + // For non-tracking windows, calling the API is a no-op + await caches.open("wow").then( + _ => { ok(true, "DOM Cache can be used!"); }, + _ => { ok(false, "DOM Cache can be used!"); }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + null, false, false);
new file mode 100644 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivateWindow.js @@ -0,0 +1,36 @@ +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +AntiTracking.runTest("Storage Access API called in a private window", + // blocking callback + async _ => { + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } finally { + helper.destruct(); + } + ok(!threw, "requestStorageAccess should not throw"); + threw = false; + try { + await p; + } catch (e) { + threw = true; + } + ok(threw, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + false, // no blocking notifications + true, // run in private window + null // iframe sandbox +);
new file mode 100644 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js @@ -0,0 +1,168 @@ +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +AntiTracking.runTest("Storage Access API called in a sandboxed iframe", + // blocking callback + async _ => { + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } finally { + helper.destruct(); + } + ok(!threw, "requestStorageAccess should not throw"); + threw = false; + try { + await p; + } catch (e) { + threw = true; + } + ok(threw, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + false, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin" +); + +AntiTracking.runTest("Storage Access API called in a sandboxed iframe with" + + " allow-storage-access-by-user-activation", + // blocking callback + async _ => { + let dwu = SpecialPowers.getDOMWindowUtils(window); + let helper = dwu.setHandlingUserInput(true); + + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } finally { + helper.destruct(); + } + ok(!threw, "requestStorageAccess should not throw"); + threw = false; + try { + await p; + } catch (e) { + threw = true; + } + ok(!threw, "requestStorageAccess should be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + true, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-storage-access-by-user-activation" +); + +AntiTracking.runTest("Verify that sandboxed contexts don't get the saved permission", + // blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + false, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin" +); + +AntiTracking.runTest("Verify that sandboxed contexts with" + + " allow-storage-access-by-user-activation get the" + + " saved permission", + // blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Has storage access"); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + null, // cleanup function + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + false, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-storage-access-by-user-activation" +); + +AntiTracking.runTest("Verify that private browsing contexts don't get the saved permission", + // blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + false, // no blocking notifications + true, // run in private window + null // iframe sandbox +); + +AntiTracking.runTest("Verify that non-sandboxed contexts get the" + + " saved permission", + // blocking callback + async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Has storage access"); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); + }, + [["dom.storage_access.enabled", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + false // no blocking notifications +);
new file mode 100644 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js @@ -0,0 +1,198 @@ +ChromeUtils.import("resource://gre/modules/Services.jsm"); + +add_task(async function() { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.storage_access.enabled", true], + ["browser.contentblocking.enabled", true], + ["browser.contentblocking.ui.enabled", true], + ["browser.contentblocking.rejecttrackers.ui.enabled", true], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ]}); + + await UrlClassifierTestUtils.addTestTrackers(); +}); + +add_task(async function testWindowOpenHeuristic() { + info("Starting window.open() heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await ContentTask.spawn(browser, { + page: TEST_3RD_PARTY_PAGE_WO, + }, async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now obtained storage access"); + }).toString(); + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function() { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function() { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); +}); + +add_task(async function testUserInteractionHeuristic() { + info("Starting user interaction heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await ContentTask.spawn(browser, { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + let hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Now obtained storage access"); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { ifr.onload = resolve; }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info("The 3rd party content should not have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification(aSubject, aTopic, aData) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + ifr.contentWindow.open(obj.popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.nonBlockingCallback }, "*"); + }); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function() { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); + }); +}); +
--- a/toolkit/components/antitracking/test/browser/head.js +++ b/toolkit/components/antitracking/test/browser/head.js @@ -20,27 +20,28 @@ var gFeatures = undefined; let {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm", {}); requestLongerTimeout(2); this.AntiTracking = { runTest(name, callbackTracking, callbackNonTracking, cleanupFunction, extraPrefs, windowOpenTest = true, userInteractionTest = true, expectedBlockingNotifications = true, - runInPrivateWindow = false) { + runInPrivateWindow = false, iframeSandbox = null) { // Here we want to test that a 3rd party context is simply blocked. this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_TRACKER, blockingByContentBlocking: true, allowList: false, callback: callbackTracking, extraPrefs, expectedBlockingNotifications, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); if (callbackNonTracking) { let runExtraTests = true; let options = {}; if (typeof callbackNonTracking == "object") { callbackNonTracking = callbackNonTracking.callback; @@ -76,136 +77,148 @@ this.AntiTracking = { name, cookieBehavior: BEHAVIOR_ACCEPT, blockingByContentBlocking: true, allowList: false, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_FOREIGN, blockingByContentBlocking: false, allowList: false, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_TRACKER, blockingByContentBlocking: false, allowList: false, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_FOREIGN, blockingByContentBlocking: false, allowList: true, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_TRACKER, blockingByContentBlocking: false, allowList: true, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_ACCEPT, blockingByContentBlocking: false, allowList: false, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); // Try testing using the allow list with both reject foreign and reject tracker cookie behaviors this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_FOREIGN, blockingByContentBlocking: true, allowList: true, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_TRACKER, blockingByContentBlocking: true, allowList: true, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); } else { this._createTask({ name, cookieBehavior: options.cookieBehavior, blockingByContentBlocking: options.blockingByContentBlocking, allowList: options.blockingByAllowList, callback: callbackNonTracking, extraPrefs: [], expectedBlockingNotifications: false, runInPrivateWindow, + iframeSandbox, }); this._createCleanupTask(cleanupFunction); } // Phase 2: Here we want to test that a third-party context doesn't // get blocked with when the same origin is opened through window.open(). if (windowOpenTest) { - this._createWindowOpenTask(name, callbackTracking, callbackNonTracking, runInPrivateWindow, extraPrefs); + this._createWindowOpenTask(name, callbackTracking, callbackNonTracking, + runInPrivateWindow, iframeSandbox, extraPrefs); this._createCleanupTask(cleanupFunction); } // Phase 3: Here we want to test that a third-party context doesn't // get blocked with user interaction present if (userInteractionTest) { - this._createUserInteractionTask(name, callbackTracking, callbackNonTracking, runInPrivateWindow, extraPrefs); + this._createUserInteractionTask(name, callbackTracking, callbackNonTracking, + runInPrivateWindow, iframeSandbox, extraPrefs); this._createCleanupTask(cleanupFunction); } } }, async _setupTest(win, cookieBehavior, blockingByContentBlocking, extraPrefs) { await SpecialPowers.flushPrefEnv(); await SpecialPowers.pushPrefEnv({"set": [ + ["dom.storage_access.enabled", true], ["browser.contentblocking.enabled", blockingByContentBlocking], ["network.cookie.cookieBehavior", cookieBehavior], ["privacy.trackingprotection.enabled", false], ["privacy.trackingprotection.pbmode.enabled", false], ["privacy.trackingprotection.annotate_channels", cookieBehavior != BEHAVIOR_ACCEPT], [win.ContentBlocking.prefIntroCount, win.ContentBlocking.MAX_INTROS], ["browser.fastblock.enabled", false], // prevent intermittent failures ]}); @@ -217,17 +230,18 @@ this.AntiTracking = { await UrlClassifierTestUtils.addTestTrackers(); }, _createTask(options) { add_task(async function() { info("Starting " + (options.cookieBehavior != BEHAVIOR_ACCEPT ? "blocking" : "non-blocking") + " cookieBehavior (" + options.cookieBehavior + ") and " + (options.blockingByContentBlocking ? "blocking" : "non-blocking") + " contentBlocking with" + (options.allowList ? "" : "out") + " allow list test " + options.name + - " running in a " + (options.runInPrivateWindow ? "private" : "normal") + " window"); + " running in a " + (options.runInPrivateWindow ? "private" : "normal") + " window " + + " with iframe sandbox set to " + options.iframeSandbox); let win = window; if (options.runInPrivateWindow) { win = OpenBrowserWindow({private: true}); await TestUtils.topicObserved("browser-delayed-startup-finished"); } await AntiTracking._setupTest(win, options.cookieBehavior, @@ -257,24 +271,28 @@ this.AntiTracking = { // The previous function reloads the browser, so wait for it to load again! await BrowserTestUtils.browserLoaded(browser); } info("Creating a 3rd party content"); await ContentTask.spawn(browser, { page: TEST_3RD_PARTY_PAGE, - callback: options.callback.toString() }, + callback: options.callback.toString(), + iframeSandbox: options.iframeSandbox }, async function(obj) { await new content.Promise(resolve => { let ifr = content.document.createElement("iframe"); ifr.onload = function() { info("Sending code to the 3rd party content"); ifr.contentWindow.postMessage(obj.callback, "*"); }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } content.addEventListener("message", function msg(event) { if (event.data.type == "finish") { content.removeEventListener("message", msg); resolve(); return; } @@ -321,17 +339,18 @@ this.AntiTracking = { add_task(async function() { info("Cleaning up."); if (cleanupFunction) { await cleanupFunction(); } }); }, - _createWindowOpenTask(name, blockingCallback, nonBlockingCallback, runInPrivateWindow, extraPrefs) { + _createWindowOpenTask(name, blockingCallback, nonBlockingCallback, runInPrivateWindow, + iframeSandbox, extraPrefs) { add_task(async function() { info("Starting window-open test " + name); let win = window; if (runInPrivateWindow) { win = OpenBrowserWindow({private: true}); await TestUtils.topicObserved("browser-delayed-startup-finished"); } @@ -350,24 +369,28 @@ this.AntiTracking = { pageURL += "?noopener"; } info("Creating a 3rd party content"); await ContentTask.spawn(browser, { page: pageURL, blockingCallback: blockingCallback.toString(), nonBlockingCallback: nonBlockingCallback.toString(), + iframeSandbox, }, async function(obj) { await new content.Promise(resolve => { let ifr = content.document.createElement("iframe"); ifr.onload = function() { info("Sending code to the 3rd party content"); ifr.contentWindow.postMessage(obj, "*"); }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } content.addEventListener("message", function msg(event) { if (event.data.type == "finish") { content.removeEventListener("message", msg); resolve(); return; } @@ -393,17 +416,18 @@ this.AntiTracking = { BrowserTestUtils.removeTab(tab); if (runInPrivateWindow) { win.close(); } }); }, - _createUserInteractionTask(name, blockingCallback, nonBlockingCallback, runInPrivateWindow, extraPrefs) { + _createUserInteractionTask(name, blockingCallback, nonBlockingCallback, + runInPrivateWindow, iframeSandbox, extraPrefs) { add_task(async function() { info("Starting user-interaction test " + name); let win = window; if (runInPrivateWindow) { win = OpenBrowserWindow({private: true}); await TestUtils.topicObserved("browser-delayed-startup-finished"); } @@ -418,20 +442,24 @@ this.AntiTracking = { await BrowserTestUtils.browserLoaded(browser); info("Creating a 3rd party content"); await ContentTask.spawn(browser, { page: TEST_3RD_PARTY_PAGE_UI, popup: TEST_POPUP_PAGE, blockingCallback: blockingCallback.toString(), nonBlockingCallback: nonBlockingCallback.toString(), + iframeSandbox, }, async function(obj) { let ifr = content.document.createElement("iframe"); let loading = new content.Promise(resolve => { ifr.onload = resolve; }); + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } content.document.body.appendChild(ifr); ifr.src = obj.page; await loading; info("The 3rd party content should not have access to first party storage."); await new content.Promise(resolve => { content.addEventListener("message", function msg(event) { if (event.data.type == "finish") {
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm +++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm @@ -73,35 +73,16 @@ var _fallbackThemeData = null; // Holds whether or not the default theme should display in dark mode. This is // typically the case when the OS has a dark system appearance. var _defaultThemeIsInDarkMode = false; // Holds the dark theme to be used if the OS has a dark system appearance and // the default theme is selected. var _defaultDarkThemeID = null; -// Convert from the old storage format (in which the order of usedThemes -// was combined with isThemeSelected to determine which theme was selected) -// to the new one (where a selectedThemeID determines which theme is selected). -(function() { - let wasThemeSelected = _prefs.getBoolPref("isThemeSelected", false); - - if (wasThemeSelected) { - _prefs.clearUserPref("isThemeSelected"); - let themes = []; - try { - themes = JSON.parse(_prefs.getStringPref("usedThemes")); - } catch (e) { } - - if (Array.isArray(themes) && themes[0]) { - _prefs.setCharPref("selectedThemeID", themes[0].id); - } - } -})(); - var LightweightThemeManager = { get name() { return "LightweightThemeManager"; }, set fallbackThemeData(data) { if (data && Object.getOwnPropertyNames(data).length) { _fallbackThemeData = Object.assign({}, data);
--- a/toolkit/xre/nsEmbeddingModule.cpp +++ b/toolkit/xre/nsEmbeddingModule.cpp @@ -5,34 +5,30 @@ #include "mozilla/ModuleUtils.h" #include "nsDialogParamBlock.h" #include "nsWindowWatcher.h" #include "nsAppStartupNotifier.h" #include "nsFind.h" #include "nsWebBrowserFind.h" #include "nsWebBrowserPersist.h" -#include "nsCommandParams.h" -#include "nsCommandGroup.h" #include "nsNetCID.h" #include "nsEmbedCID.h" #ifdef NS_PRINTING #include "nsPrintingPromptService.h" #include "nsPrintingProxy.h" #endif NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsWindowWatcher, Init) NS_GENERIC_FACTORY_CONSTRUCTOR(nsAppStartupNotifier) NS_GENERIC_FACTORY_CONSTRUCTOR(nsFind) NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebBrowserFind) NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebBrowserPersist) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsCommandParams) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsControllerCommandGroup) #ifdef MOZ_XUL NS_GENERIC_FACTORY_CONSTRUCTOR(nsDialogParamBlock) #ifdef NS_PRINTING NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsPrintingPromptService, nsPrintingPromptService::GetSingleton) #ifdef PROXY_PRINTING NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsPrintingProxy, @@ -47,18 +43,16 @@ NS_DEFINE_NAMED_CID(NS_DIALOGPARAMBLOCK_ NS_DEFINE_NAMED_CID(NS_PRINTINGPROMPTSERVICE_CID); #endif #endif NS_DEFINE_NAMED_CID(NS_WINDOWWATCHER_CID); NS_DEFINE_NAMED_CID(NS_FIND_CID); NS_DEFINE_NAMED_CID(NS_WEB_BROWSER_FIND_CID); NS_DEFINE_NAMED_CID(NS_APPSTARTUPNOTIFIER_CID); NS_DEFINE_NAMED_CID(NS_WEBBROWSERPERSIST_CID); -NS_DEFINE_NAMED_CID(NS_COMMAND_PARAMS_CID); -NS_DEFINE_NAMED_CID(NS_CONTROLLER_COMMAND_GROUP_CID); static const mozilla::Module::CIDEntry kEmbeddingCIDs[] = { #ifdef MOZ_XUL { &kNS_DIALOGPARAMBLOCK_CID, false, nullptr, nsDialogParamBlockConstructor }, #ifdef NS_PRINTING #ifdef PROXY_PRINTING { &kNS_PRINTINGPROMPTSERVICE_CID, false, nullptr, nsPrintingPromptServiceConstructor, @@ -70,35 +64,31 @@ static const mozilla::Module::CIDEntry k #endif #endif #endif { &kNS_WINDOWWATCHER_CID, false, nullptr, nsWindowWatcherConstructor }, { &kNS_FIND_CID, false, nullptr, nsFindConstructor }, { &kNS_WEB_BROWSER_FIND_CID, false, nullptr, nsWebBrowserFindConstructor }, { &kNS_APPSTARTUPNOTIFIER_CID, false, nullptr, nsAppStartupNotifierConstructor }, { &kNS_WEBBROWSERPERSIST_CID, false, nullptr, nsWebBrowserPersistConstructor }, - { &kNS_COMMAND_PARAMS_CID, false, nullptr, nsCommandParamsConstructor }, - { &kNS_CONTROLLER_COMMAND_GROUP_CID, false, nullptr, nsControllerCommandGroupConstructor }, { nullptr } }; static const mozilla::Module::ContractIDEntry kEmbeddingContracts[] = { #ifdef MOZ_XUL { NS_DIALOGPARAMBLOCK_CONTRACTID, &kNS_DIALOGPARAMBLOCK_CID }, #ifdef NS_PRINTING { NS_PRINTINGPROMPTSERVICE_CONTRACTID, &kNS_PRINTINGPROMPTSERVICE_CID }, #endif #endif { NS_WINDOWWATCHER_CONTRACTID, &kNS_WINDOWWATCHER_CID }, { NS_FIND_CONTRACTID, &kNS_FIND_CID }, { NS_WEB_BROWSER_FIND_CONTRACTID, &kNS_WEB_BROWSER_FIND_CID }, { NS_APPSTARTUPNOTIFIER_CONTRACTID, &kNS_APPSTARTUPNOTIFIER_CID }, { NS_WEBBROWSERPERSIST_CONTRACTID, &kNS_WEBBROWSERPERSIST_CID }, - { NS_COMMAND_PARAMS_CONTRACTID, &kNS_COMMAND_PARAMS_CID }, - { NS_CONTROLLER_COMMAND_GROUP_CONTRACTID, &kNS_CONTROLLER_COMMAND_GROUP_CID }, { nullptr } }; static const mozilla::Module kEmbeddingModule = { mozilla::Module::kVersion, kEmbeddingCIDs, kEmbeddingContracts };
--- a/widget/nsBaseWidget.cpp +++ b/widget/nsBaseWidget.cpp @@ -2482,19 +2482,18 @@ nsIWidget::GetEditCommands(nsIWidget::Na MOZ_ASSERT(aCommands.IsEmpty()); } already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboard() { if (XRE_IsContentProcess()) { return CreateBidiKeyboardContentProcess(); - } else { - return CreateBidiKeyboardInner(); } + return CreateBidiKeyboardInner(); } #ifdef ANDROID already_AddRefed<nsIBidiKeyboard> nsIWidget::CreateBidiKeyboardInner() { // no bidi keyboard implementation return nullptr;
--- a/xpcom/base/nsCycleCollector.cpp +++ b/xpcom/base/nsCycleCollector.cpp @@ -159,18 +159,18 @@ #include "mozilla/DebugOnly.h" #include "mozilla/HashFunctions.h" #include "mozilla/HashTable.h" #include "mozilla/HoldDropJSObjects.h" /* This must occur *after* base/process_util.h to avoid typedefs conflicts. */ #include "mozilla/LinkedList.h" #include "mozilla/MemoryReporting.h" #include "mozilla/Move.h" +#include "mozilla/MruCache.h" #include "mozilla/SegmentedVector.h" -#include "mozilla/Variant.h" #include "nsCycleCollectionParticipant.h" #include "nsCycleCollectionNoteRootCallback.h" #include "nsDeque.h" #include "nsExceptionHandler.h" #include "nsCycleCollector.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" @@ -2089,52 +2089,26 @@ private: nsCycleCollectionParticipant* mJSParticipant; nsCycleCollectionParticipant* mJSZoneParticipant; nsCString mNextEdgeName; RefPtr<nsCycleCollectorLogger> mLogger; bool mMergeZones; nsAutoPtr<NodePool::Enumerator> mCurrNode; uint32_t mNoteChildCount; - class GraphCache + struct PtrInfoCache : public MruCache<void*, PtrInfo*, PtrInfoCache, 491> { - public: - // This either returns a pointer if present, or an index, if it isn't. - Variant<PtrInfo*, uint32_t> GetEntryOrIndex(void* aPtr) - { - uint32_t hash = mozilla::HashGeneric(aPtr); - uint32_t index = hash % kCacheSize; - PtrInfo* result = mCache[index]; - if (result && result->mPointer == aPtr) { - return AsVariant(result); - } - - return AsVariant(index); - } - - void Add(uint32_t aIndex, PtrInfo* aPtrInfo) + static HashNumber Hash(const void* aKey) { return HashGeneric(aKey); } + static bool Match(const void* aKey, const PtrInfo* aVal) { - mCache[aIndex] = aPtrInfo; + return aVal->mPointer == aKey; } - - void Remove(void* aPtr) - { - uint32_t hash = mozilla::HashGeneric(aPtr); - uint32_t index = hash % kCacheSize; - PtrInfo* pinfo = mCache[index]; - if (pinfo && pinfo->mPointer == aPtr) { - mCache[index] = nullptr; - } - } - private: - const static uint32_t kCacheSize = 491; - PtrInfo* mCache[kCacheSize] = {0}; }; - GraphCache mGraphCache; + PtrInfoCache mGraphCache; public: CCGraphBuilder(CCGraph& aGraph, CycleCollectorResults& aResults, CycleCollectedJSRuntime* aCCRuntime, nsCycleCollectorLogger* aLogger, bool aMergeZones); virtual ~CCGraphBuilder(); @@ -2282,24 +2256,22 @@ CCGraphBuilder::~CCGraphBuilder() PtrInfo* CCGraphBuilder::AddNode(void* aPtr, nsCycleCollectionParticipant* aParticipant) { if (mGraph.mOutOfMemory) { return nullptr; } - Variant<PtrInfo*, uint32_t> cacheVariant = mGraphCache.GetEntryOrIndex(aPtr); - if (cacheVariant.is<PtrInfo*>()) { - MOZ_ASSERT(cacheVariant.as<PtrInfo*>()->mParticipant == aParticipant, + PtrInfoCache::Entry cached = mGraphCache.Lookup(aPtr); + if (cached) { + MOZ_ASSERT(cached.Data()->mParticipant == aParticipant, "nsCycleCollectionParticipant shouldn't change!"); - return cacheVariant.as<PtrInfo*>(); - } - - MOZ_ASSERT(cacheVariant.is<uint32_t>()); + return cached.Data(); + } PtrInfo* result; auto p = mGraph.mPtrInfoMap.lookupForAdd(aPtr); if (!p) { // New entry result = mNodeBuilder.Add(aPtr, aParticipant); if (!result) { return nullptr; @@ -2314,17 +2286,17 @@ CCGraphBuilder::AddNode(void* aPtr, nsCy } } else { result = *p; MOZ_ASSERT(result->mParticipant == aParticipant, "nsCycleCollectionParticipant shouldn't change!"); } - mGraphCache.Add(cacheVariant.as<uint32_t>(), result); + cached.Set(result); return result; } bool CCGraphBuilder::AddPurpleRoot(void* aRoot, nsCycleCollectionParticipant* aParti) { ToParticipant(aRoot, &aParti);
new file mode 100644 --- /dev/null +++ b/xpcom/ds/MruCache.h @@ -0,0 +1,183 @@ +/* -*- 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_MruCache_h +#define mozilla_MruCache_h + +#include <cstdint> +#include <type_traits> +#include <utility> + +#include "mozilla/Attributes.h" +#include "mozilla/HashFunctions.h" + +namespace mozilla { + +namespace detail { + +// Helper struct for checking if a value is empty. +// +// `IsNotEmpty` will return true if `Value` is not a pointer type or if the +// pointer value is not null. +template <typename Value, bool IsPtr = std::is_pointer<Value>::value> +struct EmptyChecker +{ + static bool IsNotEmpty(const Value&) { return true; } +}; +// Template specialization for the `IsPtr == true` case. +template <typename Value> +struct EmptyChecker<Value, true> +{ + static bool IsNotEmpty(const Value& aVal) { return aVal != nullptr; } +}; + +} // namespace detail + +// Provides a most recently used cache that can be used as a layer on top of +// a larger container where lookups can be expensive. The default size is 31, +// which as a prime number provides a better distrubution of cached entries. +// +// Users are expected to provide a `Cache` class that defines two required +// methods: +// - A method for providing the hash of a key: +// +// static HashNumber Hash(const KeyType& aKey) +// +// - A method for matching a key to a value, for pointer types the value +// is guaranteed not to be null. +// +// static bool Match(const KeyType& aKey, const ValueType& aVal) +// +// For example: +// class MruExample : public MruCache<void*, PtrInfo*, MruExample> +// { +// static HashNumber Hash(const KeyType& aKey) +// { +// return HashGeneric(aKey); +// } +// static Match(const KeyType& aKey, const ValueType& aVal) +// { +// return aVal->mPtr == aKey; +// } +// }; +template <class Key, class Value, class Cache, size_t Size=31> +class MruCache +{ + // Best distribution is achieved with a prime number. Ideally the closest + // to a power of two will be the most efficient use of memory. This + // assertion is pretty weak, but should catch the common inclination to + // use a power-of-two. + static_assert(Size % 2 != 0, "Use a prime number"); + + // This is a stronger assertion but significantly limits the values to just + // those close to a power-of-two value. + //static_assert(Size == 7 || Size == 13 || Size == 31 || Size == 61 || + // Size == 127 || Size == 251 || Size == 509 || Size == 1021, + // "Use a prime number less than 1024"); + +public: + using KeyType = Key; + using ValueType = Value; + + MruCache() = default; + MruCache(const MruCache&) = delete; + MruCache(const MruCache&&) = delete; + + // Inserts the given value into the cache. Potentially overwrites an + // existing entry. + template <typename U> + void Put(const KeyType& aKey, U&& aVal) + { + *RawEntry(aKey) = std::forward<U>(aVal); + } + + // Removes the given entry if it is in the cache. + void Remove(const KeyType& aKey) + { + Lookup(aKey).Remove(); + } + + // Clears all cached entries and resets them to a default value. + void Clear() + { + for (ValueType& val : mCache) { + val = ValueType{}; + } + } + + // Helper that holds an entry that matched a lookup key. Usage: + // + // auto p = mCache.Lookup(aKey); + // if (p) { + // return p.Data(); + // } + // + // auto foo = new Foo(); + // mTable.Insert(aKey, foo); + // p.Set(foo); + // return foo; + class Entry + { + public: + Entry(ValueType* aEntry, bool aMatch) + : mEntry(aEntry) + , mMatch(aMatch) + { + MOZ_ASSERT(mEntry); + } + + explicit operator bool() const { return mMatch; } + + ValueType& Data() const + { + MOZ_ASSERT(mMatch); + return *mEntry; + } + + template<typename U> + void Set(U&& aValue) + { + mMatch = true; + Data() = std::forward<U>(aValue); + } + + void Remove() + { + if (mMatch) { + Data() = ValueType{}; + mMatch = false; + } + } + + private: + ValueType* mEntry; // Location of the entry in the cache. + bool mMatch; // Whether the value matched. + }; + + // Retrieves an entry from the cache. Can be used to test if an entry is + // present, update the entry to a new value, or remove the entry if one was + // matched. + Entry Lookup(const KeyType& aKey) + { + using EmptyChecker = detail::EmptyChecker<ValueType>; + + auto entry = RawEntry(aKey); + bool match = EmptyChecker::IsNotEmpty(*entry) && Cache::Match(aKey, *entry); + return Entry(entry, match); + } + +private: + MOZ_ALWAYS_INLINE ValueType* RawEntry(const KeyType& aKey) + { + return &mCache[Cache::Hash(aKey) % Size]; + } + + ValueType mCache[Size] = {}; +}; + +} // namespace mozilla + +#endif // mozilla_mrucache_h
--- a/xpcom/ds/StaticAtoms.py +++ b/xpcom/ds/StaticAtoms.py @@ -69,16 +69,18 @@ STATIC_ATOMS = [ Atom("allowfullscreen", "allowfullscreen"), Atom("allowmodals", "allow-modals"), Atom("alloworientationlock", "allow-orientation-lock"), Atom("allowpaymentrequest", "allowpaymentrequest"), Atom("allowpointerlock", "allow-pointer-lock"), Atom("allowpopupstoescapesandbox", "allow-popups-to-escape-sandbox"), Atom("allowpopups", "allow-popups"), Atom("allowpresentation", "allow-presentation"), + Atom("allowstorageaccessbyuseractivatetion", + "allow-storage-access-by-user-activation"), Atom("allowsameorigin", "allow-same-origin"), Atom("allowscripts", "allow-scripts"), Atom("allowscriptstoclose", "allowscriptstoclose"), Atom("allowtopnavigation", "allow-top-navigation"), Atom("allowuntrusted", "allowuntrusted"), Atom("alt", "alt"), Atom("alternate", "alternate"), Atom("always", "always"),
--- a/xpcom/ds/moz.build +++ b/xpcom/ds/moz.build @@ -82,16 +82,17 @@ EXPORTS += [ EXPORTS.mozilla += [ 'ArenaAllocator.h', 'ArenaAllocatorExtensions.h', 'ArrayIterator.h', 'AtomArray.h', 'Dafsa.h', 'IncrementalTokenizer.h', + 'MruCache.h', 'Observer.h', 'SimpleEnumerator.h', 'StickyTimeDuration.h', 'Tokenizer.h', ] UNIFIED_SOURCES += [ 'Dafsa.cpp',
--- a/xpcom/ds/nsAtomTable.cpp +++ b/xpcom/ds/nsAtomTable.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/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/HashFunctions.h" #include "mozilla/MemoryReporting.h" +#include "mozilla/MruCache.h" #include "mozilla/Mutex.h" #include "mozilla/DebugOnly.h" #include "mozilla/Sprintf.h" #include "mozilla/Unused.h" #include "nsAtom.h" #include "nsAtomTable.h" #include "nsAutoPtr.h" @@ -216,19 +217,27 @@ struct AtomTableKey struct AtomTableEntry : public PLDHashEntryHdr { // These references are either to dynamic atoms, in which case they are // non-owning, or they are to static atoms, which aren't really refcounted. // See the comment at the top of this file for more details. nsAtom* MOZ_NON_OWNING_REF mAtom; }; -#define RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE 31 -static nsAtom* - sRecentlyUsedMainThreadAtoms[RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE] = {}; +struct AtomCache : public MruCache<AtomTableKey, nsAtom*, AtomCache> +{ + static HashNumber Hash(const AtomTableKey& aKey) { return aKey.mHash; } + static bool Match(const AtomTableKey& aKey, const nsAtom* aVal) + { + MOZ_ASSERT(aKey.mUTF16String); + return aVal->Equals(aKey.mUTF16String, aKey.mLength); + } +}; + +static AtomCache sRecentlyUsedMainThreadAtoms; // In order to reduce locking contention for concurrent atomization, we segment // the atom table into N subtables, each with a separate lock. If the hash // values we use to select the subtable are evenly distributed, this reduces the // probability of contention by a factor of N. See bug 1440824. // // NB: This is somewhat similar to the technique used by Java's // ConcurrentHashTable. @@ -413,19 +422,17 @@ nsAtomTable::AddSizeOfIncludingThis(Mall MutexAutoLock lock(table.mLock); table.AddSizeOfExcludingThisLocked(aMallocSizeOf, aSizes); } } void nsAtomTable::GC(GCKind aKind) { MOZ_ASSERT(NS_IsMainThread()); - for (uint32_t i = 0; i < RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE; ++i) { - sRecentlyUsedMainThreadAtoms[i] = nullptr; - } + sRecentlyUsedMainThreadAtoms.Clear(); // Note that this is effectively an incremental GC, since only one subtable // is locked at a time. for (auto& table: mSubTables) { MutexAutoLock lock(table.mLock); table.GCLocked(aKind); } @@ -765,42 +772,36 @@ NS_Atomize(const nsAString& aUTF16String already_AddRefed<nsAtom> nsAtomTable::AtomizeMainThread(const nsAString& aUTF16String) { MOZ_ASSERT(NS_IsMainThread()); RefPtr<nsAtom> retVal; uint32_t hash; AtomTableKey key(aUTF16String.Data(), aUTF16String.Length(), &hash); - uint32_t index = hash % RECENTLY_USED_MAIN_THREAD_ATOM_CACHE_SIZE; - nsAtom* atom = sRecentlyUsedMainThreadAtoms[index]; - if (atom) { - uint32_t length = atom->GetLength(); - if (length == key.mLength && - (memcmp(atom->GetUTF16String(), - key.mUTF16String, length * sizeof(char16_t)) == 0)) { - retVal = atom; - return retVal.forget(); - } + auto p = sRecentlyUsedMainThreadAtoms.Lookup(key); + if (p) { + retVal = p.Data(); + return retVal.forget(); } nsAtomSubTable& table = SelectSubTable(key); MutexAutoLock lock(table.mLock); AtomTableEntry* he = table.Add(key); if (he->mAtom) { retVal = he->mAtom; } else { RefPtr<nsAtom> newAtom = dont_AddRef(nsDynamicAtom::Create(aUTF16String, hash)); he->mAtom = newAtom; retVal = newAtom.forget(); } - sRecentlyUsedMainThreadAtoms[index] = he->mAtom; + p.Set(retVal); return retVal.forget(); } already_AddRefed<nsAtom> NS_AtomizeMainThread(const nsAString& aUTF16String) { MOZ_ASSERT(gAtomTable); return gAtomTable->AtomizeMainThread(aUTF16String);
new file mode 100644 --- /dev/null +++ b/xpcom/tests/gtest/TestMruCache.cpp @@ -0,0 +1,393 @@ +/* -*- 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 "gtest/gtest.h" + +#include "mozilla/MruCache.h" +#include "nsString.h" + +using namespace mozilla; + +// A few MruCache implementations to use during testing. +struct IntMap : public MruCache<int, int, IntMap> +{ + static HashNumber Hash(const KeyType& aKey) { return aKey - 1; } + static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal; } +}; + +struct UintPtrMap : public MruCache<uintptr_t, int*, UintPtrMap> +{ + static HashNumber Hash(const KeyType& aKey) { return aKey - 1; } + static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == (KeyType)aVal; } +}; + +struct StringStruct +{ + nsCString mKey; + nsCString mOther; +}; + +struct StringStructMap : public MruCache<nsCString, StringStruct, StringStructMap> +{ + static HashNumber Hash(const KeyType& aKey) { return *aKey.BeginReading() - 1; } + static bool Match(const KeyType& aKey, const ValueType& aVal) { return aKey == aVal.mKey; } +}; + +// Helper for emulating convertable holders such as RefPtr. +template <typename T> +struct Convertable +{ + T mItem; + operator T() const { return mItem; } +}; + +// Helper to create a StringStructMap key. +nsCString MakeStringKey(char aKey) +{ + nsCString key; + key.Append(aKey); + return key; +} + +TEST(MruCache, TestNullChecker) +{ + using mozilla::detail::EmptyChecker; + + { + int test = 0; + EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test)); + + test = 42; + EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test)); + } + + { + const char* test = "abc"; + EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test)); + + test = nullptr; + EXPECT_FALSE(EmptyChecker<decltype(test)>::IsNotEmpty(test)); + } + + { + int foo = 42; + int* test = &foo; + EXPECT_TRUE(EmptyChecker<decltype(test)>::IsNotEmpty(test)); + + test = nullptr; + EXPECT_FALSE(EmptyChecker<decltype(test)>::IsNotEmpty(test)); + } +} + +TEST(MruCache, TestEmptyCache) +{ + { + // Test a basic empty cache. + IntMap mru; + + // Make sure the default values are set. + for (int i = 1; i < 32; i++) { + auto p = mru.Lookup(i); + + // Shouldn't be found. + EXPECT_FALSE(p); + } + } + + { + // Test an empty cache with pointer values. + UintPtrMap mru; + + // Make sure the default values are set. + for (uintptr_t i = 1; i < 32; i++) { + auto p = mru.Lookup(i); + + // Shouldn't be found. + EXPECT_FALSE(p); + } + } + + { + // Test an empty cache with more complex structure. + StringStructMap mru; + + // Make sure the default values are set. + for (char i = 1; i < 32; i++) { + const nsCString key = MakeStringKey(i); + auto p = mru.Lookup(key); + + // Shouldn't be found. + EXPECT_FALSE(p); + } + } +} + +TEST(MruCache, TestPut) +{ + IntMap mru; + + // Fill it up. + for (int i = 1; i < 32; i++) {