// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * Pure-Node.js ELF binary analysis — no native dependencies.
 *
 * Provides:
 * - ELF header parsing (arch, entry point, type, endianness)
 * - Section enumeration (names, sizes, permissions)
 * - Symbol table extraction (imports/exports)
 * - Security feature detection (NX, PIE, RELRO, stack canaries, RPATH)
 * - Format string vulnerability pattern detection in source code
 * - ROP gadget scanning via objdump subprocess
 *
 * Ported from pipeline/binary_analysis.py (799 LOC Python).
 */

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, extname } from 'node:path';
import { execFileSync } from 'node:child_process';

// ---------------------------------------------------------------------------
// ELF constants
// ---------------------------------------------------------------------------

const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF

// e_ident offsets
const EI_CLASS = 4;
const EI_DATA = 5;

// ELF classes
const ELFCLASS32 = 1;
const ELFCLASS64 = 2;
const CLASS_NAMES = Object.freeze({ [ELFCLASS32]: 'ELF32', [ELFCLASS64]: 'ELF64' });

// Data encoding
const ELFDATA2LSB = 1;
const ELFDATA2MSB = 2;
const ENDIAN_NAMES = Object.freeze({
  [ELFDATA2LSB]: 'little-endian',
  [ELFDATA2MSB]: 'big-endian',
});

// Object file types
const TYPE_NAMES = Object.freeze({
  0: 'NONE', 1: 'REL', 2: 'EXEC', 3: 'DYN', 4: 'CORE',
});

// Machine types
const MACHINE_NAMES = Object.freeze({
  0: 'NONE', 3: 'x86', 8: 'MIPS', 20: 'PowerPC', 40: 'ARM',
  43: 'SPARC v9', 62: 'x86-64', 183: 'AArch64', 243: 'RISC-V',
});

// Section types
const SHT_SYMTAB = 2;
const SHT_STRTAB = 3;
const SHT_DYNSYM = 11;

// Program header types
const PT_DYNAMIC = 2;
const PT_INTERP = 3;
const PT_GNU_RELRO = 0x6474e552;
const PT_GNU_STACK = 0x6474e551;

// Dynamic tags
const DT_NEEDED = 1;
const DT_RPATH = 15;
const DT_RUNPATH = 29;
const DT_FLAGS = 30;
const DT_FLAGS_1 = 0x6ffffffb;
const DF_BIND_NOW = 0x8;
const DF_1_NOW = 0x1;
const DF_1_PIE = 0x08000000;

// Section header flags
const SHF_WRITE = 0x1;
const SHF_ALLOC = 0x2;
const SHF_EXECINSTR = 0x4;

// ---------------------------------------------------------------------------
// Buffer read helpers — abstract endianness
// ---------------------------------------------------------------------------

/**
 * Read unsigned 16-bit integer.
 * @param {Buffer} buf
 * @param {number} off byte offset
 * @param {boolean} le true = little-endian
 * @returns {number}
 */
function readU16(buf, off, le) {
  return le ? buf.readUInt16LE(off) : buf.readUInt16BE(off);
}

/**
 * Read unsigned 32-bit integer.
 * @param {Buffer} buf
 * @param {number} off byte offset
 * @param {boolean} le true = little-endian
 * @returns {number}
 */
function readU32(buf, off, le) {
  return le ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
}

/**
 * Read unsigned 64-bit integer as Number (safe for file offsets < 2^53).
 * @param {Buffer} buf
 * @param {number} off byte offset
 * @param {boolean} le true = little-endian
 * @returns {number}
 */
function readU64(buf, off, le) {
  const big = le ? buf.readBigUInt64LE(off) : buf.readBigUInt64BE(off);
  return Number(big);
}

/**
 * Read signed 32-bit integer.
 * @param {Buffer} buf
 * @param {number} off byte offset
 * @param {boolean} le true = little-endian
 * @returns {number}
 */
function readI32(buf, off, le) {
  return le ? buf.readInt32LE(off) : buf.readInt32BE(off);
}

