/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- /
/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */
/* 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 EXPORTED_SYMBOLS = ["ReadTopManifest", "CreateUrls"];

const {
    NS_GFXINFO_CONTRACTID,

    TYPE_REFTEST_EQUAL,
    TYPE_REFTEST_NOTEQUAL,
    TYPE_LOAD,
    TYPE_SCRIPT,
    TYPE_PRINT,

    EXPECTED_PASS,
    EXPECTED_FAIL,
    EXPECTED_RANDOM,
    EXPECTED_FUZZY,

    PREF_BOOLEAN,
    PREF_STRING,
    PREF_INTEGER,

    FOCUS_FILTER_NEEDS_FOCUS_TESTS,
    FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS,

    g,
} = ChromeUtils.import("resource://reftest/globals.jsm");
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const { AppConstants } = ChromeUtils.importESModule(
    "resource://gre/modules/AppConstants.sys.mjs"
);

const NS_SCRIPTSECURITYMANAGER_CONTRACTID = "@mozilla.org/scriptsecuritymanager;1";
const NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX = "@mozilla.org/network/protocol;1?name=";
const NS_XREAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";

const RE_PROTOCOL = /^\w+:/;
const RE_PREF_ITEM = /^(|test-|ref-)pref\((.+?),(.*)\)$/;


function ReadTopManifest(aFileURL, aFilter, aManifestID)
{
    var url = g.ioService.newURI(aFileURL);
    if (!url)
        throw "Expected a file or http URL for the manifest.";

    g.manifestsLoaded = {};
    ReadManifest(url, aFilter, aManifestID);
}

// Note: If you materially change the reftest manifest parsing,
// please keep the parser in layout/tools/reftest/__init__.py in sync.
function ReadManifest(aURL, aFilter, aManifestID)
{
    // Ensure each manifest is only read once. This assumes that manifests that
    // are included with filters will be read via their include before they are
    // read directly in the case of a duplicate
    if (g.manifestsLoaded.hasOwnProperty(aURL.spec)) {
        if (g.manifestsLoaded[aURL.spec] === null)
            return;
        else
            aFilter = [aFilter[0], aFilter[1], true];
    }
    g.manifestsLoaded[aURL.spec] = aFilter[1];

    var secMan = Cc[NS_SCRIPTSECURITYMANAGER_CONTRACTID]
                     .getService(Ci.nsIScriptSecurityManager);

    var listURL = aURL;
    var channel = NetUtil.newChannel({uri: aURL,
                                      loadUsingSystemPrincipal: true});
    try {
        var inputStream = channel.open();
    } catch (e) {
        g.logger.error("failed to open manifest at : " + aURL.spec);
        throw e;
    }
    if (channel instanceof Ci.nsIHttpChannel
        && channel.responseStatus != 200) {
      g.logger.error("HTTP ERROR : " + channel.responseStatus);
    }
    var streamBuf = getStreamContent(inputStream);
    inputStream.close();
    var lines = streamBuf.split(/\n|\r|\r\n/);

    // The sandbox for fails-if(), etc., condition evaluation. This is not
    // always required and so is created on demand.
    var sandbox;
    function GetOrCreateSandbox() {
        if (!sandbox) {
            sandbox = BuildConditionSandbox(aURL);
        }
        return sandbox;
    }

    var lineNo = 0;
    var urlprefix = "";
    var defaults = [];
    var defaultTestPrefSettings = [], defaultRefPrefSettings = [];
    if (g.compareRetainedDisplayLists) {
        AddRetainedDisplayListTestPrefs(GetOrCreateSandbox(), defaultTestPrefSettings,
                                        defaultRefPrefSettings);
    }
    for (var str of lines) {
        ++lineNo;
        if (str.charAt(0) == "#")
            continue; // entire line was a comment
        var i = str.search(/\s+#/);
        if (i >= 0)
            str = str.substring(0, i);
        // strip leading and trailing whitespace
        str = str.replace(/^\s*/, '').replace(/\s*$/, '');
        if (!str || str == "")
            continue;
        var items = str.split(/\s+/); // split on whitespace

        if (items[0] == "url-prefix") {
            if (items.length != 2)
                throw "url-prefix requires one url in manifest file " + aURL.spec + " line " + lineNo;
            urlprefix = items[1];
            continue;
        }

        if (items[0] == "defaults") {
            items.shift();
            defaults = items;
            continue;
        }

        var expected_status = EXPECTED_PASS;
        var allow_silent_fail = false;
        var minAsserts = 0;
        var maxAsserts = 0;
        var needs_focus = false;
        var slow = false;
        var skip = false;
        var testPrefSettings = defaultTestPrefSettings.concat();
        var refPrefSettings = defaultRefPrefSettings.concat();
        var fuzzy_delta = { min: 0, max: 2 };
        var fuzzy_pixels = { min: 0, max: 1 };
        var chaosMode = false;
        var wrCapture = { test: false, ref: false };
        var nonSkipUsed = false;
        var noAutoFuzz = false;

        var origLength = items.length;
        items = defaults.concat(items);
        while (items[0].match(/^(fails|needs-focus|random|skip|asserts|slow|require-or|silentfail|pref|test-pref|ref-pref|fuzzy|chaos-mode|wr-capture|wr-capture-ref|noautofuzz)/)) {
            var item = items.shift();
            var stat;
            var cond;
            var m = item.match(/^(fails|random|skip|silentfail)-if(\(.*\))$/);
            if (m) {
                stat = m[1];
                // Note: m[2] contains the parentheses, and we want them.
                cond = Cu.evalInSandbox(m[2], GetOrCreateSandbox());
            } else if (item.match(/^(fails|random|skip)$/)) {
                stat = item;
                cond = true;
            } else if (item == "needs-focus") {
                needs_focus = true;
                cond = false;
            } else if ((m = item.match(/^asserts\((\d+)(-\d+)?\)$/))) {
                cond = false;
                minAsserts = Number(m[1]);
                maxAsserts = (m[2] == undefined) ? minAsserts
                                                 : Number(m[2].substring(1));
            } else if ((m = item.match(/^asserts-if\((.*?),(\d+)(-\d+)?\)$/))) {
                cond = false;
                if (Cu.evalInSandbox("(" + m[1] + ")", GetOrCreateSandbox())) {
                    minAsserts = Number(m[2]);
                    maxAsserts =
                      (m[3] == undefined) ? minAsserts
                                          : Number(m[3].substring(1));
                }
            } else if (item == "slow") {
                cond = false;
                slow = true;
            } else if ((m = item.match(/^require-or\((.*?)\)$/))) {
                var args = m[1].split(/,/);
                if (args.length != 2) {
                    throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": wrong number of args to require-or";
                }
                var [precondition_str, fallback_action] = args;
                var preconditions = precondition_str.split(/&&/);
                cond = false;
                for (var precondition of preconditions) {
                    if (precondition === "debugMode") {
                        // Currently unimplemented. Requires asynchronous
                        // JSD call + getting an event while no JS is running
                        stat = fallback_action;
                        cond = true;
                        break;
                    } else if (precondition === "true") {
                        // For testing
                    } else {
                        // Unknown precondition. Assume it is unimplemented.
                        stat = fallback_action;
                        cond = true;
                        break;
                    }
                }
            } else if ((m = item.match(/^slow-if\((.*?)\)$/))) {
                cond = false;
                if (Cu.evalInSandbox("(" + m[1] + ")", GetOrCreateSandbox()))
                    slow = true;
            } else if (item == "silentfail") {
                cond = false;
                allow_silent_fail = true;
            } else if ((m = item.match(RE_PREF_ITEM))) {
                cond = false;
                if (!AddPrefSettings(m[1], m[2], m[3], GetOrCreateSandbox(),
                                     testPrefSettings, refPrefSettings)) {
                    throw "Error in pref value in manifest file " + aURL.spec + " line " + lineNo;
                }
            } else if ((m = item.match(/^fuzzy\((\d+)-(\d+),(\d+)-(\d+)\)$/))) {
              cond = false;
              expected_status = EXPECTED_FUZZY;
              fuzzy_delta = ExtractRange(m, 1);
              fuzzy_pixels = ExtractRange(m, 3);
            } else if ((m = item.match(/^fuzzy-if\((.*?),(\d+)-(\d+),(\d+)-(\d+)\)$/))) {
              cond = false;
              if (Cu.evalInSandbox("(" + m[1] + ")", GetOrCreateSandbox())) {
                expected_status = EXPECTED_FUZZY;
                fuzzy_delta = ExtractRange(m, 2);
                fuzzy_pixels = ExtractRange(m, 4);
              }
            } else if (item == "chaos-mode") {
                cond = false;
                chaosMode = true;
            } else if (item == "wr-capture") {
                cond = false;
                wrCapture.test = true;
            } else if (item == "wr-capture-ref") {
                cond = false;
                wrCapture.ref = true;
            } else if (item == "noautofuzz") {
                cond = false;
                noAutoFuzz = true;
            } else {
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": unexpected item " + item;
            }

            if (stat != "skip") {
                nonSkipUsed = true;
            }

            if (cond) {
                if (stat == "fails") {
                    expected_status = EXPECTED_FAIL;
                } else if (stat == "random") {
                    expected_status = EXPECTED_RANDOM;
                } else if (stat == "skip") {
                    skip = true;
                } else if (stat == "silentfail") {
                    allow_silent_fail = true;
                }
            }
        }

        if (items.length > origLength) {
            // Implies we broke out of the loop before we finished processing
            // defaults. This means defaults contained an invalid token.
            throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": invalid defaults token '" + items[0] + "'";
        }

        if (minAsserts > maxAsserts) {
            throw "Bad range in manifest file " + aURL.spec + " line " + lineNo;
        }

        var runHttp = false;
        var httpDepth;
        if (items[0] == "HTTP") {
            runHttp = (aURL.scheme == "file"); // We can't yet run the local HTTP server
                                               // for non-local reftests.
            httpDepth = 0;
            items.shift();
        } else if (items[0].match(/HTTP\(\.\.(\/\.\.)*\)/)) {
            // Accept HTTP(..), HTTP(../..), HTTP(../../..), etc.
            runHttp = (aURL.scheme == "file"); // We can't yet run the local HTTP server
                                               // for non-local reftests.
            httpDepth = (items[0].length - 5) / 3;
            items.shift();
        }

        // do not prefix the url for include commands or urls specifying
        // a protocol
        if (urlprefix && items[0] != "include") {
            if (items.length > 1 && !items[1].match(RE_PROTOCOL)) {
                items[1] = urlprefix + items[1];
            }
            if (items.length > 2 && !items[2].match(RE_PROTOCOL)) {
                items[2] = urlprefix + items[2];
            }
        }

        var principal = secMan.createContentPrincipal(aURL, {});

        if (items[0] == "include") {
            if (items.length != 2)
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": incorrect number of arguments to include";
            if (runHttp)
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": use of include with http";

            // If the expected_status is EXPECTED_PASS (the default) then allow
            // the include. If 'skip' is true, that means there was a skip
            // or skip-if annotation (with a true condition) on this include
            // statement, so we should skip the include. Any other expected_status
            // is disallowed since it's nonintuitive as to what the intended
            // effect is.
            if (nonSkipUsed) {
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": include statement with annotation other than 'skip' or 'skip-if'";
            } else if (skip) {
                g.logger.info("Skipping included manifest at " + aURL.spec + " line " + lineNo + " due to matching skip condition");
            } else {
                // poor man's assertion
                if (expected_status != EXPECTED_PASS) {
                    throw "Error in manifest file parsing code: we should never get expected_status=" + expected_status + " when nonSkipUsed=false (from " + aURL.spec + " line " + lineNo + ")";
                }

                var incURI = g.ioService.newURI(items[1], null, listURL);
                secMan.checkLoadURIWithPrincipal(principal, incURI,
                                                 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);

                // Cannot use nsIFile or similar to manipulate the manifest ID; although it appears
                // path-like, it does not refer to an actual path in the filesystem.
                var newManifestID = aManifestID;
                var included = items[1];
                // Remove included manifest file name.
                // eg. dir1/dir2/reftest.list -> dir1/dir2
                var pos = included.lastIndexOf("/");
                if (pos <= 0) {
                    included = "";
                } else {
                    included = included.substring(0, pos);
                }
                // Simplify references to parent directories.
                // eg. dir1/dir2/../dir3 -> dir1/dir3
                while (included.startsWith("../")) {
                    pos = newManifestID.lastIndexOf("/");
                    if (pos < 0) {
                        pos = 0;
                    }
                    newManifestID = newManifestID.substring(0, pos);
                    included = included.substring(3);
                }
                // Use a new manifest ID if the included manifest is in a different directory.
                if (included.length > 0) {
                    if (newManifestID.length > 0) {
                        newManifestID = newManifestID + "/" + included;
                    } else {
                        // parent directory includes may refer to the topsrcdir
                        newManifestID = included;
                    }
                }
                ReadManifest(incURI, aFilter, newManifestID);
            }
        } else if (items[0] == TYPE_LOAD || items[0] == TYPE_SCRIPT) {
            var type = items[0];
            if (items.length != 2)
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": incorrect number of arguments to " + type;
            if (type == TYPE_LOAD && expected_status != EXPECTED_PASS)
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": incorrect known failure type for load test";
            AddTestItem({ type: type,
                          expected: expected_status,
                          manifest: aURL.spec,
                          manifestID: TestIdentifier(aURL.spec, aManifestID),
                          allowSilentFail: allow_silent_fail,
                          minAsserts: minAsserts,
                          maxAsserts: maxAsserts,
                          needsFocus: needs_focus,
                          slow: slow,
                          skip: skip,
                          prefSettings1: testPrefSettings,
                          prefSettings2: refPrefSettings,
                          fuzzyMinDelta: fuzzy_delta.min,
                          fuzzyMaxDelta: fuzzy_delta.max,
                          fuzzyMinPixels: fuzzy_pixels.min,
                          fuzzyMaxPixels: fuzzy_pixels.max,
                          runHttp: runHttp,
                          httpDepth: httpDepth,
                          url1: items[1],
                          url2: null,
                          chaosMode: chaosMode,
                          wrCapture: wrCapture,
                          noAutoFuzz: noAutoFuzz }, aFilter, aManifestID);
        } else if (items[0] == TYPE_REFTEST_EQUAL || items[0] == TYPE_REFTEST_NOTEQUAL || items[0] == TYPE_PRINT) {
            if (items.length != 3)
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": incorrect number of arguments to " + items[0];

            if (items[0] == TYPE_REFTEST_NOTEQUAL &&
                expected_status == EXPECTED_FUZZY &&
                (fuzzy_delta.min > 0 || fuzzy_pixels.min > 0)) {
                throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": minimum fuzz must be zero for tests of type " + items[0];
            }

            var type = items[0];
            if (g.compareRetainedDisplayLists) {
                type = TYPE_REFTEST_EQUAL;

                // We expect twice as many assertion failures when comparing
                // tests because we run each test twice.
                minAsserts *= 2;
                maxAsserts *= 2;

                // Skip the test if it is expected to fail in both modes.
                // It would unexpectedly "pass" in comparison mode mode when
                // comparing the two failures, which is not a useful result.
                if (expected_status === EXPECTED_FAIL ||
                    expected_status === EXPECTED_RANDOM) {
                    skip = true;
                }
            }

            AddTestItem({ type: type,
                          expected: expected_status,
                          manifest: aURL.spec,
                          manifestID: TestIdentifier(aURL.spec, aManifestID),
                          allowSilentFail: allow_silent_fail,
                          minAsserts: minAsserts,
                          maxAsserts: maxAsserts,
                          needsFocus: needs_focus,
                          slow: slow,
                          skip: skip,
                          prefSettings1: testPrefSettings,
                          prefSettings2: refPrefSettings,
                          fuzzyMinDelta: fuzzy_delta.min,
                          fuzzyMaxDelta: fuzzy_delta.max,
                          fuzzyMinPixels: fuzzy_pixels.min,
                          fuzzyMaxPixels: fuzzy_pixels.max,
                          runHttp: runHttp,
                          httpDepth: httpDepth,
                          url1: items[1],
                          url2: items[2],
                          chaosMode: chaosMode,
                          wrCapture: wrCapture,
                          noAutoFuzz: noAutoFuzz }, aFilter, aManifestID);
        } else {
            throw "Error in manifest file " + aURL.spec + " line " + lineNo + ": unknown test type " + items[0];
        }
    }
}

