/**
 * COMMON symbol ordering plugin for linkers supporting the External Linker Plugin API.
 *
 * Copyright (C) 2025 Tharo
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <assert.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include "endian.h"
#include "elf.h"
#include "plugin-api.h"

#define ELF32_IDENT_VALID(ehdr)                                                   \
   ((ehdr)->e_ident[EI_MAG0] == ELFMAG0 && (ehdr)->e_ident[EI_MAG1] == ELFMAG1 && \
    (ehdr)->e_ident[EI_MAG2] == ELFMAG2 && (ehdr)->e_ident[EI_MAG3] == ELFMAG3)

#define ELF32_IS_32(ehdr) ((ehdr)->e_ident[EI_CLASS] == ELFCLASS32)

#define ELF32_IS_BE(ehdr) ((ehdr)->e_ident[EI_DATA] == ELFDATA2MSB)

#define iswhitespace(c) (isblank(c) || ((c) == '\n'))

#define GLUE2(a,b) a##b
#define GLUE(a,b) GLUE2(a,b)
#define ARRLEN(a) (sizeof(a) / sizeof((a)[0]))

#define BITSET_TYPE             uint64_t
#define BITSET_MASK             (8 * sizeof(BITSET_TYPE) - 1)
#define BITSET_SHIFT            (__builtin_ctz((uint32_t)~BITSET_MASK))
#define BITSET_SIZE_BYTES(n)    ((((n) + 8 * sizeof(BITSET_TYPE) - 1) & ~(8 * sizeof(BITSET_TYPE) - 1)) >> 3)
#define BITSET_ALLOC(size)      calloc(1, BITSET_SIZE_BYTES(size))
#define BITSET_FREE(set)        free(set)
#define BITSET_GET(set, key)    ((set)[(key) >> BITSET_SHIFT] & ((BITSET_TYPE)1 << ((key) & BITSET_MASK)))
#define BITSET_SET(set, key)    ((set)[(key) >> BITSET_SHIFT] |= ((BITSET_TYPE)1 << ((key) & BITSET_MASK)))

#define strequ(s1, s2const) (strncmp(s1, s2const, sizeof(s2const) - 1) == 0)

struct bss_ofile {
    char        *path;
    size_t       num_symbols;
    char       **symbol_names;
    Elf32_Sym   *symbols;
    BITSET_TYPE *symbols_found;
};

static struct {
    // Our state
    char                               *string_pool;
    struct bss_ofile                   *ofiles;
    size_t                              num_ofiles;
    // Our options
    char                               *order_path;
    size_t                              min_align;
    // LD plugin
    int                                 api_version;
    int                                 gnu_ld_version;
    int                                 linker_output;
    const char                         *output_name;
    ld_plugin_message                   message __attribute__((format(printf, 2, 3)));
    ld_plugin_add_symbols               add_symbols;
    ld_plugin_get_symbols               get_symbols;
    ld_plugin_get_symbols               get_symbols_v2;
    ld_plugin_add_input_file            add_input_file;
    ld_plugin_get_input_file            get_input_file;
    ld_plugin_register_cleanup          register_cleanup;
    ld_plugin_register_claim_file       register_claim_file;
    ld_plugin_register_all_symbols_read register_all_symbols_read;
} pl;

#define ltell(fd) lseek((fd), 0, SEEK_CUR)

static int read_s_(int fd, void *buf, size_t nbyte, int line)
{
    ssize_t result = read(fd, buf, nbyte);
    if ((size_t)result != nbyte) {
        pl.message(LDPL_FATAL, "[%d] Could not read %lu bytes from input file [%ld]", line, nbyte, result);
        return 1;
    }
    return 0;
}
#define read_s(fd, buf, nbyte) \
    do { if (read_s_(fd, buf, nbyte, __LINE__)) return LDPS_ERR; } while (0)

#define SEEKF(file, pos) \
    lseek((file)->fd, (file)->offset + (pos), SEEK_SET)

#define WITH_NEW_FILE_POS(file, pos)                                                            \
    for (off_t GLUE(_o_,__LINE__) = ltell((file)->fd),GLUE(_b_,__LINE__)=(SEEKF(file, pos), 1); \
         GLUE(_b_,__LINE__);                                                                    \
         GLUE(_b_,__LINE__)=0,lseek((file)->fd, GLUE(_o_,__LINE__), SEEK_SET))

static bool
elf_syms_equal(Elf32_Sym *sym1, Elf32_Sym *sym2)
{
    return (sym1->st_value == sym2->st_value) &&
           (sym1->st_size == sym2->st_size) &&
           (sym1->st_info == sym2->st_info) &&
           (sym1->st_other == sym2->st_other) &&
           (sym1->st_shndx == sym2->st_shndx);
}

static enum ld_plugin_status
claim_file(const struct ld_plugin_input_file *file, int *claimed)
{
    *claimed = false;

    // Read the input file for COMMON symbol definitions
    // The input is assumed to be a Big-Endian 32-bit ELF file

    bool error = false;

    WITH_NEW_FILE_POS(file, 0) {
        Elf32_Ehdr ehdr;
        read_s(file->fd, &ehdr, sizeof(ehdr));

        if (!ELF32_IDENT_VALID(&ehdr) || !ELF32_IS_32(&ehdr) || !ELF32_IS_BE(&ehdr)) {
            pl.message(LDPL_FATAL, "Input file %s is not a 32-bit Big-Endian ELF file", file->name);
            return LDPS_ERR;
        }

        uint32_t e_shoff = be32toh(ehdr.e_shoff);
        uint16_t e_shentsize = be16toh(ehdr.e_shentsize);
        uint16_t e_shnum = be16toh(ehdr.e_shnum);

        if (e_shentsize != sizeof(Elf32_Shdr)) {
            pl.message(LDPL_FATAL, "e_shentsize unexpected size");
            return LDPS_ERR;
        }

        Elf32_Shdr strtab;
        char *strtab_data = NULL;
        size_t last_strtab_ind = (size_t)-1;

        // Look for symbol tables to search for COMMON symbols in
        SEEKF(file, e_shoff);
        for (size_t i = 0; i < e_shnum; i++) {
            Elf32_Shdr shdr;
            read_s(file->fd, &shdr, sizeof(shdr));

            uint32_t sh_type = be32toh(shdr.sh_type);

            if (sh_type != SHT_SYMTAB)
                continue;

            // Found a symtab
            uint32_t sh_offset = be32toh(shdr.sh_offset);
            uint32_t sh_size = be32toh(shdr.sh_size);

            // Read strtab for this symtab, cache it
            size_t strtab_ind = be32toh(shdr.sh_link);
            if (strtab_ind != last_strtab_ind) {
                if (strtab_data != NULL)
                    free(strtab_data);

                // Read strtab header
                WITH_NEW_FILE_POS(file, e_shoff + strtab_ind * sizeof(Elf32_Shdr)) {
                    read_s(file->fd, &strtab, sizeof(strtab));
                }

                // Read strtab contents
                uint32_t strtab_offset = be32toh(strtab.sh_offset);
                uint32_t strtab_size = be32toh(strtab.sh_size);
                strtab_data = malloc(strtab_size);
                WITH_NEW_FILE_POS(file, strtab_offset) {
                    read_s(file->fd, strtab_data, strtab_size);
                }

                last_strtab_ind = strtab_ind;
            }

            // Read each symbol, save the COMMON symbols into a list
            WITH_NEW_FILE_POS(file, sh_offset) {
                size_t nsym = sh_size / sizeof(Elf32_Sym);

                for (size_t j = 0; j < nsym; j++) {
                    Elf32_Sym elfsym;
                    read_s(file->fd, &elfsym, sizeof(elfsym));

                    if (be16toh(elfsym.st_shndx) != SHN_COMMON)
                        continue; // Skip non-COMMON symbols

                    const char *sym_name = &strtab_data[be32toh(elfsym.st_name)];

                    // Try to find this symbol in one of the specified bss output files
                    for (size_t f = 0; f < pl.num_ofiles; f++) {
                        struct bss_ofile *ofile = &pl.ofiles[f];

                        for (size_t k = 0; k < ofile->num_symbols; k++) {
                            if (strcmp(sym_name, ofile->symbol_names[k]) == 0) {
                                if (BITSET_GET(ofile->symbols_found, k)) {
                                    // Already occupied, check equivalence
                                    if (!elf_syms_equal(&ofile->symbols[k], &elfsym)) {
                                        pl.message(LDPL_ERROR, "Found distinct COMMON symbols with the same name: %s",
                                                   ofile->symbol_names[k]);
                                        error = true;
                                    }
                                } else {
                                    BITSET_SET(ofile->symbols_found, k);
                                    ofile->symbols[k] = elfsym;
                                }
                                goto found;
                            }
                        }
                    }
                    pl.message(LDPL_WARNING,
                               "Found COMMON symbol %s in input file %s not mentioned in the order spec",
                               sym_name, file->name);
                found:;
                }
            }
        }
        if (strtab_data != NULL)
            free(strtab_data);
    }
    return (error) ? LDPS_ERR : LDPS_OK;
}

static enum ld_plugin_status
all_symbols_read(void)
{
    for (size_t f = 0; f < pl.num_ofiles; f++) {
        struct bss_ofile *ofile = &pl.ofiles[f];

        // Make sure we got every symbol mentioned
        bool error = false;
        for (size_t i = 0; i < ofile->num_symbols; i++) {
            if (!BITSET_GET(ofile->symbols_found, i)) {
                pl.message(LDPL_ERROR, "Did not find symbol %s mentioned in the order specification in any input file",
                           ofile->symbol_names[i]);
                error = true;
            }
        }
        if (error)
            return LDPS_ERR;

        // Create a dummy ELF file full of bss definitions generated from the COMMON defs and add it as an input file.
        // TODO look into replacing all this with pl.add_symbols? Seems like it's not possible
        // to add symbols at this stage though, since pl.add_symbols requires a file handle..
        //  ELF layout:
        //  Elf32_Ehdr  ehdr
        //  char        symbol_names[sum of name lengths]
        //  Elf32_Sym   symbols[nsyms]
        //  char        section_names[sum of name lengths]
        //  Elf32_Shdr  NULL
        //  Elf32_Shdr  .bss
        //  Elf32_Shdr  symtab
        //  Elf32_Shdr  strtab
        //  Elf32_Shdr  shstrtab

        FILE *elf = fopen(ofile->path, "wb");
        if (elf == NULL) {
            pl.message(LDPL_FATAL, "Could not open bss output file \"%s\" for writing", ofile->path);
            return LDPS_ERR;
        }

        // Skip the ELF header, we'll write it at the end.
        fseek(elf, sizeof(Elf32_Ehdr), SEEK_SET);

        // First write the strtab
        uint32_t *string_offsets = malloc((2 + ofile->num_symbols) * sizeof(uint32_t));
        size_t strtab_offset = ftell(elf);
        size_t n = 0;
        string_offsets[n++] = ftell(elf) - strtab_offset;
        fprintf(elf, "%c", '\0');
        string_offsets[n++] = ftell(elf) - strtab_offset;
        fprintf(elf, "%s%c", ".bss", '\0');
        for (size_t i = 0; i < ofile->num_symbols; i++) {
            string_offsets[n++] = ftell(elf) - strtab_offset;
            fprintf(elf, "%s%c", ofile->symbol_names[i], '\0');
        }
        size_t strtab_size = ftell(elf) - strtab_offset;

        // Align to 4 for symbols
        fwrite("\0\0\0\0", 4 - (ftell(elf) & 3), 1, elf);

        // Record greatest alignment for the overall section alignment
        size_t greatest_align = 0;

        // Now write symbols
        uint32_t symtab_offset = ftell(elf);
        n = 0;

        Elf32_Sym null_sym = {
            .st_name = htobe32(string_offsets[n++]),
            .st_value = 0,
            .st_size = 0,
            .st_info = ELF32_ST_INFO(STB_LOCAL, STT_NOTYPE),
            .st_other = ELF32_ST_VISIBILITY(STV_DEFAULT),
            .st_shndx = htobe16(SHN_UNDEF),
        };
        fwrite(&null_sym, sizeof(null_sym), 1, elf);
        Elf32_Sym bss_sym = {
            .st_name = htobe32(string_offsets[n++]),
            .st_value = 0,
            .st_size = 0,
            .st_info = ELF32_ST_INFO(STB_LOCAL, STT_SECTION),
            .st_other = ELF32_ST_VISIBILITY(STV_DEFAULT),
            .st_shndx = htobe16(1), /* .bss */
        };
        fwrite(&bss_sym, sizeof(bss_sym), 1, elf);

        size_t sym_offset = 0;
        for (size_t i = 0; i < ofile->num_symbols; i++) {
            Elf32_Sym *sym = &ofile->symbols[i];
            uint32_t align = be32toh(sym->st_value);
            sym_offset = (sym_offset + align - 1) & ~(align - 1);

            if (align > greatest_align)
                greatest_align = align;

            Elf32_Sym outsym = {
                .st_name = htobe32(string_offsets[n++]),
                .st_value = htobe32(sym_offset),
                .st_size = sym->st_size,
                .st_info = sym->st_info,
                .st_other = sym->st_other,
                .st_shndx = htobe16(1), /* .bss */
            };
            fwrite(&outsym, sizeof(outsym), 1, elf);

            sym_offset += be32toh(sym->st_size);
        }
        if (greatest_align < pl.min_align)
            greatest_align = pl.min_align;
        sym_offset = (sym_offset + greatest_align - 1) & ~(greatest_align - 1);

        size_t symtab_size = ftell(elf) - symtab_offset;

        free(string_offsets);

        const uint32_t shdr_name_offsets[5] = { 0, 1, 6, 13, 20 };
        size_t shstrtab_offset = ftell(elf);
        // Write shstrtab
        fwrite(
            /* NULL     [ 0] */ "\0"
            /* .bss     [ 1] */ ".bss\0"
            /* symtab   [ 6] */ "symtab\0"
            /* strtab   [13] */ "strtab\0"
            /* shstrtab [20] */ "shstrtab\0"
            /*          [29] */ "\0\0\0",
            32, 1, elf
        );
        size_t shstrtab_size = ftell(elf) - shstrtab_offset;

        // We need: { NULL, .bss, symtab, strtab, shstrtab }
        Elf32_Shdr shdrs[5] = {
            /* NULL */ {
                .sh_name = htobe32(shdr_name_offsets[0]),
                .sh_type = htobe32(SHT_NULL),
                .sh_flags = htobe32(0),
                .sh_addr = htobe32(0x00000000),
                .sh_offset = htobe32(0x00000000),
                .sh_size = htobe32(0x00000000),
                .sh_link = htobe32(0), /* No Link */
                .sh_info = htobe32(0),
                .sh_addralign = htobe32(0),
                .sh_entsize = htobe32(0), /* Not a fixed-length array */
            },
            /* .bss */ {
                .sh_name = htobe32(shdr_name_offsets[1]),
                .sh_type = htobe32(SHT_NOBITS),
                .sh_flags = htobe32(SHF_WRITE | SHF_ALLOC),
                .sh_addr = htobe32(0x00000000),
                .sh_offset = htobe32(sizeof(Elf32_Ehdr)),
                .sh_size = htobe32(sym_offset),
                .sh_link = htobe32(0), /* No Link */
                .sh_info = htobe32(0),
                .sh_addralign = htobe32(greatest_align),
                .sh_entsize = htobe32(0), /* Not a fixed-length array */
            },
            /* symtab */ {
                .sh_name = htobe32(shdr_name_offsets[2]),
                .sh_type = htobe32(SHT_SYMTAB),
                .sh_flags = htobe32(0),
                .sh_addr = htobe32(0),
                .sh_offset = htobe32(symtab_offset),
                .sh_size = htobe32(symtab_size),
                .sh_link = htobe32(3), /* strtab */
                .sh_info = htobe32(2),
                .sh_addralign = htobe32(4),
                .sh_entsize = htobe32(sizeof(Elf32_Sym)),
            },
            /* strtab */ {
                .sh_name = htobe32(shdr_name_offsets[3]),
                .sh_type = htobe32(SHT_STRTAB),
                .sh_flags = htobe32(0),
                .sh_addr = htobe32(0),
                .sh_offset = htobe32(strtab_offset),
                .sh_size = htobe32(strtab_size),
                .sh_link = htobe32(0), /* No Link */
                .sh_info = htobe32(0),
                .sh_addralign = htobe32(1),
                .sh_entsize = htobe32(0), /* Not a fixed-length array */
            },
            /* shstrtab */ {
                .sh_name = htobe32(shdr_name_offsets[4]),
                .sh_type = htobe32(SHT_STRTAB),
                .sh_flags = htobe32(0),
                .sh_addr = htobe32(0),
                .sh_offset = htobe32(shstrtab_offset),
                .sh_size = htobe32(shstrtab_size),
                .sh_link = htobe32(0), /* No Link */
                .sh_info = htobe32(0),
                .sh_addralign = htobe32(1),
                .sh_entsize = htobe32(0), /* Not a fixed-length array */
            },
        };
        size_t shdrs_offset = ftell(elf);
        fwrite(shdrs, sizeof(shdrs), 1, elf);

        // TODO some of these fields should change based on the properties of the input files, or even better
        // it should be based on the current output format but that might not be visible to us with the limited
        // plugin api...
        Elf32_Ehdr ehdr = {
            .e_ident = {
                [EI_MAG0] = ELFMAG0,
                [EI_MAG1] = ELFMAG1,
                [EI_MAG2] = ELFMAG2,
                [EI_MAG3] = ELFMAG3,
                [EI_CLASS] = ELFCLASS32,
                [EI_DATA] = ELFDATA2MSB,
                [EI_VERSION] = 1,
                [EI_OSABI] = ELFOSABI_SYSV,
                [EI_ABIVERSION] = 0,
            },
            .e_type = htobe16(ET_REL),
            .e_machine = htobe16(EM_MIPS),
            .e_version = htobe32(EV_CURRENT),
            .e_entry = htobe32(0x00000000),
            .e_phoff = htobe32(0x00000000),
            .e_shoff = htobe32(shdrs_offset),
            .e_flags = htobe32(EF_MIPS_ARCH_3 | EF_MIPS_32BITMODE | EF_MIPS_NOREORDER),
            .e_ehsize = htobe16(sizeof(Elf32_Ehdr)),
            .e_phentsize = htobe16(0x0000),
            .e_phnum = htobe16(0),
            .e_shentsize = htobe16(sizeof(Elf32_Shdr)),
            .e_shnum = htobe16(ARRLEN(shdrs)),
            .e_shstrndx = htobe16(4), /* shstrtab */
        };
        fseek(elf, 0, SEEK_SET);
        fwrite(&ehdr, sizeof(ehdr), 1, elf);

        fflush(elf);
        fclose(elf);

        // Add it as an additional input file
        enum ld_plugin_status ps = pl.add_input_file(ofile->path);
        if (ps != LDPS_OK) {
            pl.message(LDPL_ERROR, "error adding %s as an additional input file\n", ofile->path);
            return ps;
        }
    }
    return LDPS_OK;
}