/**
 * Read signed 64-bit integer as Number.
 * @param {Buffer} buf
 * @param {number} off byte offset
 * @param {boolean} le true = little-endian
 * @returns {number}
 */
function readI64(buf, off, le) {
  const big = le ? buf.readBigInt64LE(off) : buf.readBigInt64BE(off);
  return Number(big);
}

/**
 * Read null-terminated ASCII string from buffer.
 * @param {Buffer} data
 * @param {number} offset
 * @returns {string}
 */
function readStr(data, offset) {
  const end = data.indexOf(0, offset);
  if (end < 0) return '';
  return data.subarray(offset, end).toString('ascii');
}

// ---------------------------------------------------------------------------
// Data classes
// ---------------------------------------------------------------------------

class ELFSection {
  /**
   * @param {object} opts
   * @param {string} opts.name
   * @param {number} opts.typeId
   * @param {number} opts.flags
   * @param {number} opts.addr
   * @param {number} opts.offset
   * @param {number} opts.size
   * @param {number} [opts.link=0]
   * @param {number} [opts.info=0]
   * @param {number} [opts.entsize=0]
   */
  constructor(opts) {
    this.name = opts.name;
    this.typeId = opts.typeId;
    this.flags = opts.flags;
    this.addr = opts.addr;
    this.offset = opts.offset;
    this.size = opts.size;
    this.link = opts.link ?? 0;
    this.info = opts.info ?? 0;
    this.entsize = opts.entsize ?? 0;
  }

  get isExecutable() {
    return Boolean(this.flags & SHF_EXECINSTR);
  }

  get isWritable() {
    return Boolean(this.flags & SHF_WRITE);
  }

  get permissions() {
    const r = this.flags & SHF_ALLOC ? 'R' : '-';
    const w = this.flags & SHF_WRITE ? 'W' : '-';
    const x = this.flags & SHF_EXECINSTR ? 'X' : '-';
    return `${r}${w}${x}`;
  }

  toDict() {
    return {
      name: this.name,
      size: this.size,
      permissions: this.permissions,
      address: '0x' + this.addr.toString(16),
    };
  }
}

class ELFSymbol {
  /**
   * @param {object} opts
   * @param {string} opts.name
   * @param {number} opts.value
   * @param {number} opts.size
   * @param {string} opts.bind  LOCAL | GLOBAL | WEAK
   * @param {string} opts.type  FUNC | OBJECT | NOTYPE
   * @param {number} opts.sectionIndex
   */
  constructor(opts) {
    this.name = opts.name;
    this.value = opts.value;
    this.size = opts.size;
    this.bind = opts.bind;
    this.type = opts.type;
    this.sectionIndex = opts.sectionIndex;
  }

  toDict() {
    return {
      name: this.name,
      value: '0x' + this.value.toString(16),
      bind: this.bind,
      type: this.type,
    };
  }
}

class SecurityFeatures {
  /**
   * @param {object} [opts]
   */
  constructor(opts = {}) {
    this.nx = opts.nx ?? false;
    this.pie = opts.pie ?? false;
    this.relro = opts.relro ?? 'none';
    this.canary = opts.canary ?? false;
    this.fortify = opts.fortify ?? false;
    this.rpath = opts.rpath ?? false;
    this.runpath = opts.runpath ?? false;
    this.stripped = opts.stripped ?? false;
  }

  /** Hardening score 0-100. */
  get score() {
    let s = 0;
    if (this.nx) s += 20;
    if (this.pie) s += 20;
    if (this.relro === 'full') s += 20;
    else if (this.relro === 'partial') s += 10;
    if (this.canary) s += 20;
    if (this.fortify) s += 10;
    if (!this.rpath) s += 10;
    return Math.min(s, 100);
  }

  toDict() {
    return {
      NX: this.nx,
      PIE: this.pie,
      RELRO: this.relro,
      Canary: this.canary,
      Fortify: this.fortify,
      RPATH: this.rpath,
      Stripped: this.stripped,
    };
  }
}