// Read all available data from an input stream and return it
// as a string.
function getStreamContent(inputStream)
{
    var streamBuf = "";
    var sis = Cc["@mozilla.org/scriptableinputstream;1"].
                  createInstance(Ci.nsIScriptableInputStream);
    sis.init(inputStream);

    var available;
    while ((available = sis.available()) != 0) {
        streamBuf += sis.read(available);
    }

    return streamBuf;
}

// Build the sandbox for fails-if(), etc., condition evaluation.
function BuildConditionSandbox(aURL) {
    var sandbox = new Cu.Sandbox(aURL.spec);
    var xr = Cc[NS_XREAPPINFO_CONTRACTID].getService(Ci.nsIXULRuntime);
    var appInfo = Cc[NS_XREAPPINFO_CONTRACTID].getService(Ci.nsIXULAppInfo);
    sandbox.isDebugBuild = g.debug.isDebugBuild;
    sandbox.isCoverageBuild = g.isCoverageBuild;
    var prefs = Cc["@mozilla.org/preferences-service;1"].
                getService(Ci.nsIPrefBranch);

    sandbox.xulRuntime = Cu.cloneInto({widgetToolkit: xr.widgetToolkit, OS: xr.OS, XPCOMABI: xr.XPCOMABI}, sandbox);

    var testRect = g.browser.getBoundingClientRect();
    sandbox.smallScreen = false;
    if (g.containingWindow.innerWidth < 800 || g.containingWindow.innerHeight < 1000) {
        sandbox.smallScreen = true;
    }

    var gfxInfo = (NS_GFXINFO_CONTRACTID in Cc) && Cc[NS_GFXINFO_CONTRACTID].getService(Ci.nsIGfxInfo);
    let readGfxInfo = function (obj, key) {
      if (g.contentGfxInfo && (key in g.contentGfxInfo)) {
        return g.contentGfxInfo[key];
      }
      return obj[key];
    }

    try {
      sandbox.d2d = readGfxInfo(gfxInfo, "D2DEnabled");
      sandbox.dwrite = readGfxInfo(gfxInfo, "DWriteEnabled");
      sandbox.embeddedInFirefoxReality = readGfxInfo(gfxInfo, "EmbeddedInFirefoxReality");
    } catch (e) {
      sandbox.d2d = false;
      sandbox.dwrite = false;
      sandbox.embeddedInFirefoxReality = false;
    }

    var canvasBackend = readGfxInfo(gfxInfo, "AzureCanvasBackend");
    var contentBackend = readGfxInfo(gfxInfo, "AzureContentBackend");

    sandbox.gpuProcess = gfxInfo.usingGPUProcess;
    sandbox.azureCairo = canvasBackend == "cairo";
    sandbox.azureSkia = canvasBackend == "skia";
    sandbox.skiaContent = contentBackend == "skia";
    sandbox.azureSkiaGL = false;
    // true if we are using the same Azure backend for rendering canvas and content
    sandbox.contentSameGfxBackendAsCanvas = contentBackend == canvasBackend
                                            || (contentBackend == "none" && canvasBackend == "cairo");

    sandbox.remoteCanvas = prefs.getBoolPref("gfx.canvas.remote") && sandbox.d2d && sandbox.gpuProcess;

    sandbox.layersGPUAccelerated =
      g.windowUtils.layerManagerType != "Basic";
    sandbox.d3d11 =
      g.windowUtils.layerManagerType == "Direct3D 11";
    sandbox.d3d9 =
      g.windowUtils.layerManagerType == "Direct3D 9";
    sandbox.layersOpenGL =
      g.windowUtils.layerManagerType == "OpenGL";
    sandbox.swgl =
      g.windowUtils.layerManagerType.startsWith("WebRender (Software");
    sandbox.layersOMTC =
      g.windowUtils.layerManagerRemote == true;

    // Shortcuts for widget toolkits.
    sandbox.Android = xr.OS == "Android";
    sandbox.cocoaWidget = xr.widgetToolkit == "cocoa";
    sandbox.gtkWidget = xr.widgetToolkit == "gtk";
    sandbox.qtWidget = xr.widgetToolkit == "qt";
    sandbox.winWidget = xr.widgetToolkit == "windows";

    sandbox.is64Bit = xr.is64Bit;

    // Use this to annotate reftests that fail in drawSnapshot, but
    // the reason hasn't been investigated (or fixed) yet.
    sandbox.useDrawSnapshot = g.useDrawSnapshot;
    // Use this to annotate reftests that use functionality
    // that isn't available to drawSnapshot (like any sort of
    // compositor feature such as async scrolling).
    sandbox.unsupportedWithDrawSnapshot = g.useDrawSnapshot;

    sandbox.retainedDisplayList =
      prefs.getBoolPref("layout.display-list.retain") && !sandbox.useDrawSnapshot;

    // Needed to specifically test the new and old behavior. This will eventually be removed.
    sandbox.retainedDisplayListNew =
        sandbox.retainedDisplayList && prefs.getBoolPref("layout.display-list.retain.sc");

    // GeckoView is currently uniquely identified by "android + e10s" but
    // we might want to make this condition more precise in the future.
    sandbox.geckoview = (sandbox.Android && g.browserIsRemote);

    // Scrollbars that are semi-transparent. See bug 1169666.
    sandbox.transparentScrollbars = xr.widgetToolkit == "gtk";

    var sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
    if (sandbox.Android) {
        // This is currently used to distinguish Android 4.0.3 (SDK version 15)
        // and later from Android 2.x
        sandbox.AndroidVersion = sysInfo.getPropertyAsInt32("version");

        sandbox.emulator = readGfxInfo(gfxInfo, "adapterDeviceID").includes("Android Emulator");
        sandbox.device = !sandbox.emulator;
    }

    sandbox.MinGW = sandbox.winWidget && sysInfo.getPropertyAsBool("isMinGW");

    sandbox.AddressSanitizer = AppConstants.ASAN;
    sandbox.ThreadSanitizer = AppConstants.TSAN;
    sandbox.webrtc = AppConstants.MOZ_WEBRTC;
    sandbox.jxl = AppConstants.MOZ_JXL;

    sandbox.compareRetainedDisplayLists = g.compareRetainedDisplayLists;

    sandbox.release_or_beta = AppConstants.RELEASE_OR_BETA;

    var hh = Cc[NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX + "http"].
                 getService(Ci.nsIHttpProtocolHandler);
    var httpProps = ["userAgent", "appName", "appVersion", "vendor",
                     "vendorSub", "product", "productSub", "platform",
                     "oscpu", "language", "misc"];
    sandbox.http = new sandbox.Object();
    httpProps.forEach((x) => sandbox.http[x] = hh[x]);

    // set to specific Android13 version (Pixel 5 in CI)
    sandbox.Android13 = sandbox.Android && (sandbox.http["platform"] == "Android 13");

    // Set OSX to be the Mac OS X version, as an integer, or undefined
    // for other platforms.  The integer is formed by 100 times the
    // major version plus the minor version, so 1006 for 10.6, 1010 for
    // 10.10, etc.
    var osxmatch = /Mac OS X (\d+).(\d+)$/.exec(hh.oscpu);
    sandbox.OSX = osxmatch ? parseInt(osxmatch[1]) * 100 + parseInt(osxmatch[2]) : undefined;

    // config specific prefs
    sandbox.appleSilicon = prefs.getBoolPref("sandbox.apple_silicon", false);

    // Set a flag on sandbox if the windows default theme is active
    sandbox.windowsDefaultTheme = g.containingWindow.matchMedia("(-moz-windows-default-theme)").matches;
    sandbox.gpuProcessForceEnabled = prefs.getBoolPref("layers.gpu-process.force-enabled", false);

    sandbox.prefs = Cu.cloneInto({
        getBoolPref: function(p) { return prefs.getBoolPref(p); },
        getIntPref:  function(p) { return prefs.getIntPref(p); }
    }, sandbox, { cloneFunctions: true });

    // Tests shouldn't care about this except for when they need to
    // crash the content process
    sandbox.browserIsRemote = g.browserIsRemote;
    sandbox.browserIsFission = g.browserIsFission;

    try {
        sandbox.asyncPan = g.containingWindow.docShell.asyncPanZoomEnabled && !sandbox.useDrawSnapshot;
    } catch (e) {
        sandbox.asyncPan = false;
    }

    // Graphics features
    sandbox.usesRepeatResampling = sandbox.d2d;

    // Running in a test-verify session?
    sandbox.verify = prefs.getBoolPref("reftest.verify", false);

    // Running with a variant enabled?
    sandbox.fission = Services.appinfo.fissionAutostart;
    sandbox.serviceWorkerE10s = true;

    if (!g.dumpedConditionSandbox) {
        g.logger.info("Dumping representation of sandbox which can be used for expectation annotations");
        for (let entry of Object.entries(Cu.waiveXrays(sandbox)).sort((a, b) => a[0].localeCompare(b[0]))) {
            let value = typeof entry[1] === "object" ? JSON.stringify(entry[1]) : entry[1];
            g.logger.info(`    ${entry[0]}: ${value}`);
        }
        g.dumpedConditionSandbox = true;
    }

    return sandbox;
}

