/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8                               :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2023 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/calls/calls.h"
#include "libc/ctype.h"
#include "libc/limits.h"
#include "libc/nt/struct/imageimportbyname.internal.h"
#include "libc/nt/struct/imageimportdescriptor.internal.h"
#include "libc/nt/struct/imagentheaders.internal.h"
#include "libc/nt/struct/imageoptionalheader.internal.h"
#include "libc/nt/struct/imagesectionheader.internal.h"
#include "libc/runtime/runtime.h"
#include "libc/serialize.h"
#include "libc/stdckdint.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"

/**
 * @fileoverview Linter for PE static executable files.
 *
 * Generating PE files from scratch is tricky. There's numerous things
 * that can go wrong, and operating systems don't explain what's wrong
 * when they refuse to run a program. This program can help illuminate
 * any issues with your generated binaries, with better error messages
 */

struct Exe {
  char *map;
  size_t size;
  const char *path;
  struct NtImageNtHeaders *pe;
  struct NtImageSectionHeader *sections;
  uint32_t section_count;
};

static wontreturn void Die(const char *thing, const char *reason) {
  tinyprint(2, thing, ": ", reason, "\n", NULL);
  exit(1);
}

static wontreturn void DieSys(const char *thing) {
  perror(thing);
  exit(1);
}

static void LogPeSections(FILE *f, struct NtImageSectionHeader *p, size_t n) {
  size_t i;
  fprintf(f, "Name     Offset   RelativeVirtAddr   FileSiz  MemSiz   Flg\n");
  for (i = 0; i < n; ++i) {
    fprintf(f, "%-8.8s 0x%06lx 0x%016lx 0x%06lx 0x%06lx %c%c%c\n", p[i].Name,
            p[i].PointerToRawData, p[i].VirtualAddress, p[i].SizeOfRawData,
            p[i].Misc.VirtualSize,
            p[i].Characteristics & kNtPeSectionMemRead ? 'R' : ' ',
            p[i].Characteristics & kNtPeSectionMemWrite ? 'W' : ' ',
            p[i].Characteristics & kNtPeSectionMemExecute ? 'E' : ' ');
  }
}

// resolves relative virtual address
//
// this is a trivial process when an executable has been loaded properly
// i.e. a separate mmap() call was made for each individual section; but
// we've only mapped the executable file itself into memory; thus, we'll
// need to remap a virtual address into a file offset to get the pointer
//
// returns pointer to image data, or null on error
static void *GetRva(struct Exe *exe, uint32_t rva, uint32_t size) {
  int i;
  for (i = 0; i < exe->section_count; ++i) {
    if (exe->sections[i].VirtualAddress <= rva &&
        rva < exe->sections[i].VirtualAddress +
                  exe->sections[i].Misc.VirtualSize) {
      if (rva + size <=
          exe->sections[i].VirtualAddress + exe->sections[i].Misc.VirtualSize) {
        return exe->map + exe->sections[i].PointerToRawData +
               (rva - exe->sections[i].VirtualAddress);
      } else {
        break;
      }
    }
  }
  return 0;
}

static bool HasControlCodes(const char *s) {
  int c;
  while ((c = *s++)) {
    if (isascii(c) && iscntrl(c)) {
      return true;
    }
  }
  return false;
}

static void CheckPeImportByName(struct Exe *exe, uint32_t rva) {
  struct NtImageImportByName *hintname;
  if (rva & 1)
    Die(exe->path, "PE IMAGE_IMPORT_BY_NAME (hint name) structures must "
                   "be 2-byte aligned");
  if (!(hintname = GetRva(exe, rva, sizeof(struct NtImageImportByName))))
    Die(exe->path, "PE import table RVA entry didn't reslove");
  if (!*hintname->Name)
    Die(exe->path, "PE imported function name is empty string");
  if (HasControlCodes(hintname->Name))
    Die(exe->path, "PE imported function name contains ascii control codes");
}