class ELFAnalysis {
  /**
   * @param {object} opts
   * @param {string} opts.filename
   */
  constructor(opts) {
    this.filename = opts.filename;
    this.elfClass = opts.elfClass ?? '';
    this.endianness = opts.endianness ?? '';
    this.elfType = opts.elfType ?? '';
    this.machine = opts.machine ?? '';
    this.entryPoint = opts.entryPoint ?? 0;
    this.sections = opts.sections ?? [];
    this.symbols = opts.symbols ?? [];
    this.imports = opts.imports ?? [];
    this.libraries = opts.libraries ?? [];
    this.security = opts.security ?? new SecurityFeatures();
    this.suspicious = opts.suspicious ?? [];
    this.errors = opts.errors ?? [];
  }

  toDict() {
    return {
      filename: this.filename,
      class: this.elfClass,
      endianness: this.endianness,
      type: this.elfType,
      machine: this.machine,
      entry_point: '0x' + this.entryPoint.toString(16),
      sections: this.sections.length,
      symbols: this.symbols.length,
      imports: this.imports.slice(0, 50),
      libraries: this.libraries,
      security: this.security.toDict(),
      hardening_score: this.security.score,
      suspicious: this.suspicious,
      errors: this.errors,
    };
  }
}

// ---------------------------------------------------------------------------
// ELF Parser
// ---------------------------------------------------------------------------

class ELFParser {
  /**
   * Parse an ELF binary and return full analysis.
   * @param {string} filepath
   * @returns {ELFAnalysis}
   */
  parse(filepath) {
    const result = new ELFAnalysis({ filename: filepath });

    let data;
    try {
      data = readFileSync(filepath);
    } catch (err) {
      result.errors.push(`Cannot read file: ${err.message}`);
      return result;
    }

    if (data.length < 64) {
      result.errors.push('File too small to be an ELF binary');
      return result;
    }

    if (!data.subarray(0, 4).equals(ELF_MAGIC)) {
      result.errors.push('Not an ELF file (bad magic bytes)');
      return result;
    }

    // Check for UPX packing in first 4KB
    if (data.subarray(0, 4096).includes(Buffer.from('UPX!'))) {
      result.suspicious.push('UPX-packed binary');
    }

    const eiClass = data[EI_CLASS];
    const eiData = data[EI_DATA];
    result.elfClass = CLASS_NAMES[eiClass] ?? `unknown(${eiClass})`;
    result.endianness = ENDIAN_NAMES[eiData] ?? `unknown(${eiData})`;

    const is64 = eiClass === ELFCLASS64;
    const le = eiData === ELFDATA2LSB;

    try {
      this._parseHeader(data, result, is64, le);
      this._parseSections(data, result, is64, le);
      this._parseProgramHeaders(data, result, is64, le);
      this._checkSecurity(result, data);
      this._detectSuspicious(result, data);
    } catch (err) {
      result.errors.push(`Parse error: ${err.message}`);
    }

    return result;
  }

  /**
   * Parse ELF header fields starting at offset 16.
   *
   * ELF32 header (from offset 16):
   *   offset 16: e_type    (uint16)
   *   offset 18: e_machine (uint16)
   *   offset 20: e_version (uint32)
   *   offset 24: e_entry   (uint32)  — entry point
   *
   * ELF64 header (from offset 16):
   *   offset 16: e_type    (uint16)
   *   offset 18: e_machine (uint16)
   *   offset 20: e_version (uint32)
   *   offset 24: e_entry   (uint64)  — entry point
   */
  _parseHeader(data, result, is64, le) {
    const eType = readU16(data, 16, le);
    const eMachine = readU16(data, 18, le);
    // e_version at 20 (uint32) — skipped
    const eEntry = is64 ? readU64(data, 24, le) : readU32(data, 24, le);

    result.elfType = TYPE_NAMES[eType] ?? `unknown(${eType})`;
    result.machine = MACHINE_NAMES[eMachine] ?? `unknown(${eMachine})`;
    result.entryPoint = eEntry;
  }

