// Workaround for https://github.com/blackboardsh/electrobun/issues/485
//
// Electrobun's darwin-x64 core binaries (launcher, extractor, libasar.dylib,
// bsdiff, zig-asar in v1.18.1) are Zig-built with zero/near-zero Mach-O
// headerpad and no code signature. When codesign adds the 16-byte
// LC_CODE_SIGNATURE load command it silently overwrites the first bytes of
// __text, and the signed binary segfaults on launch (Intel Macs only;
// arm64 binaries always ship with a signature slot that is re-signed in
// place).
//
// This script frees room in the load-command area by dropping the 16-byte
// LC_SOURCE_VERSION command (LC_UUID as a fallback) from any thin x86_64
// Mach-O that has less than 16 bytes of headerpad and no existing code
// signature. It is idempotent and a no-op once upstream ships rebuilt
// binaries with headerpad_size set.

import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const MH_MAGIC_64 = 0xfeedfacf;
const CPU_TYPE_X86_64 = 0x01000007;
const LC_SEGMENT_64 = 0x19;
const LC_UUID = 0x1b;
const LC_CODE_SIGNATURE = 0x1d;
const LC_SOURCE_VERSION = 0x2a;
const HEADER_SIZE = 32;
const CODE_SIGNATURE_CMD_SIZE = 16;

function analyzeMachO(buffer) {
  if (buffer.length < HEADER_SIZE || buffer.readUInt32LE(0) !== MH_MAGIC_64) {
    return null;
  }

  const cputype = buffer.readInt32LE(4);
  const ncmds = buffer.readUInt32LE(16);
  const sizeofcmds = buffer.readUInt32LE(20);

  let offset = HEADER_SIZE;
  let firstSectionOffset = null;
  let hasCodeSignature = false;
  let sourceVersionCmd = null;
  let uuidCmd = null;

  for (let i = 0; i < ncmds; i += 1) {
    const cmd = buffer.readUInt32LE(offset);
    const cmdsize = buffer.readUInt32LE(offset + 4);

    if (cmd === LC_CODE_SIGNATURE) {
      hasCodeSignature = true;
    } else if (cmd === LC_SOURCE_VERSION) {
      sourceVersionCmd = { offset, cmdsize };
    } else if (cmd === LC_UUID) {
      uuidCmd = { offset, cmdsize };
    } else if (cmd === LC_SEGMENT_64) {
      const nsects = buffer.readUInt32LE(offset + 64);
      let sectionOffset = offset + 72;
      for (let s = 0; s < nsects; s += 1) {
        const size = Number(buffer.readBigUInt64LE(sectionOffset + 40));
        const fileOffset = buffer.readUInt32LE(sectionOffset + 48);
        if (fileOffset > 0 && size > 0) {
          firstSectionOffset =
            firstSectionOffset === null
              ? fileOffset
              : Math.min(firstSectionOffset, fileOffset);
        }
        sectionOffset += 80;
      }
    }

    offset += cmdsize;
  }

  return {
    cputype,
    ncmds,
    sizeofcmds,
    headerpad:
      firstSectionOffset === null
        ? Infinity
        : firstSectionOffset - (HEADER_SIZE + sizeofcmds),
    hasCodeSignature,
    removableCmd: sourceVersionCmd ?? uuidCmd,
  };
}

function fixFile(filePath) {
  const buffer = readFileSync(filePath);
  const info = analyzeMachO(buffer);
  const name = path.basename(filePath);

  if (!info || info.cputype !== CPU_TYPE_X86_64) {
    return;
  }

  if (info.hasCodeSignature || info.headerpad >= CODE_SIGNATURE_CMD_SIZE) {
    return;
  }

  if (!info.removableCmd) {
    console.warn(
      `fix-macos-headerpad: ${name} has headerpad ${info.headerpad} but no removable load command; signing may corrupt it`,
    );
    return;
  }

  const { offset, cmdsize } = info.removableCmd;
  const loadCommandsEnd = HEADER_SIZE + info.sizeofcmds;

  // Shift the remaining load commands over the removed one, then zero the
  // freed tail so codesign finds clean padding.
  buffer.copyWithin(offset, offset + cmdsize, loadCommandsEnd);
  buffer.fill(0, loadCommandsEnd - cmdsize, loadCommandsEnd);
  buffer.writeUInt32LE(info.ncmds - 1, 16);
  buffer.writeUInt32LE(info.sizeofcmds - cmdsize, 20);

  writeFileSync(filePath, buffer);
  console.log(
    `fix-macos-headerpad: ${name} freed ${cmdsize} bytes for LC_CODE_SIGNATURE (headerpad was ${info.headerpad})`,
  );
}

export function fixMacosHeaderpad(directory) {
  if (process.platform !== "darwin") {
    return;
  }

  let entries;
  try {
    entries = readdirSync(directory);
  } catch {
    return;
  }

  for (const entry of entries) {
    const filePath = path.join(directory, entry);
    if (statSync(filePath).isFile()) {
      fixFile(filePath);
    }
  }
}

const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
const modulePath = fileURLToPath(import.meta.url);

if (invokedPath === modulePath) {
  const rootDir = path.resolve(path.dirname(modulePath), "..");
  const target =
    process.argv[2] ||
    path.join(rootDir, "node_modules/electrobun/dist-macos-x64");
  fixMacosHeaderpad(path.resolve(target));
}
