// Copyright 2023 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include <stdio.h>

#include "iree/base/api.h"
#include "iree/base/internal/file_io.h"
#include "iree/base/internal/path.h"
#include "iree/hal/local/elf/fatelf.h"

// NOTE: we don't verify ELF information in here and just pass it along. Don't
// run this on untrusted ELFs.

// NOTE: errors are handled in here just enough to get error messages - we don't
// care about leaks on failure as the process is going to die right away.

// TODO(benvanik): make this based on the archs used? It needs to be the common
// page size across all targets used within the file.
#define IREE_FATELF_PAGE_SIZE 4096

#if defined(IREE_PLATFORM_WINDOWS)
#include <fcntl.h>
#include <io.h>
#define IREE_SET_BINARY_MODE(handle) _setmode(_fileno(handle), O_BINARY)
#else
#define IREE_SET_BINARY_MODE(handle) ((void)0)
#endif  // IREE_PLATFORM_WINDOWS

static int print_usage() {
  fprintf(stderr, "Syntax: iree-fatelf [join|split|select|dump] files...\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "Join multiple ELFs into a FatELF:\n");
  fprintf(stderr, "  iree-fatelf join elf_a.so elf_b.so > fatelf.sos\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "Split a FatELF into multiple ELF files (to dir):\n");
  fprintf(stderr, "  iree-fatelf split fatelf.sos\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "Select a FatELF matching the current arch:\n");
  fprintf(stderr, "  iree-fatelf select fatelf.sos > elf.so\n");
  fprintf(stderr, "\n");
  fprintf(stderr, "Dump header records:\n");
  fprintf(stderr, "  iree-fatelf dump fatelf.sos\n");
  fprintf(stderr, "\n");
  return 1;
}

// NOTE: this is somewhat redundant with fatelf.c but that's ok - this is a
// developer tool and I'd rather have the implementation linked into every
// runtime be kept as simple as possible than not repeating 100 lines of code.
// The runtime version is also designed to gracefully accept ELF files where
// here we only want FatELF files.
static iree_status_t fatelf_parse(iree_const_byte_span_t file_data,
                                  iree_fatelf_header_t** out_header) {
  *out_header = NULL;

  if (file_data.data_length <
      sizeof(iree_fatelf_header_t) + sizeof(iree_fatelf_record_t)) {
    return iree_make_status(
        IREE_STATUS_INVALID_ARGUMENT,
        "file does not have enough data to even hold a FatELF header");
  }

  const iree_fatelf_header_t* raw_header =
      (const iree_fatelf_header_t*)file_data.data;
  iree_fatelf_header_t host_header = {
      .magic = iree_unaligned_load_le_u32(&raw_header->magic),
      .version = iree_unaligned_load_le_u16(&raw_header->version),
      .record_count = iree_unaligned_load_le_u8(&raw_header->record_count),
      .reserved = iree_unaligned_load_le_u8(&raw_header->reserved),
  };

  if (host_header.magic != IREE_FATELF_MAGIC) {
    return iree_make_status(
        IREE_STATUS_INVALID_ARGUMENT,
        "file magic %08X does not match expected FatELF magic %08X",
        host_header.magic, IREE_FATELF_MAGIC);
  }
  if (host_header.version != IREE_FATELF_FORMAT_VERSION) {
    return iree_make_status(
        IREE_STATUS_UNIMPLEMENTED,
        "FatELF has version %d but runtime only supports version %d",
        host_header.version, IREE_FATELF_FORMAT_VERSION);
  }

  iree_host_size_t required_bytes =
      sizeof(iree_fatelf_header_t) +
      host_header.record_count * sizeof(iree_fatelf_record_t);
  if (file_data.data_length < required_bytes) {
    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                            "FatELF file truncated, requires at least %" PRIhsz
                            "B for headers but only have %" PRIhsz
                            "B available",
                            required_bytes, file_data.data_length);
  }

  // Allocate storage for the parsed header and records.
  iree_fatelf_header_t* header = NULL;
  IREE_RETURN_IF_ERROR(iree_allocator_malloc(
      iree_allocator_system(),
      sizeof(iree_fatelf_header_t) +
          host_header.record_count * sizeof(iree_fatelf_record_t),
      (void**)&header));
  memcpy(header, &host_header, sizeof(*header));
  for (iree_elf64_byte_t i = 0; i < host_header.record_count; ++i) {
    const iree_fatelf_record_t* raw_record = &raw_header->records[i];
    const iree_fatelf_record_t host_record = {
        .machine = iree_unaligned_load_le_u16(&raw_record->machine),
        .osabi = iree_unaligned_load_le_u8(&raw_record->osabi),
        .osabi_version = iree_unaligned_load_le_u8(&raw_record->osabi_version),
        .word_size = iree_unaligned_load_le_u8(&raw_record->word_size),
        .byte_order = iree_unaligned_load_le_u8(&raw_record->byte_order),
        .reserved0 = iree_unaligned_load_le_u8(&raw_record->reserved0),
        .reserved1 = iree_unaligned_load_le_u8(&raw_record->reserved1),
        .offset = iree_unaligned_load_le_u64(&raw_record->offset),
        .size = iree_unaligned_load_le_u64(&raw_record->size),
    };
    memcpy(&header->records[i], &host_record, sizeof(host_record));
  }

  *out_header = header;
  return iree_ok_status();
}

// Tries to parse basic ELF metadata from |elf_data|.
// Very little verification done. Note that this must support both 32 and 64-bit
// ELF files regardless of the tool host configuration.
// The returned fields match the ELF spec and may differ from FatELF.
static iree_status_t fatelf_parse_elf_metadata(
    iree_const_byte_span_t elf_data, iree_elf64_half_t* out_machine,
    iree_elf64_byte_t* out_osabi, iree_elf64_byte_t* out_osabi_version,
    iree_elf64_byte_t* out_elf_class, iree_elf64_byte_t* out_elf_data) {
  *out_machine = 0;
  *out_osabi = 0;
  *out_osabi_version = 0;
  *out_elf_data = 0;
  *out_elf_class = 0;

  if (elf_data.data_length < sizeof(iree_elf32_ehdr_t)) {
    return iree_make_status(IREE_STATUS_FAILED_PRECONDITION,
                            "ELF data provided (%" PRIhsz
                            ") is smaller than ehdr (%zu)",
                            elf_data.data_length, sizeof(iree_elf32_ehdr_t));
  }

  // The fields we're checking are the same in both 32 and 64 classes so we just
  // use 32 for consistency.
  const iree_elf32_ehdr_t* ehdr = (const iree_elf32_ehdr_t*)elf_data.data;
  static const iree_elf_byte_t elf_magic[4] = {0x7F, 'E', 'L', 'F'};
  if (memcmp(ehdr->e_ident, elf_magic, sizeof(elf_magic)) != 0) {
    return iree_make_status(
        IREE_STATUS_FAILED_PRECONDITION,
        "data provided does not contain the ELF identifier");
  }

  *out_osabi = ehdr->e_ident[IREE_ELF_EI_OSABI];
  *out_osabi_version = ehdr->e_ident[IREE_ELF_EI_ABIVERSION];
  *out_elf_class = ehdr->e_ident[IREE_ELF_EI_CLASS];
  *out_elf_data = ehdr->e_ident[IREE_ELF_EI_DATA];

  // Note machine is multibyte and respects the declared endianness.
  if (ehdr->e_ident[IREE_ELF_EI_DATA] == IREE_ELF_ELFDATA2LSB) {
    *out_machine = iree_unaligned_load_le_u16(&ehdr->e_machine);
  } else {
#if IREE_ENDIANNESS_BIG
// TODO(benvanik): helpers for big<->little endian
// *out_machine = iree_unaligned_load_be_u16(&ehdr->e_machine);
#error "ELF parsing support only available on little-endian systems today"
#endif  // IREE_ENDIANNESS_BIG
  }

  return iree_ok_status();
}

typedef struct {
  uint64_t offset;
  iree_file_contents_t* contents;
  iree_const_byte_span_t elf_data;
} fatelf_entry_t;

// Joins one or more ELF files together and writes the output to stdout.
static iree_status_t fatelf_join(int argc, char** argv) {
  IREE_SET_BINARY_MODE(stdout);  // ensure binary output mode

#if IREE_ENDIANNESS_BIG
#error "FatELF writing support only available on little-endian systems today"
#endif  // IREE_ENDIANNESS_BIG

  // Load all source files.
  iree_elf64_byte_t entry_count = argc;
  fatelf_entry_t* entries =
      (fatelf_entry_t*)iree_alloca(entry_count * sizeof(fatelf_entry_t));
  memset(entries, 0, entry_count * sizeof(*entries));
  for (iree_elf64_byte_t i = 0; i < entry_count; ++i) {
    IREE_RETURN_IF_ERROR(
        iree_file_read_contents(argv[i], IREE_FILE_READ_FLAG_DEFAULT,
                                iree_allocator_system(), &entries[i].contents));
    entries[i].elf_data = entries[i].contents->const_buffer;
  }

  // Compute offsets of all files based on their size and padding.
  uint64_t file_offset = iree_host_align(
      sizeof(iree_fatelf_header_t) + entry_count * sizeof(iree_fatelf_record_t),
      IREE_FATELF_PAGE_SIZE);
  for (iree_elf64_byte_t i = 0; i < entry_count; ++i) {
    entries[i].offset = file_offset;
    file_offset += iree_host_align(
        entries[i].contents->const_buffer.data_length, IREE_FATELF_PAGE_SIZE);
  }

  // Write header without records.
  iree_fatelf_header_t host_header = {
      .magic = IREE_FATELF_MAGIC,
      .version = IREE_FATELF_FORMAT_VERSION,
      .record_count = entry_count,
      .reserved = 0,
  };
  fwrite(&host_header, 1, sizeof(host_header), stdout);

  // Write all records.
  for (iree_elf64_byte_t i = 0; i < entry_count; ++i) {
    iree_elf64_half_t machine = 0;
    iree_elf64_byte_t osabi = 0;
    iree_elf64_byte_t osabi_version = 0;
    iree_elf64_byte_t elf_class = 0;
    iree_elf64_byte_t elf_data = 0;
    IREE_RETURN_IF_ERROR(
        fatelf_parse_elf_metadata(entries[i].elf_data, &machine, &osabi,
                                  &osabi_version, &elf_class, &elf_data));
    iree_fatelf_record_t host_record = {
        .machine = machine,
        .osabi = osabi,
        .osabi_version = osabi_version,
        .word_size = elf_class == IREE_ELF_ELFCLASS32
                         ? IREE_FATELF_WORD_SIZE_32
                         : IREE_FATELF_WORD_SIZE_64,
        .byte_order = elf_data == IREE_ELF_ELFDATA2LSB
                          ? IREE_FATELF_BYTE_ORDER_LSB
                          : IREE_FATELF_BYTE_ORDER_MSB,
        .reserved0 = 0,
        .reserved1 = 0,
        .offset = (iree_elf64_off_t)entries[i].offset,
        .size = (iree_elf64_xword_t)entries[i].elf_data.data_length,
    };
    fwrite(&host_record, 1, sizeof(host_record), stdout);
  }

  // Write all files, padding with zeros in-between as needed.
  uint64_t write_offset =
      sizeof(iree_fatelf_header_t) + entry_count * sizeof(iree_fatelf_record_t);
  for (iree_elf64_byte_t i = 0; i < entry_count; ++i) {
    uint64_t padding = entries[i].offset - write_offset;
    for (uint64_t i = 0; i < padding; ++i) fputc(0, stdout);
    write_offset += padding;
    if (write_offset != entries[i].offset) {
      return iree_make_status(IREE_STATUS_INTERNAL,
                              "actual offset does not match expected");
    }
    fwrite(entries[i].elf_data.data, 1, entries[i].elf_data.data_length,
           stdout);
    write_offset += entries[i].elf_data.data_length;
  }
  fflush(stdout);

  for (iree_elf64_byte_t i = 0; i < entry_count; ++i) {
    iree_file_contents_free(entries[i].contents);
  }
  return iree_ok_status();
}

static const char* fatelf_machine_id_str(iree_elf64_half_t value) {
  // TODO(benvanik): include a full table from the spec?
  // http://formats.kaitai.io/elf/ has a good source of canonical short names.
  // For now we just support what we have in our ELF loader.
  switch (value) {
    case 0x03:  // EM_386 / 3
      return "x86";
    case 0x28:  // EM_ARM / 40
      return "arm";
    case 0xB7:  // EM_AARCH64 / 183
      return "aarch64";
    case 0xF3:  // EM_RISCV / 243
      return "risvc";
    case 0x3E:  // EM_X86_64 / 62
      return "x86_64";
    default:
      return "unknown";
  }
}

static const char* fatelf_osabi_id_str(iree_elf64_byte_t value) {
  switch (value) {
    case IREE_ELF_ELFOSABI_NONE:
      return "none";
    case IREE_ELF_ELFOSABI_LINUX:
      return "linux";
    case IREE_ELF_ELFOSABI_STANDALONE:
      return "standalone";
    default:
      return "unknown";
  }
}

static const char* fatelf_word_size_id_str(iree_elf64_byte_t value) {
  switch (value) {
    case IREE_FATELF_WORD_SIZE_32:
      return "lp32";
    case IREE_FATELF_WORD_SIZE_64:
      return "lp64";
    default:
      return "lpUNK";
  }
}

static const char* fatelf_byte_order_id_str(iree_elf64_byte_t value) {
  switch (value) {
    case IREE_FATELF_BYTE_ORDER_MSB:
      return "be";
    case IREE_FATELF_BYTE_ORDER_LSB:
      return "le";
    default:
      return "xx";
  }
}

// Splits a FatELF into multiple files, writing each beside the input file.
static iree_status_t fatelf_split(int argc, char** argv) {
  iree_file_contents_t* fatelf_contents = NULL;
  IREE_RETURN_IF_ERROR(
      iree_file_read_contents(argv[0], IREE_FILE_READ_FLAG_DEFAULT,
                              iree_allocator_system(), &fatelf_contents));
  iree_fatelf_header_t* header = NULL;
  IREE_RETURN_IF_ERROR(fatelf_parse(fatelf_contents->const_buffer, &header));

  iree_string_view_t dirname, basename;
  iree_file_path_split(iree_make_cstring_view(argv[0]), &dirname, &basename);
  iree_string_view_t stem, extension;
  iree_file_path_split_basename(basename, &stem, &extension);

  for (iree_elf64_byte_t i = 0; i < header->record_count; ++i) {
    const iree_fatelf_record_t* record = &header->records[i];

    const char* machine_str = fatelf_machine_id_str(record->machine);
    const char* osabi_str = fatelf_osabi_id_str(record->osabi);
    const char* word_size_str = fatelf_word_size_id_str(record->word_size);
    const char* byte_order_str = fatelf_byte_order_id_str(record->byte_order);

    char record_path[2048];
    iree_host_size_t record_path_length =
        snprintf(record_path, IREE_ARRAYSIZE(record_path),
                 "%.*s%s%.*s.%s_%s_%s%s.so", (int)dirname.size, dirname.data,
                 dirname.size ? "/" : "", (int)stem.size, stem.data,
                 machine_str, osabi_str, word_size_str, byte_order_str);
    record_path_length =
        iree_file_path_canonicalize(record_path, record_path_length);

    fprintf(stdout, "Writing record[%d] to '%.*s'...\n", i,
            (int)record_path_length, record_path);

    iree_const_byte_span_t record_data = iree_make_const_byte_span(
        fatelf_contents->const_buffer.data + record->offset, record->size);
    IREE_RETURN_IF_ERROR(iree_file_write_contents(record_path, record_data));
  }

  fprintf(stdout, "Wrote %d records to %.*s!\n", header->record_count,
          (int)dirname.size, dirname.data);
  iree_allocator_free(iree_allocator_system(), header);
  iree_file_contents_free(fatelf_contents);
  return iree_ok_status();
}

// Selects the ELF matching the current host config from a FatELF and writes
// it to stdout.
static iree_status_t fatelf_select(int argc, char** argv) {
  IREE_SET_BINARY_MODE(stdout);  // ensure binary output mode
  iree_file_contents_t* fatelf_contents = NULL;
  IREE_RETURN_IF_ERROR(
      iree_file_read_contents(argv[0], IREE_FILE_READ_FLAG_DEFAULT,
                              iree_allocator_system(), &fatelf_contents));
  iree_const_byte_span_t elf_data = iree_const_byte_span_empty();
  IREE_RETURN_IF_ERROR(
      iree_fatelf_select(fatelf_contents->const_buffer, &elf_data));
  fwrite(elf_data.data, 1, elf_data.data_length, stdout);
  iree_file_contents_free(fatelf_contents);
  return iree_ok_status();
}

static const char* fatelf_word_size_enum_str(iree_elf64_byte_t value) {
  switch (value) {
    case IREE_FATELF_WORD_SIZE_32:
      return "ELFCLASS32";
    case IREE_FATELF_WORD_SIZE_64:
      return "ELFCLASS64";
    default:
      return "<unknown>";
  }
}

static const char* fatelf_byte_order_enum_str(iree_elf64_byte_t value) {
  switch (value) {
    case IREE_FATELF_BYTE_ORDER_MSB:
      return "ELFDATA2MSB (big-endian)";
    case IREE_FATELF_BYTE_ORDER_LSB:
      return "ELFDATA2LSB (little-endian)";
    default:
      return "<unknown>";
  }
}

// Dumps the FatELF file records.
static iree_status_t fatelf_dump(int argc, char** argv) {
  iree_file_contents_t* fatelf_contents = NULL;
  IREE_RETURN_IF_ERROR(
      iree_file_read_contents(argv[0], IREE_FILE_READ_FLAG_DEFAULT,
                              iree_allocator_system(), &fatelf_contents));
  iree_fatelf_header_t* header = NULL;
  IREE_RETURN_IF_ERROR(fatelf_parse(fatelf_contents->const_buffer, &header));

  fprintf(stdout, "iree_fatelf_header_t:\n");
  fprintf(stdout, "    magic: %" PRIX32 "\n", header->magic);
  fprintf(stdout, "  version: %d\n", header->version);
  fprintf(stdout, "  records: %d\n", header->record_count);
  fprintf(stdout, " reserved: %" PRIX8 "\n", header->reserved);
  fprintf(stdout, "\n");

  for (iree_elf64_byte_t i = 0; i < header->record_count; ++i) {
    const iree_fatelf_record_t* record = &header->records[i];
    fprintf(stdout, "iree_fatelf_record_t[%d]:\n", i);
    fprintf(stdout, "    machine: %d / %04X = %s\n", record->machine,
            record->machine, fatelf_machine_id_str(record->machine));
    fprintf(stdout, "      osabi: %d / %02X = %s\n", record->osabi,
            record->osabi, fatelf_osabi_id_str(record->osabi));
    fprintf(stdout, "    version: %d / %02X\n", record->osabi_version,
            record->osabi_version);
    fprintf(stdout, "  word_size: %d / %02X = %s\n", record->word_size,
            record->word_size, fatelf_word_size_enum_str(record->word_size));
    fprintf(stdout, " byte_order: %d / %02X = %s\n", record->byte_order,
            record->byte_order, fatelf_byte_order_enum_str(record->byte_order));
    fprintf(stdout, "  reserved0: %d / %02X\n", record->reserved0,
            record->reserved0);
    fprintf(stdout, "  reserved1: %d / %02X\n", record->reserved1,
            record->reserved1);
    fprintf(stdout, "     offset: %" PRIu64 " / %016" PRIX64 "\n",
            record->offset, record->offset);
    fprintf(stdout, "       size: %" PRIu64 " / %016" PRIX64 "\n", record->size,
            record->size);
    fprintf(stdout, "\n");
  }

  iree_allocator_free(iree_allocator_system(), header);
  iree_file_contents_free(fatelf_contents);
  return iree_ok_status();
}

int main(int argc, char** argv) {
  if (argc < 2) {
    return print_usage();
  }

  char* command = argv[1];
  int command_argc = argc - 2;
  char** command_argv = argv + 2;

  iree_status_t status = iree_ok_status();
  if (strcmp(command, "join") == 0) {
    if (command_argc < 1) return print_usage();
    status = fatelf_join(command_argc, command_argv);
  } else if (strcmp(command, "split") == 0) {
    if (command_argc != 1) return print_usage();
    status = fatelf_split(command_argc, command_argv);
  } else if (strcmp(command, "select") == 0) {
    if (command_argc != 1) return print_usage();
    status = fatelf_select(command_argc, command_argv);
  } else if (strcmp(command, "dump") == 0) {
    if (command_argc != 1) return print_usage();
    status = fatelf_dump(command_argc, command_argv);
  } else {
    return print_usage();
  }

  if (!iree_status_is_ok(status)) {
    fprintf(stderr, "iree-fatelf encountered error:\n");
    iree_status_fprint(stderr, status);
    iree_status_free(status);
    return 1;
  }
  return 0;
}