  /**
   * Parse section headers.
   *
   * ELF64: e_shoff at offset 40 (uint64), e_shentsize at 58 (uint16),
   *        e_shnum at 60 (uint16), e_shstrndx at 62 (uint16).
   * ELF32: e_shoff at offset 32 (uint32), e_shentsize at 46 (uint16),
   *        e_shnum at 48 (uint16), e_shstrndx at 50 (uint16).
   */
  _parseSections(data, result, is64, le) {
    let eShoff, eShentsize, eShnum, eShstrndx;
    if (is64) {
      eShoff = readU64(data, 40, le);
      eShentsize = readU16(data, 58, le);
      eShnum = readU16(data, 60, le);
      eShstrndx = readU16(data, 62, le);
    } else {
      eShoff = readU32(data, 32, le);
      eShentsize = readU16(data, 46, le);
      eShnum = readU16(data, 48, le);
      eShstrndx = readU16(data, 50, le);
    }

    if (eShoff === 0 || eShnum === 0) return;

    // Read section header string table
    const shstrtabOff = eShoff + eShstrndx * eShentsize;
    let strtabOffset, strtabSize;
    if (is64) {
      // sh_offset at +24 (uint64), sh_size at +32 (uint64) in 64-bit section header
      strtabOffset = readU64(data, shstrtabOff + 24, le);
      strtabSize = readU64(data, shstrtabOff + 32, le);
    } else {
      // sh_offset at +16 (uint32), sh_size at +20 (uint32) in 32-bit section header
      strtabOffset = readU32(data, shstrtabOff + 16, le);
      strtabSize = readU32(data, shstrtabOff + 20, le);
    }

    const strtab = data.subarray(strtabOffset, strtabOffset + strtabSize);

    for (let i = 0; i < eShnum; i++) {
      const off = eShoff + i * eShentsize;
      let sec;
      if (is64) {
        /**
         * ELF64 section header (64 bytes):
         *   +0  sh_name     (uint32)
         *   +4  sh_type     (uint32)
         *   +8  sh_flags    (uint64)
         *   +16 sh_addr     (uint64)
         *   +24 sh_offset   (uint64)
         *   +32 sh_size     (uint64)
         *   +40 sh_link     (uint32)
         *   +44 sh_info     (uint32)
         *   +48 sh_addralign (uint64)
         *   +56 sh_entsize  (uint64)
         */
        sec = new ELFSection({
          name: readStr(strtab, readU32(data, off, le)),
          typeId: readU32(data, off + 4, le),
          flags: readU64(data, off + 8, le),
          addr: readU64(data, off + 16, le),
          offset: readU64(data, off + 24, le),
          size: readU64(data, off + 32, le),
          link: readU32(data, off + 40, le),
          info: readU32(data, off + 44, le),
          entsize: readU64(data, off + 56, le),
        });
      } else {
        /**
         * ELF32 section header (40 bytes):
         *   +0  sh_name     (uint32)
         *   +4  sh_type     (uint32)
         *   +8  sh_flags    (uint32)
         *   +12 sh_addr     (uint32)
         *   +16 sh_offset   (uint32)
         *   +20 sh_size     (uint32)
         *   +24 sh_link     (uint32)
         *   +28 sh_info     (uint32)
         *   +32 sh_addralign (uint32)
         *   +36 sh_entsize  (uint32)
         */
        sec = new ELFSection({
          name: readStr(strtab, readU32(data, off, le)),
          typeId: readU32(data, off + 4, le),
          flags: readU32(data, off + 8, le),
          addr: readU32(data, off + 12, le),
          offset: readU32(data, off + 16, le),
          size: readU32(data, off + 20, le),
          link: readU32(data, off + 24, le),
          info: readU32(data, off + 28, le),
          entsize: readU32(data, off + 36, le),
        });
      }
      result.sections.push(sec);

      // Extract symbols from .dynsym
      if (sec.typeId === SHT_DYNSYM && sec.entsize > 0) {
        this._parseSymbols(data, sec, result, is64, le);
      }
    }
  }

