/* import-globals-from head_crashreporter.js */

let gTestCrasherSyms = null;
let gModules = null;

// Returns the offset (int) of an IP with a given base address.
// This is effectively (ip - base), except a bit more complication due to
// Javascript's shaky handling of 64-bit integers.
// base & ip are passed as hex strings.
function getModuleOffset(base, ip) {
  let i = 0;
  // Find where the two addresses diverge, which enables us to perform a 32-bit
  // subtraction.
  // e.g.    "0x1111111111112222"
  //       - "0x1111111111111111"
  // becomes 2222 - 1111
  for (; i < base.length; ++i) {
    if (base[i] != ip[i]) {
      break;
    }
  }
  if (i == base.length) {
    return 0;
  }
  let lhs2 = "0x" + base.substring(i);
  let rhs2 = "0x" + ip.substring(i);
  return parseInt(rhs2) - parseInt(lhs2);
}

// Uses gTestCrasherSyms to convert an address to a symbol.
function findNearestTestCrasherSymbol(addr) {
  addr += 1; // Breakpad sometimes offsets addresses; correct for this.
  let closestDistance = null;
  let closestSym = null;
  for (let sym in gTestCrasherSyms) {
    if (addr >= gTestCrasherSyms[sym]) {
      let thisDistance = addr - gTestCrasherSyms[sym];
      if (closestDistance === null || thisDistance < closestDistance) {
        closestDistance = thisDistance;
        closestSym = sym;
      }
    }
  }
  if (closestSym === null) {
    return null;
  }
  return { symbol: closestSym, offset: closestDistance };
}

// Populate known symbols for testcrasher.dll.
// Use the same prop names as from CrashTestUtils to avoid the need for mapping.
function initTestCrasherSymbols() {
  gTestCrasherSyms = {};
  for (let k in CrashTestUtils) {
    // Not all keys here are valid symbol names. getWin64CFITestFnAddrOffset
    // will return 0 in those cases, no need to filter here.
    if (Number.isInteger(CrashTestUtils[k])) {
      let t = CrashTestUtils.getWin64CFITestFnAddrOffset(CrashTestUtils[k]);
      if (t > 0) {
        gTestCrasherSyms[k] = t;
      }
    }
  }
}

function stackFrameToString(frameIndex, frame) {
  // Calculate the module offset.
  let ip = frame.ip;
  let symbol = "";
  let moduleOffset = "unknown_offset";
  let filename = "unknown_module";

  if (
    typeof frame.module_index !== "undefined" &&
    frame.module_index >= 0 &&
    frame.module_index < gModules.length
  ) {
    let base = gModules[frame.module_index].base_addr;
    moduleOffset = getModuleOffset(base, ip);
    filename = gModules[frame.module_index].filename;

    if (filename === "testcrasher.dll") {
      let nearestSym = findNearestTestCrasherSymbol(moduleOffset);
      if (nearestSym !== null) {
        symbol = nearestSym.symbol + "+" + nearestSym.offset.toString(16);
      }
    }
  }

  let ret =
    "frames[" +
    frameIndex +
    "] ip=" +
    ip +
    " " +
    symbol +
    ", module:" +
    filename +
    ", trust:" +
    frame.trust +
    ", moduleOffset:" +
    moduleOffset.toString(16);
  return ret;
}

function dumpStackFrames(frames, maxFrames) {
  for (let i = 0; i < Math.min(maxFrames, frames.length); ++i) {
    info(stackFrameToString(i, frames[i]));
  }
}

// Test that the top of the given stack (from extra data) matches the given
// expected frames.
//
// expected is { symbol: "", trust: "" }
function assertStack(stack, expected) {
  for (let i = 0; i < stack.length; ++i) {
    if (i >= expected.length) {
      ok("Top stack frames were expected");
      return;
    }
    let frame = stack[i];
    let expectedFrame = expected[i];
    let dumpThisFrame = function () {
      info("  Actual frame: " + stackFrameToString(i, frame));
      info(
        "Expected { symbol: " +
          expectedFrame.symbol +
          ", trust: " +
          expectedFrame.trust +
          "}"
      );
    };

    if (expectedFrame.trust) {
      if (expectedFrame.trust.startsWith("!")) {
        // A "!" prefix on the frame trust matching is a logical "not".
        if (frame.trust === expectedFrame.trust.substring(1)) {
          dumpThisFrame();
          info("Expected frame trust matched when it should not have.");
          ok(false);
        }
      } else if (frame.trust !== expectedFrame.trust) {
        dumpThisFrame();
        info("Expected frame trust did not match.");
        ok(false);
      }
    }

    if (expectedFrame.symbol) {
      if (typeof frame.module_index === "undefined") {
        // Without a module_index, it happened in an unknown module. Currently
        // you can't specify an expected "unknown" module.
        info("Unknown symbol in unknown module.");
        ok(false);
      }
      if (frame.module_index < 0 || frame.module_index >= gModules.length) {
        dumpThisFrame();
        info("Unknown module.");
        ok(false);
        return;
      }
      let base = gModules[frame.module_index].base_addr;
      let moduleOffset = getModuleOffset(base, frame.ip);
      let filename = gModules[frame.module_index].filename;
      if (filename == "testcrasher.dll") {
        let nearestSym = findNearestTestCrasherSymbol(moduleOffset);
        if (nearestSym === null) {
          dumpThisFrame();
          info("Unknown symbol.");
          ok(false);
          return;
        }

        if (nearestSym.symbol !== expectedFrame.symbol) {
          dumpThisFrame();
          info("Mismatching symbol.");
          ok(false);
        }
      }
    }
  }
}

// Performs a crash, runs minidump-analyzer, and checks expected stack analysis.
//
// how: The crash to perform. Constants defined in both CrashTestUtils.jsm
//   and nsTestCrasher.cpp (i.e. CRASH_X64CFI_PUSH_NONVOL)
// expectedStack: An array of {"symbol", "trust"} where trust is "cfi",
//   "context", "scan", et al. May be null if you don't need to check the stack.
// minidumpAnalyzerArgs: An array of additional arguments to pass to
//   minidump-analyzer.exe.
async function do_x64CFITest(how, expectedStack, minidumpAnalyzerArgs) {
  // Setup is run in the subprocess so we cannot use any closures.
  let setupFn = "crashType = CrashTestUtils." + how + ";";

  let callbackFn = async function (minidumpFile, extra, extraFile) {
    runMinidumpAnalyzer(minidumpFile, minidumpAnalyzerArgs);

    // Refresh updated extra data
    extra = await IOUtils.readJSON(extraFile.path);

    initTestCrasherSymbols();
    let stackTraces = extra.StackTraces;
    let crashingThreadIndex = stackTraces.crash_info.crashing_thread;
    gModules = stackTraces.modules;
    let crashingFrames = stackTraces.threads[crashingThreadIndex].frames;

    dumpStackFrames(crashingFrames, 10);

    assertStack(crashingFrames, expectedStack);
  };

  do_crash(setupFn, callbackFn, true, true);
}