function AddRetainedDisplayListTestPrefs(aSandbox, aTestPrefSettings,
                                         aRefPrefSettings) {
    AddPrefSettings("test-", "layout.display-list.retain", "true", aSandbox,
                    aTestPrefSettings, aRefPrefSettings);
    AddPrefSettings("ref-", "layout.display-list.retain", "false", aSandbox,
                    aTestPrefSettings, aRefPrefSettings);
}

function AddPrefSettings(aWhere, aPrefName, aPrefValExpression, aSandbox, aTestPrefSettings, aRefPrefSettings) {
    var prefVal = Cu.evalInSandbox("(" + aPrefValExpression + ")", aSandbox);
    var prefType;
    var valType = typeof(prefVal);
    if (valType == "boolean") {
        prefType = PREF_BOOLEAN;
    } else if (valType == "string") {
        prefType = PREF_STRING;
    } else if (valType == "number" && (parseInt(prefVal) == prefVal)) {
        prefType = PREF_INTEGER;
    } else {
        return false;
    }
    var setting = { name: aPrefName,
                    type: prefType,
                    value: prefVal };

    if (g.compareRetainedDisplayLists && aPrefName != "layout.display-list.retain") {
        // ref-pref() is ignored, test-pref() and pref() are added to both
        if (aWhere != "ref-") {
            aTestPrefSettings.push(setting);
            aRefPrefSettings.push(setting);
        }
    } else {
        if (aWhere != "ref-") {
            aTestPrefSettings.push(setting);
        }
        if (aWhere != "test-") {
            aRefPrefSettings.push(setting);
        }
    }
    return true;
}