  /**
   * Parse symbols from a .dynsym section.
   */
  _parseSymbols(data, sec, result, is64, le) {
    // Find .dynstr section
    let dynstrSec = null;
    for (const s of result.sections) {
      if (s.typeId === SHT_STRTAB && s.name === '.dynstr') {
        dynstrSec = s;
        break;
      }
    }
    if (!dynstrSec) return;

    const symStrtab = data.subarray(dynstrSec.offset, dynstrSec.offset + dynstrSec.size);
    const nSyms = sec.entsize ? Math.floor(sec.size / sec.entsize) : 0;
    const bindNames = { 0: 'LOCAL', 1: 'GLOBAL', 2: 'WEAK' };
    const typeNames = { 0: 'NOTYPE', 1: 'OBJECT', 2: 'FUNC', 10: 'IFUNC' };

    for (let i = 0; i < nSyms; i++) {
      const off = sec.offset + i * sec.entsize;
      let nameIdx, info, shndx, value, size;

      if (is64) {
        /**
         * ELF64 symbol (24 bytes):
         *   +0  st_name  (uint32)
         *   +4  st_info  (uint8)
         *   +5  st_other (uint8)
         *   +6  st_shndx (uint16)
         *   +8  st_value (uint64)
         *   +16 st_size  (uint64)
         */
        nameIdx = readU32(data, off, le);
        info = data[off + 4];
        shndx = readU16(data, off + 6, le);
        value = readU64(data, off + 8, le);
        size = readU64(data, off + 16, le);
      } else {
        /**
         * ELF32 symbol (16 bytes):
         *   +0  st_name  (uint32)
         *   +4  st_value (uint32)
         *   +8  st_size  (uint32)
         *   +12 st_info  (uint8)
         *   +13 st_other (uint8)
         *   +14 st_shndx (uint16)
         */
        nameIdx = readU32(data, off, le);
        value = readU32(data, off + 4, le);
        size = readU32(data, off + 8, le);
        info = data[off + 12];
        shndx = readU16(data, off + 14, le);
      }

      const name = readStr(symStrtab, nameIdx);
      if (!name) continue;

      const bind = bindNames[info >> 4] ?? 'OTHER';
      const stype = typeNames[info & 0xf] ?? 'OTHER';

      const sym = new ELFSymbol({ name, value, size, bind, type: stype, sectionIndex: shndx });
      result.symbols.push(sym);

      // Import = FUNC with section index 0 (UND)
      if (shndx === 0 && stype === 'FUNC') {
        result.imports.push(name);
      }
    }
  }

  /**
   * Parse program headers.
   *
   * ELF64: e_phoff at 32 (uint64), e_phentsize at 54 (uint16), e_phnum at 56 (uint16).
   * ELF32: e_phoff at 28 (uint32), e_phentsize at 42 (uint16), e_phnum at 44 (uint16).
   */
  _parseProgramHeaders(data, result, is64, le) {
    let ePhoff, ePhentsize, ePhnum;
    if (is64) {
      ePhoff = readU64(data, 32, le);
      ePhentsize = readU16(data, 54, le);
      ePhnum = readU16(data, 56, le);
    } else {
      ePhoff = readU32(data, 28, le);
      ePhentsize = readU16(data, 42, le);
      ePhnum = readU16(data, 44, le);
    }

    if (ePhoff === 0 || ePhnum === 0) return;

    for (let i = 0; i < ePhnum; i++) {
      const off = ePhoff + i * ePhentsize;
      let pType, pFlags, pOffset, pFilesz;

      if (is64) {
        /**
         * ELF64 program header (56 bytes):
         *   +0  p_type   (uint32)
         *   +4  p_flags  (uint32)
         *   +8  p_offset (uint64)
         *   +32 p_filesz (uint64)
         */
        pType = readU32(data, off, le);
        pFlags = readU32(data, off + 4, le);
        pOffset = readU64(data, off + 8, le);
        pFilesz = readU64(data, off + 32, le);
      } else {
        /**
         * ELF32 program header (32 bytes):
         *   +0  p_type   (uint32)
         *   +4  p_offset (uint32)
         *   +16 p_filesz (uint32)
         *   +24 p_flags  (uint32)
         */
        pType = readU32(data, off, le);
        pOffset = readU32(data, off + 4, le);
        pFilesz = readU32(data, off + 16, le);
        pFlags = readU32(data, off + 24, le);
      }

      if (pType === PT_GNU_STACK) {
        // PF_X = 0x1 — if executable bit set, NX is NOT enabled
        result.security.nx = !(pFlags & 0x1);
      } else if (pType === PT_GNU_RELRO) {
        result.security.relro = 'partial';
      } else if (pType === PT_INTERP) {
        const interp = data.subarray(pOffset, pOffset + pFilesz).toString('ascii').replace(/\0+$/, '');
        result.suspicious.push(`interpreter: ${interp}`);
      } else if (pType === PT_DYNAMIC) {
        this._parseDynamic(data, pOffset, pFilesz, result, is64, le);
      }
    }
  }