static void CheckPe(const char *path, char *map, size_t size) {

  int pagesz = 4096;

  // sanity check mz header
  if (size < 64)  //
    Die(path, "Image too small for MZ header");
  if (READ16LE(map) != ('M' | 'Z' << 8))
    Die(path, "Image doesn't start with MZ");
  uint32_t pe_offset;
  if ((pe_offset = READ32LE(map + 60)) >= size)
    Die(path, "PE header offset points past end of image");
  if (pe_offset & 7)
    Die(path, "PE header offset must possess an 8-byte alignment");
  if (pe_offset + sizeof(struct NtImageNtHeaders) > size)
    Die(path, "PE mandatory headers overlap end of image");
  struct NtImageNtHeaders *pe = (struct NtImageNtHeaders *)(map + pe_offset);
  if ((pe_offset + sizeof(struct NtImageFileHeader) + 4 +
       pe->FileHeader.SizeOfOptionalHeader) > size)
    Die(path, "PE optional header size overlaps end of image");

  // sanity check pe header
  if (pe->Signature != ('P' | 'E' << 8))
    Die(path, "PE Signature must be 0x00004550");
  if (!(pe->FileHeader.Characteristics & kNtPeFileExecutableImage))
    Die(path, "PE Characteristics must have executable bit set");
  if (pe->FileHeader.Characteristics & kNtPeFileDll)
    Die(path, "PE Characteristics can't have DLL bit set");
  if (pe->FileHeader.NumberOfSections < 1)
    Die(path, "PE NumberOfSections >= 1 must be the case");
  if (pe->OptionalHeader.Magic != kNtPe64bit)
    Die(path, "PE OptionalHeader Magic must be 0x020b");
  if (pe->OptionalHeader.FileAlignment < 512)
    Die(path, "PE FileAlignment must be at least 512");
  if (pe->OptionalHeader.FileAlignment > 65536)
    Die(path, "PE FileAlignment can't exceed 65536");
  if (pe->OptionalHeader.FileAlignment & (pe->OptionalHeader.FileAlignment - 1))
    Die(path, "PE FileAlignment must be a two power");
  if (pe->OptionalHeader.SectionAlignment &
      (pe->OptionalHeader.SectionAlignment - 1))
    Die(path, "PE SectionAlignment must be a two power");
  if (pe->OptionalHeader.SectionAlignment < pe->OptionalHeader.FileAlignment)
    Die(path, "PE SectionAlignment >= FileAlignment must be the case");
  if (pe->OptionalHeader.SectionAlignment < pagesz &&
      pe->OptionalHeader.SectionAlignment != pe->OptionalHeader.FileAlignment)
    Die(path, "PE SectionAlignment must equal FileAlignment if it's less than "
              "the microprocessor architecture's page size");
  if (pe->OptionalHeader.ImageBase & 65535)
    Die(path, "PE ImageBase must be multiple of 65536");
  if (pe->OptionalHeader.ImageBase > INT_MAX &&
      !(pe->FileHeader.Characteristics & kNtImageFileLargeAddressAware))
    Die(path, "PE FileHeader.Characteristics needs "
              "IMAGE_FILE_LARGE_ADDRESS_AWARE if ImageBase > INT_MAX");

  // validate the size of the pe optional headers
  int len;
  if (ckd_mul(&len, pe->OptionalHeader.NumberOfRvaAndSizes,
              sizeof(struct NtImageDataDirectory)) ||
      ckd_add(&len, len, sizeof(struct NtImageOptionalHeader)))
    Die(path, "encountered overflow computing PE SizeOfOptionalHeader");
  if (pe->FileHeader.SizeOfOptionalHeader != len)
    Die(path, "PE SizeOfOptionalHeader had incorrect value");
  if (len > size || (char *)&pe->OptionalHeader + len > map + size)
    Die(path, "PE OptionalHeader overflows image");

  // perform even more pe validation
  if (pe->OptionalHeader.SizeOfImage &
      (pe->OptionalHeader.SectionAlignment - 1))
    Die(path, "PE SizeOfImage must be multiple of SectionAlignment");
  if (pe->OptionalHeader.SizeOfHeaders & (pe->OptionalHeader.FileAlignment - 1))
    Die(path, "PE SizeOfHeaders must be multiple of FileAlignment");
  if (pe->OptionalHeader.SizeOfHeaders > pe->OptionalHeader.AddressOfEntryPoint)
    Die(path, "PE SizeOfHeaders <= AddressOfEntryPoint must be the case");
  if (pe->OptionalHeader.SizeOfHeaders >= pe->OptionalHeader.SizeOfImage)
    Die(path, "PE SizeOfHeaders < SizeOfImage must be the case");
  if (pe->OptionalHeader.SizeOfStackCommit >> 32)
    Die(path, "PE SizeOfStackCommit can't exceed 4GB");
  if (pe->OptionalHeader.SizeOfStackReserve >> 32)
    Die(path, "PE SizeOfStackReserve can't exceed 4GB");
  if (pe->OptionalHeader.SizeOfHeapCommit >> 32)
    Die(path, "PE SizeOfHeapCommit can't exceed 4GB");
  if (pe->OptionalHeader.SizeOfHeapReserve >> 32)
    Die(path, "PE SizeOfHeapReserve can't exceed 4GB");

  // check pe section headers
  struct NtImageSectionHeader *sections =
      (struct NtImageSectionHeader *)((char *)&pe->OptionalHeader +
                                      pe->FileHeader.SizeOfOptionalHeader);
  for (int i = 0; i < pe->FileHeader.NumberOfSections; ++i) {
    if (sections[i].SizeOfRawData & (pe->OptionalHeader.FileAlignment - 1))
      Die(path, "PE SizeOfRawData should be multiple of FileAlignment");
    if (sections[i].PointerToRawData & (pe->OptionalHeader.FileAlignment - 1))
      Die(path, "PE PointerToRawData must be multiple of FileAlignment");
    if (map + sections[i].PointerToRawData >= map + size)
      Die(path, "PE PointerToRawData points outside image");
    if (map + sections[i].PointerToRawData + sections[i].SizeOfRawData >
        map + size)
      Die(path, "PE SizeOfRawData overlaps end of image");
    if (!sections[i].VirtualAddress)
      Die(path, "PE VirtualAddress shouldn't be zero");
    if (sections[i].VirtualAddress & (pe->OptionalHeader.SectionAlignment - 1))
      Die(path, "PE VirtualAddress must be multiple of SectionAlignment");
    if ((sections[i].Characteristics &
         (kNtPeSectionCntCode | kNtPeSectionCntInitializedData |
          kNtPeSectionCntUninitializedData)) ==
        kNtPeSectionCntUninitializedData) {
      if (sections[i].SizeOfRawData)
        Die(path, "PE SizeOfRawData should be zero for pure BSS section");
      if (sections[i].PointerToRawData)
        Die(path, "PE PointerToRawData should be zero for pure BSS section");
    }
    if (!i) {
      if (sections[i].VirtualAddress !=
          ((pe->OptionalHeader.SizeOfHeaders +
            (pe->OptionalHeader.SectionAlignment - 1)) &
           -pe->OptionalHeader.SectionAlignment))
        Die(path, "PE VirtualAddress of first section must be SizeOfHeaders "
                  "rounded up to SectionAlignment");
    } else {
      if (sections[i].VirtualAddress !=
          sections[i - 1].VirtualAddress +
              ((sections[i - 1].Misc.VirtualSize +
                (pe->OptionalHeader.SectionAlignment - 1)) &
               -pe->OptionalHeader.SectionAlignment))
        Die(path, "PE sections must be in ascending order and the virtual "
                  "memory they define must be adjacent after VirtualSize is "
                  "rounded up to the SectionAlignment");
    }
  }

  // create an object for our portable executable
  struct Exe exe[1] = {{
      .pe = pe,
      .path = path,
      .map = map,
      .size = size,
      .sections = sections,
      .section_count = pe->FileHeader.NumberOfSections,
  }};

  // validate dll imports
  struct NtImageDataDirectory *ddImports =
      exe->pe->OptionalHeader.DataDirectory + kNtImageDirectoryEntryImport;
  if (exe->pe->OptionalHeader.NumberOfRvaAndSizes >= 2 && ddImports->Size) {
    if (ddImports->Size % sizeof(struct NtImageImportDescriptor) != 0)
      Die(exe->path, "PE Imports data directory entry Size should be a "
                     "multiple of sizeof(IMAGE_IMPORT_DESCRIPTOR)");
    if (ddImports->VirtualAddress & 3)
      Die(exe->path, "PE IMAGE_IMPORT_DESCRIPTOR table must be 4-byte aligned");
    struct NtImageImportDescriptor *idt;
    if (!(idt = GetRva(exe, ddImports->VirtualAddress, ddImports->Size)))
      Die(exe->path, "couldn't resolve VirtualAddress/Size RVA of PE Import "
                     "Directory Table to within a defined PE section");
    if (idt->ImportLookupTable >= exe->size)
      Die(exe->path, "Import Directory Table VirtualAddress/Size RVA resolved "
                     "to dense unrelated binary content");
    for (int i = 0; idt->ImportLookupTable; ++i, ++idt) {
      char *dllname;
      if (!(dllname = GetRva(exe, idt->DllNameRva, 2)))
        Die(exe->path, "PE DllNameRva doesn't resolve to a PE section");
      if (!*dllname)
        Die(exe->path, "PE import DllNameRva pointed to empty string");
      if (HasControlCodes(dllname))
        Die(exe->path, "PE import DllNameRva contained ascii control codes");
      if (idt->ImportLookupTable & 7)
        Die(exe->path, "PE ImportLookupTable must be 8-byte aligned");
      if (idt->ImportAddressTable & 7)
        Die(exe->path, "PE ImportAddressTable must be 8-byte aligned");
      uint64_t *ilt, *iat;
      if (!(ilt = GetRva(exe, idt->ImportLookupTable, 8)))
        Die(exe->path, "PE ImportLookupTable RVA didn't resolve to a section");
      if (!(iat = GetRva(exe, idt->ImportAddressTable, 8)))
        Die(exe->path, "PE ImportAddressTable RVA didn't resolve to a section");
      for (int j = 0;; ++j, ++ilt, ++iat) {
        if (*ilt != *iat) {
          Die(exe->path, "PE ImportLookupTable and ImportAddressTable should "
                         "have identical content");
        }
        if (!*ilt)
          break;
        CheckPeImportByName(exe, *ilt);
      }
    }
  }
}

int main(int argc, char *argv[]) {
  int i, fd;
  void *map;
  ssize_t size;
  const char *path;
#ifdef MODE_DBG
  ShowCrashReports();
#endif
  for (i = 1; i < argc; ++i) {
    path = argv[i];
    if ((fd = open(path, O_RDONLY)) == -1)
      DieSys(path);
    if ((size = lseek(fd, 0, SEEK_END)) == -1)
      DieSys(path);
    map = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED)
      DieSys(path);
    CheckPe(path, map, size);
    if (munmap(map, size))
      DieSys(path);
    if (close(fd))
      DieSys(path);
  }
}