function ExtractRange(matches, startIndex) {
    return {
        min: Number(matches[startIndex]),
        max: Number(matches[startIndex + 1])
    };
}

function ServeTestBase(aURL, depth) {
    var listURL = aURL.QueryInterface(Ci.nsIFileURL);
    var directory = listURL.file.parent;

    // Allow serving a tree that's an ancestor of the directory containing
    // the files so that they can use resources in ../ (etc.).
    var dirPath = "/";
    while (depth > 0) {
        dirPath = "/" + directory.leafName + dirPath;
        directory = directory.parent;
        --depth;
    }

    g.count++;
    var path = "/" + Date.now() + "/" + g.count;
    g.server.registerDirectory(path + "/", directory);
    // this one is needed so tests can use example.org urls for cross origin testing
    g.server.registerDirectory("/", directory);

    return g.ioService.newURI("http://localhost:" + g.httpServerPort + path + dirPath);
}

function CreateUrls(test) {
    let secMan = Cc[NS_SCRIPTSECURITYMANAGER_CONTRACTID]
                    .getService(Ci.nsIScriptSecurityManager);

    let manifestURL = g.ioService.newURI(test.manifest);

    let testbase = manifestURL;
    if (test.runHttp) {
        testbase = ServeTestBase(manifestURL, test.httpDepth)
    }

    let testbasePrincipal = secMan.createContentPrincipal(testbase, {});
    Services.perms.addFromPrincipal(testbasePrincipal, "allowXULXBL", Services.perms.ALLOW_ACTION);

    function FileToURI(file)
    {
        if (file === null)
            return file;

        var testURI = g.ioService.newURI(file, null, testbase);
        let isChromeOrViewSource = testURI.scheme == "chrome" || testURI.scheme == "view-source";
        let principal = isChromeOrViewSource ? secMan.getSystemPrincipal() :
                                               secMan.createContentPrincipal(manifestURL, {});
        secMan.checkLoadURIWithPrincipal(principal, testURI,
                                         Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
        return testURI;
    }

    let files = [test.url1, test.url2];
    [test.url1, test.url2] = files.map(FileToURI);

    return test;
}

function TestIdentifier(aUrl, aManifestID) {
    // Construct a platform-independent and location-independent test identifier for
    // a url; normally the identifier looks like a posix-compliant relative file
    // path.
    // Test urls may be simple file names, chrome: urls with full paths, about:blank, etc.
    if (aUrl.startsWith("about:") || aUrl.startsWith("data:")) {
        return aUrl;
    }
    var pos = aUrl.lastIndexOf("/");
    var url = (pos < 0) ? aUrl : aUrl.substring(pos + 1);
    return (aManifestID + "/" + url);
}

function AddTestItem(aTest, aFilter, aManifestID) {
    if (!aFilter)
        aFilter = [null, [], false];

    var identifier = TestIdentifier(aTest.url1, aManifestID);
    if (aTest.url2 !== null) {
        identifier = [identifier, aTest.type, TestIdentifier(aTest.url2, aManifestID)];
    }

    var {url1, url2} = CreateUrls(Object.assign({}, aTest));

    var globalFilter = aFilter[0];
    var manifestFilter = aFilter[1];
    var invertManifest = aFilter[2];
    if (globalFilter && !globalFilter.test(url1.spec)) {
        if (url2 === null)
            return;
        if (globalFilter && !globalFilter.test(url2.spec))
            return;
    }
    if (manifestFilter && !(invertManifest ^ manifestFilter.test(url1.spec))) {
        if (url2 === null)
            return;
        if (manifestFilter && !(invertManifest ^ manifestFilter.test(url2.spec)))
            return;
    }
    if (g.focusFilterMode == FOCUS_FILTER_NEEDS_FOCUS_TESTS &&
        !aTest.needsFocus)
        return;
    if (g.focusFilterMode == FOCUS_FILTER_NON_NEEDS_FOCUS_TESTS &&
        aTest.needsFocus)
        return;

    aTest.identifier = identifier;
    g.urls.push(aTest);
    // Periodically log progress to avoid no-output timeout on slow platforms.
    // No-output timeouts during manifest parsing have been a problem for
    // jsreftests on Android/debug. Any logging resets the no-output timer,
    // even debug logging which is normally not displayed.
    if ((g.urls.length % 5000) == 0)
        g.logger.debug(g.urls.length + " tests found...");
}