  /**
   * Parse dynamic section entries for library dependencies and security flags.
   */
  _parseDynamic(data, offset, size, result, is64, le) {
    // Find .dynstr for library name resolution
    let dynstr = Buffer.alloc(0);
    for (const sec of result.sections) {
      if (sec.name === '.dynstr') {
        dynstr = data.subarray(sec.offset, sec.offset + sec.size);
        break;
      }
    }

    const entrySize = is64 ? 16 : 8;
    const nEntries = Math.floor(size / entrySize);
    let bindNow = false;

    for (let i = 0; i < nEntries; i++) {
      const entOff = offset + i * entrySize;
      let dTag, dVal;

      if (is64) {
        dTag = readI64(data, entOff, le);
        dVal = readU64(data, entOff + 8, le);
      } else {
        dTag = readI32(data, entOff, le);
        dVal = readU32(data, entOff + 4, le);
      }

      if (dTag === 0) break; // DT_NULL
      if (dTag === DT_NEEDED && dynstr.length > 0) {
        result.libraries.push(readStr(dynstr, dVal));
      } else if (dTag === DT_RPATH) {
        result.security.rpath = true;
      } else if (dTag === DT_RUNPATH) {
        result.security.runpath = true;
      } else if (dTag === DT_FLAGS && (dVal & DF_BIND_NOW)) {
        bindNow = true;
      } else if (dTag === DT_FLAGS_1) {
        if (dVal & DF_1_NOW) bindNow = true;
        if (dVal & DF_1_PIE) result.security.pie = true;
      }
    }

    if (bindNow && result.security.relro === 'partial') {
      result.security.relro = 'full';
    }
  }

  /**
   * Additional security feature detection.
   */
  _checkSecurity(result, data) {
    // PIE heuristic: DYN type + entry != 0
    if (result.elfType === 'DYN' && result.entryPoint !== 0) {
      result.security.pie = true;
    }

    // Stack canary: check for __stack_chk_fail import
    for (const sym of result.symbols) {
      if (sym.name.includes('__stack_chk_fail')) {
        result.security.canary = true;
      }
      if (sym.name.startsWith('__') && sym.name.includes('_chk')) {
        result.security.fortify = true;
      }
    }

    // Stripped: no .symtab section
    const hasSymtab = result.sections.some((s) => s.name === '.symtab');
    result.security.stripped = !hasSymtab;
  }

  /**
   * Detect suspicious binary indicators.
   */
  _detectSuspicious(result, data) {
    const sectionNames = new Set(result.sections.map((s) => s.name));

    if (sectionNames.has('.upx0') || sectionNames.has('.upx1')) {
      result.suspicious.push('UPX-packed (section names)');
    }

    // All sections stripped — only empty name and .shstrtab remain
    const meaningful = new Set(sectionNames);
    meaningful.delete('');
    meaningful.delete('.shstrtab');
    if (meaningful.size === 0 && result.sections.length > 0) {
      result.suspicious.push('All sections stripped — possibly packed');
    }

    // W+X sections
    for (const sec of result.sections) {
      if (sec.isWritable && sec.isExecutable && sec.name) {
        result.suspicious.push(`W+X section: ${sec.name} (${sec.size} bytes)`);
      }
    }

    // Anti-debug imports
    const antiDebugFuncs = new Set(['ptrace', 'prctl', 'getppid', 'isDebuggerPresent']);
    for (const imp of result.imports) {
      if (antiDebugFuncs.has(imp)) {
        result.suspicious.push(`Anti-debug function: ${imp}`);
      }
    }
  }
}