static enum ld_plugin_status
cleanup(void)
{
    free(pl.string_pool);
    free(pl.order_path);
    for (size_t f = 0; f < pl.num_ofiles; f++) {
        struct bss_ofile *ofile = &pl.ofiles[f];
        free(ofile->symbol_names);
        free(ofile->symbols);
        BITSET_FREE(ofile->symbols_found);
    }
    free(pl.ofiles);
    return LDPS_OK;
}

static enum ld_plugin_status
parse_order_file(const char *order_file)
{
    // Read the entire bss order file and null-terminate it
    FILE *bss_order = fopen(order_file, "rb");
    if (bss_order == NULL) {
        pl.message(LDPL_FATAL, "Could not open bss order file \"%s\" for reading: %s", order_file, strerror(errno));
        return LDPS_ERR;
    }

    fseek(bss_order, 0, SEEK_END);
    size_t fsize = ftell(bss_order);
    fseek(bss_order, 0, SEEK_SET);

    pl.string_pool = malloc((fsize + 1) * sizeof(char));
    if (fread(pl.string_pool, fsize, 1, bss_order) != 1) {
        pl.message(LDPL_FATAL, "Failed to read bss order file \"%s\": %s", order_file, strerror(errno));
        free(pl.string_pool);
        fclose(bss_order);
        return LDPS_ERR;
    }
    pl.string_pool[fsize] = '\0';

    fclose(bss_order);

    // Strip comments -> spaces, leaving newlines alone for correct error reporting

    int line = 1;
    bool comment = false;
    bool oneline = false;
    char *s = pl.string_pool;
    while (*s != '\0') {
        if (*s == '\n')
            line++;

        if (*s == '#' || (*s == '/' && s[1] == '/'))
            comment = true, oneline = true;
        else if (*s == '/' && s[1] == '*')
            comment = true, oneline = false;

        if (oneline && *s == '\n')
            comment = false;

        if (comment && !oneline && *s == '*' && s[1] == '/')
            comment = false, s[0] = s[1] = ' ';

        if (comment && (*s != '\n'))
            *s = ' ';

        s++;
    }

    const char *errmsg = "?";

#define SYNTAX_ERR(msg) do { errmsg = msg; goto syntaxerror; } while (0)

    if (comment && !oneline)
        SYNTAX_ERR("Unclosed multi-line comment");

    // Before any detailed parsing, allocate memory based on upper bounds for the number of output files and symbols
    size_t total_file_count = 0;
    s = pl.string_pool;
    for (size_t i = 0; i < fsize; i++, s++) {
        if (*s == '{')
            total_file_count++;
    }
    pl.num_ofiles = total_file_count;
    pl.ofiles = malloc(total_file_count * sizeof(struct bss_ofile));

    size_t cur_ofile = (size_t)-1;
    size_t cur_symbol = 0;

    s = pl.string_pool;
    for (size_t i = 0; i < fsize; i++, s++) {
        if (*s == '{') {
            if (cur_ofile != (size_t)-1) {
                assert (cur_ofile < total_file_count);
                pl.ofiles[cur_ofile].num_symbols = cur_symbol;
                pl.ofiles[cur_ofile].symbols = malloc(cur_symbol * sizeof(Elf32_Sym));
                pl.ofiles[cur_ofile].symbol_names = malloc(cur_symbol * sizeof(char *));
                pl.ofiles[cur_ofile].symbols_found = BITSET_ALLOC(cur_symbol);
                cur_symbol = 0;
            }
            cur_ofile++;
        } else if (*s == ';') {
            cur_symbol++;
        }
    }
    assert (cur_ofile < total_file_count);
    pl.ofiles[cur_ofile].num_symbols = cur_symbol;
    pl.ofiles[cur_ofile].symbols = malloc(cur_symbol * sizeof(Elf32_Sym));
    pl.ofiles[cur_ofile].symbol_names = malloc(cur_symbol * sizeof(char *));
    pl.ofiles[cur_ofile].symbols_found = BITSET_ALLOC(cur_symbol);

    bool quote = false;
    bool eof_ok = false;
    char *start = NULL;
    char *end = NULL;
    void *next = &&fpath;
    line = 1;
    cur_ofile = (size_t)-1;
    cur_symbol = 0;

    s = pl.string_pool;
whitespace:
    // Skip whitespace, count newlines for syntax error messages
    while (iswhitespace(*s)) {
        if (*s == '\n') line++;
        s++;
    };
    // If we're at EOF, check if that's OK or whether it's a syntax error
    if (*s == '\0') {
        if (eof_ok) goto eof;
        SYNTAX_ERR("Unexpected EOF");
    }
    goto *next;

fpath:
    // We're looking for a file path now, up until an opening brace {.
    // If the file path is unquoted, the first whitespace encountered cuts this short, but if the file path
    // is quoted whitespace will be included in the path up until the closing quote. If quoted, newlines will
    // cause a syntax error if used between the quotes.
    eof_ok = false;
    // Check quote immediately, it's only allowed at the start
    if (*s == '"') s++, quote = true;

    // Consume the path
    start = s;
    while (*s != '{') {
        if (*s == '\0') SYNTAX_ERR("Unexpected EOF");
        if (!quote && iswhitespace(*s)) break; // Unquoted whitespace cuts this short
        if (quote && *s == '\n') SYNTAX_ERR("Quoted string spanning multiple lines");
        if (quote && *s == '"') break; // Ending quotes cuts this short
        s++;
    }
    end = s;

    // Check for closing quote
    if (*s == '"') {
        if (!quote) SYNTAX_ERR("Found a closing quote with no opening quote");
        quote = false;
        s++;
    }

    // Consume any whitespace up to {
    next = &&obrace;
    goto whitespace;

obrace:
    // We're looking for an opening brace character {, anything else is a fail
    if (*s++ != '{') SYNTAX_ERR("Expected an {");
    // NULL-terminate the file path in-place and save it
    *end = '\0';
    cur_ofile++;
    cur_symbol = 0;
    assert(cur_ofile < total_file_count);
    pl.ofiles[cur_ofile].path = start;
    // Consume whitespace up to the first symbol name
    next = &&sym;
    goto whitespace;

sym:
    // Symbol names are similar to the file path, but we're looking to end on a semicolon ;
    // If the symbol name is unquoted, the first whitespace encountered cuts this short, but if the symbol name
    // is quoted whitespace will be included in the name up until the closing quote. If quoted, newlines will
    // cause a syntax error if used between the quotes.
    // Check quote immediately, it's only allowed at the start
    if (*s == '"') quote = true, s++;

    // Consume the symbol name
    start = s;
    while (*s != ';') {
        if (*s == '\0') SYNTAX_ERR("Unexpected EOF");
        if (quote && *s == '\n') SYNTAX_ERR("Quoted string spanning multiple lines");
        // Unlike file paths, we're not friendly to whitespace between the end of the symbol and the semicolon
        // TODO we could be though?
        if (!quote && iswhitespace(*s)) SYNTAX_ERR("Whitespace in unquoted symbol name");
        if (quote && *s == '"') {
            // It's tempting to allow whitespace here but it would be inconsistent with the above, but if the above
            // changes this should similarly allow whitespace
            if (s[1] != ';') SYNTAX_ERR("Expected ;");
            break;
        }
        s++;
    }
    end = s;

    // Check for end quote
    if (*s == '"') {
        if (!quote) SYNTAX_ERR("Found a closing quote with no opening quote");
        quote = false;
        s++;
    }
    // This assertion should always be true unless the above logic is wrong, it's
    // not meant to be caused by a syntax error.
    assert(*s++ == ';');

    // NULL-terminate
    *end = '\0';
    assert(cur_symbol < pl.ofiles[cur_ofile].num_symbols);
    pl.ofiles[cur_ofile].symbol_names[cur_symbol++] = start;

    // Consume any whitespace following the semicolon
    next = &&cbrace;
    goto whitespace;

cbrace:
    // If the first non-whitespace character after a semicolon isn't }, assume it's another
    // symbol in the list. If it is }, this is the end of the list.
    if (*s != '}') goto sym;
    s++;
    // This might be a valid end of file (after any trailing whitespace) if this is the last
    // output file in the specification.
    pl.ofiles[cur_ofile].num_symbols = cur_symbol;
    eof_ok = true;
    next = &&fpath;
    goto whitespace;

eof:
    // Reached end of file, we must have all output files and their symbols at this point.
    assert(cur_ofile < pl.num_ofiles);
    // Reduce the number of files to the actual number from the upper bound
    pl.num_ofiles = cur_ofile + 1;
    return LDPS_OK;

syntaxerror:
    pl.message(LDPL_FATAL, "%s(%d): Syntax error: %s", pl.order_path, line, errmsg);
    return LDPS_ERR;
}

