neo-decompiler 0.6.2

Minimal tooling for inspecting Neo N3 NEF bytecode
Documentation
import { jumpTarget } from "./high-level-utils.js";
import {
  collectDerivedWarnings,
  rewriteForLoops,
} from "./high-level-control-flow-shared.js";

export function createTryHelpers(runtime) {
  const { createState, cloneState, executeStraightLine } = runtime;

  function tryLiftSimpleTryBlock(instructions, manifestMethod, context, methodOffset) {
    const tryIndex = instructions.findIndex(
      (instruction) => instruction.opcode.mnemonic === "TRY",
    );
    if (tryIndex < 0) {
      return null;
    }

    const tryInstruction = instructions[tryIndex];
    const handlerTargets = tryHandlerTargets(tryInstruction);
    if (handlerTargets === null) {
      return null;
    }

    const { bodyStart, catchTarget, finallyTarget } = handlerTargets;
    const indexByOffset = new Map(
      instructions.map((instruction, index) => [instruction.offset, index]),
    );

    const bodyStartIndex = indexByOffset.get(bodyStart);
    if (bodyStartIndex === undefined) {
      return null;
    }

    const endtryIndexInSlice = instructions.slice(bodyStartIndex).findIndex(
      (instruction) => instruction.opcode.mnemonic === "ENDTRY",
    );
    if (endtryIndexInSlice < 0) {
      return null;
    }

    const endtryGlobalIndex = bodyStartIndex + endtryIndexInSlice;
    let catchSlice = [];
    let finallySlice = [];
    let resumeSlice = [];
    let allowBareTry = false;

    if (catchTarget !== null && finallyTarget !== null) {
      const catchIndex = indexByOffset.get(catchTarget);
      const finallyIndex = indexByOffset.get(finallyTarget);
      if (catchIndex === undefined || finallyIndex === undefined) {
        return null;
      }

      const endtrySliceStart = endtryGlobalIndex + 1;
      const finallyEndIndexInSlice = instructions.slice(endtrySliceStart).findIndex(
        (instruction) => instruction.opcode.mnemonic === "ENDFINALLY",
      );
      let finallyEndGlobalIndex = null;
      if (finallyEndIndexInSlice >= 0) {
        finallyEndGlobalIndex = endtrySliceStart + finallyEndIndexInSlice;
      }

      catchSlice = instructions.slice(catchIndex, finallyIndex);
      if (finallyEndGlobalIndex !== null) {
        finallySlice = instructions.slice(finallyIndex, finallyEndGlobalIndex);
        resumeSlice = instructions.slice(finallyEndGlobalIndex + 1);
      }
    } else if (catchTarget !== null) {
      const catchIndex = indexByOffset.get(catchTarget);
      if (catchIndex === undefined) {
        return null;
      }

      const afterEndtryIndex = endtryGlobalIndex + 1;
      const catchEndtryIndexInSlice = instructions.slice(afterEndtryIndex).findIndex(
        (instruction) => instruction.opcode.mnemonic === "ENDTRY",
      );
      let catchEndGlobalIndex = null;
      if (catchEndtryIndexInSlice >= 0) {
        catchEndGlobalIndex = afterEndtryIndex + catchEndtryIndexInSlice;
        const catchEndInstruction = instructions[catchEndGlobalIndex];
        const catchEndTarget = jumpTarget(catchEndInstruction);
        if (catchEndTarget !== null && catchEndTarget > catchTarget) {
          catchSlice = instructions.slice(catchIndex, indexByOffset.get(catchEndTarget));
          resumeSlice = instructions.slice(catchEndGlobalIndex + 1);
        }
      }

      if (
        catchSlice.length === 0 &&
        instructions[catchIndex]?.opcode.mnemonic === "ENDFINALLY"
      ) {
        allowBareTry = true;
        resumeSlice = instructions.slice(catchIndex + 1);
      }
    } else if (finallyTarget !== null) {
      const finallyIndex = indexByOffset.get(finallyTarget);
      if (finallyIndex === undefined) {
        return null;
      }

      const finallyEndIndexInSlice = instructions.slice(finallyIndex).findIndex(
        (instruction) => instruction.opcode.mnemonic === "ENDFINALLY",
      );
      if (finallyEndIndexInSlice >= 0) {
        const finallyEndGlobalIndex = finallyIndex + finallyEndIndexInSlice;
        finallySlice = instructions.slice(finallyIndex, finallyEndGlobalIndex);
        resumeSlice = instructions.slice(endtryGlobalIndex + 1);
      }
    }

    if (catchSlice.length === 0 && finallySlice.length === 0 && !allowBareTry) {
      return null;
    }

    const prefixState = createState(manifestMethod, context, methodOffset, instructions);
    executeStraightLine(prefixState, instructions.slice(0, tryIndex));
    const tryBodyState = cloneState(prefixState);
    executeStraightLine(tryBodyState, instructions.slice(bodyStartIndex, endtryGlobalIndex));

    const statements = [...prefixState.statements];
    statements.push("try {");
    statements.push(...tryBodyState.statements.slice(prefixState.statements.length));
    let catchState = null;
    let finallyState = null;
    let resumeState = null;

    if (catchSlice.length > 0) {
      catchState = cloneState(prefixState);
      catchState.stack.push("exception");
      executeStraightLine(catchState, catchSlice);
      statements.push("} catch {");
      statements.push(...catchState.statements.slice(prefixState.statements.length));
    }

    if (finallySlice.length > 0) {
      finallyState = cloneState(prefixState);
      executeStraightLine(finallyState, finallySlice);
      statements.push("} finally {");
      statements.push(...finallyState.statements.slice(prefixState.statements.length));
    }

    statements.push("}");

    if (resumeSlice.length > 0) {
      resumeState = cloneState(prefixState);
      executeStraightLine(resumeState, resumeSlice);
      statements.push(...resumeState.statements.slice(prefixState.statements.length));
    }

    return rewriteForLoops({
      statements,
      warnings: collectDerivedWarnings(
        prefixState,
        tryBodyState,
        catchState,
        finallyState,
        resumeState,
      ),
    });
  }

  return { tryLiftSimpleTryBlock };
}

function tryHandlerTargets(instruction) {
  if (instruction.opcode.mnemonic !== "TRY" && instruction.opcode.mnemonic !== "TRY_L") {
    return null;
  }

  const operand = instruction.operand;
  if (operand === null || operand.kind !== "Bytes") {
    return null;
  }

  const bytes = operand.value;
  let catchDelta;
  let finallyDelta;

  if (bytes.length === 2) {
    catchDelta = bytes[0];
    finallyDelta = bytes[1];
    if (catchDelta > 127) catchDelta -= 256;
    if (finallyDelta > 127) finallyDelta -= 256;
  } else if (bytes.length === 8) {
    catchDelta = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
    finallyDelta = bytes[4] | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24);
    if (catchDelta > 2147483647) catchDelta -= 4294967296;
    if (finallyDelta > 2147483647) finallyDelta -= 4294967296;
  } else {
    return null;
  }

  const width = 1 + bytes.length;
  const bodyStart = instruction.offset + width;

  let catchTarget = null;
  if (catchDelta !== 0) {
    const target = instruction.offset + catchDelta;
    if (target > instruction.offset) {
      catchTarget = target;
    }
  }

  let finallyTarget = null;
  if (finallyDelta !== 0) {
    const target = instruction.offset + finallyDelta;
    if (target > instruction.offset) {
      finallyTarget = target;
    }
  }

  return { bodyStart, catchTarget, finallyTarget };
}