// ---------------------------------------------------------------------------
// Format String Scanner
// ---------------------------------------------------------------------------

const PRINTF_FAMILY = /\b(s?n?printf|fprintf|vprintf|vsprintf|vsnprintf|vfprintf|syslog|err|errx|warn|warnx)\s*\(/i;

const C_EXTENSIONS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp']);

class FormatStringFinding {
  /**
   * @param {object} opts
   * @param {string} opts.file
   * @param {number} opts.lineNumber
   * @param {string} opts.lineContent
   * @param {string} opts.function
   * @param {string} opts.severity
   * @param {string} opts.reason
   */
  constructor(opts) {
    this.file = opts.file;
    this.lineNumber = opts.lineNumber;
    this.lineContent = opts.lineContent;
    this.function = opts.function;
    this.severity = opts.severity;
    this.reason = opts.reason;
  }

  toDict() {
    return {
      file: this.file,
      line: this.lineNumber,
      function: this.function,
      severity: this.severity,
      reason: this.reason,
      content: this.lineContent.trim().slice(0, 200),
    };
  }
}

class FormatStringScanner {
  /**
   * Scan a source file for format string vulnerabilities.
   * @param {string} filepath
   * @returns {FormatStringFinding[]}
   */
  scanFile(filepath) {
    const ext = extname(filepath).toLowerCase();
    if (!C_EXTENSIONS.has(ext)) return [];

    let content;
    try {
      content = readFileSync(filepath, 'utf-8');
    } catch {
      return [];
    }

    const lines = content.split('\n');
    const findings = [];

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      const stripped = line.trim();
      if (stripped.startsWith('//') || stripped.startsWith('/*')) continue;

      const m = PRINTF_FAMILY.exec(line);
      if (!m) continue;

      const func = m[1];
      const after = line.slice(m.index + m[0].length).trimStart();

      // String literal format = safe
      if (after.startsWith('"') || after.startsWith("'")) continue;

      // Variable as format string — dangerous
      const severity = ['printf', 'sprintf', 'fprintf'].includes(func) ? 'critical' : 'high';

      findings.push(
        new FormatStringFinding({
          file: filepath,
          lineNumber: i + 1,
          lineContent: line,
          function: func,
          severity,
          reason: `Variable passed as format string to ${func}()`,
        }),
      );
    }

    return findings;
  }

  /**
   * Scan a directory recursively for format string vulnerabilities.
   * @param {string} directory
   * @returns {FormatStringFinding[]}
   */
  scanDirectory(directory) {
    const findings = [];
    const walk = (dir) => {
      let entries;
      try {
        entries = readdirSync(dir, { withFileTypes: true });
      } catch {
        return;
      }
      for (const ent of entries.sort((a, b) => a.name.localeCompare(b.name))) {
        const full = join(dir, ent.name);
        if (ent.isDirectory()) {
          walk(full);
        } else if (ent.isFile() && C_EXTENSIONS.has(extname(ent.name).toLowerCase())) {
          findings.push(...this.scanFile(full));
        }
      }
    };
    walk(directory);
    return findings;
  }
}

// ---------------------------------------------------------------------------
// ROP Gadget Scanner — uses objdump subprocess per D050
// ---------------------------------------------------------------------------

class ROPGadget {
  /**
   * @param {object} opts
   * @param {number} opts.address
   * @param {string} opts.instructions
   * @param {string} opts.bytesHex
   * @param {number} opts.length  number of instructions
   */
  constructor(opts) {
    this.address = opts.address;
    this.instructions = opts.instructions;
    this.bytesHex = opts.bytesHex;
    this.length = opts.length;
  }

  toDict() {
    return {
      address: '0x' + this.address.toString(16),
      instructions: this.instructions,
      bytes: this.bytesHex,
      length: this.length,
    };
  }
}