static enum ld_plugin_status
read_int(size_t *out, const char *s)
{
    if (s[0] == '\0')
        goto err;

    size_t res;

    size_t length = strlen(s);
    size_t start = 0;

    // we expect unsigned input, error on - and skip +
    if (s[start] == '-')
        goto err;
    if (s[start] == '+')
        start++;

    // determine the base of the input via the presence of an 0x prefix
    int base;
    if (start + 2 < length && s[start + 0] == '0' && tolower(s[start + 1]) == 'x') {
        start += 2;
        base = 16;
    } else {
        base = 10;
    }

    // read the value in the appropriate base
    char *str_end;
    res = strtoull(&s[start], &str_end, base);
    size_t end = str_end - s;
    if (start == end)
        goto err;

    *out = res;
    return LDPS_OK;
err:
    fprintf(stderr, "Bad input to min_align, must be an unsigned integer in base 10 or 16: %s", s);
    return LDPS_ERR;
}

static enum ld_plugin_status
parse_option(const char *opt)
{
    if (strequ(opt, "order=")) {
        pl.order_path = strdup(opt + sizeof("order=") - 1);
        return LDPS_OK;
    }
    if (strequ(opt, "min_align=")) {
        return read_int(&pl.min_align, opt + sizeof("min_align=") - 1);
    }

    fprintf(stderr, "Unknown option: %s\n", opt);
    return LDPS_ERR;
}

enum ld_plugin_status
onload(struct ld_plugin_tv *tv)
{
    enum ld_plugin_status ps;

    // Initialize our state
    memset(&pl, 0, sizeof(pl));
    pl.api_version = -1;
    pl.gnu_ld_version = -1;
    pl.linker_output = -1;

    // Parse the transfer vector
    for (struct ld_plugin_tv *ptv = tv; ptv->tv_tag != LDPT_NULL; ptv++) {
        switch (ptv->tv_tag) {
            case LDPT_OPTION:
                if (ps = parse_option(ptv->tv_u.tv_string), ps != LDPS_OK)
                    return ps;
                break;

            case LDPT_MESSAGE:
                pl.message = ptv->tv_u.tv_message;
                break;

            case LDPT_API_VERSION:
                pl.api_version = ptv->tv_u.tv_val;
                break;

            case LDPT_GNU_LD_VERSION:
                pl.gnu_ld_version = ptv->tv_u.tv_val;
                break;

            case LDPT_LINKER_OUTPUT:
                pl.linker_output = ptv->tv_u.tv_val;
                break;

            case LDPT_OUTPUT_NAME:
                pl.output_name = ptv->tv_u.tv_string;
                break;

            case LDPT_REGISTER_CLAIM_FILE_HOOK:
                pl.register_claim_file = ptv->tv_u.tv_register_claim_file;
                break;

            case LDPT_REGISTER_ALL_SYMBOLS_READ_HOOK:
                pl.register_all_symbols_read = ptv->tv_u.tv_register_all_symbols_read;
                break;

            case LDPT_REGISTER_CLEANUP_HOOK:
                pl.register_cleanup = ptv->tv_u.tv_register_cleanup;
                break;

            case LDPT_ADD_SYMBOLS:
                pl.add_symbols = ptv->tv_u.tv_add_symbols;
                break;

            case LDPT_GET_INPUT_FILE:
                pl.get_input_file = ptv->tv_u.tv_get_input_file;
                break;

            case LDPT_GET_SYMBOLS:
                pl.get_symbols = ptv->tv_u.tv_get_symbols;
                break;

            case LDPT_GET_SYMBOLS_V2:
                pl.get_symbols_v2 = ptv->tv_u.tv_get_symbols;
                break;

            case LDPT_ADD_INPUT_FILE:
                pl.add_input_file = ptv->tv_u.tv_add_input_file;
                break;

            default:
                break;
        }
    }

    // Check pl.message
    if (pl.message == NULL) {
        fprintf(stderr, "No message() provided to plugin");
        return LDPS_ERR;
    }

    // Check args

    if (pl.order_path == NULL) {
        pl.message(LDPL_ERROR, "Missing option -plugin-opt order=<order.txt>");
        return LDPS_ERR;
    }

    // Read the order file

    if (ps = parse_order_file(pl.order_path), ps != LDPS_OK)
        return ps;

    // Register callbacks

    pl.register_claim_file(claim_file);
    pl.register_all_symbols_read(all_symbols_read);
    pl.register_cleanup(cleanup);

    return LDPS_OK;
}