class ROPScanner {
  /** Max instructions per gadget. */
  static MAX_GADGET_INSNS = 6;

  /**
   * Check if objdump is available.
   * @returns {boolean}
   */
  static available() {
    try {
      execFileSync('objdump', ['--version'], { stdio: 'pipe', timeout: 5000 });
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Scan an ELF binary for ROP gadgets using objdump disassembly.
   * @param {string} filepath
   * @param {object} [opts]
   * @param {number} [opts.maxGadgets=200]
   * @param {number} [opts.maxDepth=5] max instructions before gadget-ending instruction
   * @returns {ROPGadget[]}
   */
  scan(filepath, opts = {}) {
    const maxGadgets = opts.maxGadgets ?? 200;
    const maxDepth = opts.maxDepth ?? 5;

    let output;
    try {
      output = execFileSync('objdump', ['-d', '-j', '.text', filepath], {
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
        timeout: 30000,
        maxBuffer: 50 * 1024 * 1024, // 50MB
      });
    } catch {
      return [];
    }

    // Parse objdump output lines
    // Format:  "  4004c3:	c3                   	ret"
    //          "  addr:  hex_bytes               mnemonic operands"
    const lines = output.split('\n');
    const parsed = []; // { addr, bytes, mnemonic, operands }

    for (const line of lines) {
      const m = line.match(/^\s*([0-9a-f]+):\s+((?:[0-9a-f]{2}\s?)+)\s+(\S+)(?:\s+(.*))?$/);
      if (!m) continue;
      const addr = parseInt(m[1], 16);
      const bytes = m[2].trim();
      const mnemonic = m[3];
      const operands = (m[4] ?? '').trim();
      parsed.push({ addr, bytes, mnemonic, operands });
    }

    // Find gadget-ending instructions
    const gadgetEnds = new Set(['ret', 'retq']);
    const syscallEnds = new Set(['syscall']);
    // int 0x80 represented as "int" with operand "$0x80"

    const gadgets = [];
    const seen = new Set();

    for (let i = 0; i < parsed.length; i++) {
      const ins = parsed[i];
      const isEnd =
        gadgetEnds.has(ins.mnemonic) ||
        syscallEnds.has(ins.mnemonic) ||
        (ins.mnemonic === 'int' && ins.operands === '$0x80');

      if (!isEnd) continue;

      // Collect preceding instruction sequences of varying depths
      for (let depth = 1; depth <= maxDepth && depth <= i; depth++) {
        const startIdx = i - depth;
        const sequence = parsed.slice(startIdx, i + 1);

        const insnStr = sequence
          .map((s) => (s.operands ? `${s.mnemonic} ${s.operands}` : s.mnemonic))
          .join(' ; ');

        if (seen.has(insnStr)) continue;
        seen.add(insnStr);

        const bytesHex = sequence.map((s) => s.bytes.replace(/\s/g, '')).join('');

        gadgets.push(
          new ROPGadget({
            address: sequence[0].addr,
            instructions: insnStr,
            bytesHex,
            length: sequence.length,
          }),
        );

        if (gadgets.length >= maxGadgets) {
          return gadgets.sort((a, b) => a.address - b.address);
        }
      }
    }

    return gadgets.sort((a, b) => a.address - b.address);
  }

  /**
   * Find gadgets matching an instruction pattern (regex).
   * @param {ROPGadget[]} gadgets
   * @param {string|RegExp} pattern
   * @returns {ROPGadget[]}
   */
  static findGadgetsByPattern(gadgets, pattern) {
    const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
    return gadgets.filter((g) => re.test(g.instructions));
  }
}

// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------

export {
  // Constants
  ELF_MAGIC,
  SHF_WRITE,
  SHF_ALLOC,
  SHF_EXECINSTR,
  // Helpers
  readU16,
  readU32,
  readU64,
  readStr,
  // Data classes
  ELFSection,
  ELFSymbol,
  SecurityFeatures,
  ELFAnalysis,
  // Parser
  ELFParser,
  // Format string scanner
  FormatStringFinding,
  FormatStringScanner,
  // ROP
  ROPGadget,
  ROPScanner,
};
