// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright 2022 BitWagon Software LLC.  All rights reserved.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

#ifdef __APPLE__  //{
// /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach-o/loader.h
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#else  //}{
    enum { // cmd
        LC_REQ_DYLD      = 0x80000000,  // OR'ed ==> must not ignore
        LC_SEGMENT       = 0x1,
        LC_SYMTAB        = 0x2,
        LC_THREAD        = 0x4,
        LC_UNIXTHREAD    = 0x5,
        LC_DYSYMTAB      = 0xb,
        LC_LOAD_DYLIB    = 0xc,
        LC_ID_DYLIB      = 0xd,
        LC_LOAD_DYLINKER = 0xe,
        LC_ID_DYLINKER   = 0xf,
        LC_ROUTINES      = 0x11,
        LC_TWOLEVEL_HINTS= 0x16,
        LC_LOAD_WEAK_DYLIB= (0x18 | LC_REQ_DYLD),
        LC_SEGMENT_64    = 0x19,
        LC_ROUTINES_64   = 0x1a,
        LC_UUID          = 0x1b,
        LC_RPATH         = 0x1c,
        LC_CODE_SIGNATURE = 0x1d,
        LC_SEGMENT_SPLIT_INFO = 0x1e,
        LC_REEXPORT_DYLIB = (0x1f | LC_REQ_DYLD),
        LC_LAZY_LOAD_DYLIB= 0x20,
        LC_ENCRYPTION_INFO= 0x21,
        LC_DYLD_INFO      = 0x22,  // compressed dyld information (10.6.x)
        LC_DYLD_INFO_ONLY = (0x22|LC_REQ_DYLD),
        LC_VERSION_MIN_MACOSX= 0x24,
        LC_VERSION_MIN_IPHONEOS= 0x25,
        LC_FUNCTION_STARTS= 0x26,
        LC_DYLD_ENVIRONMENT= 0x27,  // string as environment variable
        LC_MAIN           = (0x28|LC_REQ_DYLD),
        LC_DATA_IN_CODE   = 0x29,
        LC_SOURCE_VERSION = 0x2a,
        LC_DYLIB_CODE_SIGN_DRS= 0x2B,
        LC_ENCRYPTION_INFO_64= 0x2C,
        LC_LINKER_OPTIMIZATION_HINT = 0x2E,
        LC_VERSION_MIN_TVOS= 0x2F,
        LC_VERSION_MIN_WATCHOS= 0x30,
        LC_NOTE           = 0x31,
        LC_BUILD_VERSION  = 0x32,
        LC_DYLD_EXPORTS_TRIE   = (0x33|LC_REQ_DYLD),
        LC_DYLD_CHAINED_FIXUPS = (0x34|LC_REQ_DYLD),
        LC_FILESET_ENTRY       = (0x35|LC_REQ_DYLD),
    };

#include <stdint.h>
struct mach_header_64 {
    uint32_t magic;
    uint32_t cputype;
    uint32_t cpusubtype;
    uint32_t filetype;
    uint32_t ncmds;
    uint32_t sizeofcmds;
    uint32_t flags;
    uint32_t reserved;
};
struct load_command {
    uint32_t cmd;
    uint32_t cmdsize;
};
struct segment_command_64 {
    uint32_t cmd;
    uint32_t cmdsize;
    char     segname[16];
    uint64_t vmaddr;
    uint64_t vmsize;
    uint64_t fileoff;
    uint64_t filesize;
    uint32_t maxprot;
    uint32_t initprot;
    uint32_t nsects;
    uint32_t flags;
};
struct linkedit_data_command {
    uint32_t cmd;
    uint32_t cmdsize;
    uint32_t dataoff;
    uint32_t datasize;
};
struct symtab_command {
    uint32_t cmd;
    uint32_t cmdsize;
    uint32_t symoff;
    uint32_t nsyms;
    uint32_t stroff;
    uint32_t strsize;
};
struct dysymtab_command {
    uint32_t cmd;
    uint32_t cmdsize;

    uint32_t ilocalsym;
    uint32_t nlocalsym;

    uint32_t iextdefsym;
    uint32_t nextdefsym;

    uint32_t iundefsym;
    uint32_t nundefsym;

    uint32_t tocoff;
    uint32_t ntoc;

    uint32_t modtaboff;
    uint32_t nmodtab;

    uint32_t extrefsymoff;
    uint32_t nextrefsyms;

    uint32_t indirectsymoff;
    uint32_t nindirectsyms;

    uint32_t extreloff;
    uint32_t nextrel;

    uint32_t locreloff;
    uint32_t nlocrel;
};
struct nlist_64 {
    union {
        uint32_t  n_strx;
    } n_un;
    uint8_t n_type;
    uint8_t n_sect;
    uint16_t n_desc;
    uint64_t n_value;
};
#endif  //}

struct Cmd_names {
    unsigned char val;
    char name[23];
} const cmd_names[] = {
    [ 0x1]  = { 0x1, "LC_SEGMENT"},
    [ 0x2]  = { 0x2, "LC_SYMTAB"},
    [ 0x4]  = { 0x4, "LC_THREAD"},
    [ 0x5]  = { 0x5, "LC_UNIXTHREAD"},
    [ 0xb]  = { 0xb, "LC_DYSYMTAB"},
    [ 0xc]  = { 0xc, "LC_LOAD_DYLIB"},
    [ 0xd]  = { 0xd, "LC_ID_DYLIB"},
    [ 0xe]  = { 0xe, "LC_LOAD_DYLINKER"},
    [ 0xf]  = { 0xf, "LC_ID_DYLINKER"},
    [ 0x11]  = { 0x11, "LC_ROUTINES"},
    [ 0x16]  = { 0x16, "LC_TWOLEVEL_HINTS"},
    [ (0x18 /*|LC_REQ_DYLD*/ )] = {0x18, "LC_LOAD_WEAK_DYLIB"},
    [ 0x19]  = { 0x19, "LC_SEGMENT_64"},
    [ 0x1a]  = { 0x1a, "LC_ROUTINES_64"},
    [ 0x1b]  = { 0x1b, "LC_UUID"},
    [ 0x1c]  = { 0x1c, "LC_RPATH"},
    [ 0x1d]  = { 0x1d, "LC_CODE_SIGNATURE"},
    [ 0x1e]  = { 0x1e, "LC_SEGMENT_SPLIT_INFO"},
    [ (0x1f /*|LC_REQ_DYLD*/ )] = { 0x1f, "LC_REEXPORT_DYLIB"},
    [ 0x20]  = { 0x20, "LC_LAZY_LOAD_DYLIB"},
    [ 0x21]  = { 0x21, "LC_ENCRYPTION_INFO"},
//  [ 0x22]  = { 0x22, "LC_DYLD_INFO"  // compressed dyld information (10.6.x)
    [ (0x22 /*|LC_REQ_DYLD*/ )]  = { 0x22, "LC_DYLD_INFO_ONLY"},
    [ 0x24]  = { 0x24, "LC_VERSION_MIN_MACOSX"},
    [ 0x25]  = { 0x25, "LC_VERSION_MIN_IPHONEOS"},
    [ 0x26]  = { 0x26, "LC_FUNCTION_STARTS"},
    [ 0x27]  = { 0x27, "LC_DYLD_ENVIRONMENT"},  // string as environment variable
    [ (0x28 /*|LC_REQ_DYLD*/ )]  = { 0x28, "LC_MAIN"},
    [ 0x29]  = { 0x29, "LC_DATA_IN_CODE"},
    [ 0x2a]  = { 0x2a, "LC_SOURCE_VERSION"},
    [ 0x2B]  = { 0x2B, "LC_DYLIB_CODE_SIGN_DRS"},
    [ 0x2C]  = { 0x2C, "LC_ENCRYPTION_INFO_64"},
    [ 0x2F]  = { 0x2F, "LC_VERSION_MIN_TVOS"},
    [ 0x30]  = { 0x30, "LC_VERSION_MIN_WATCHOS"},
    [ 0x31]  = { 0x31, "LC_NOTE"},
    [ 0x32]  = { 0x32, "LC_BUILD_VERSION"},
    [(0x33 /*|LC_REQ_DYLD*/ )]  = {0x33, "LC_DYLD_EXPORTS_TRIE"},
    [(0x34 /*|LC_REQ_DYLD*/ )]  = {0x34, "LC_DYLD_CHAINED_FIXUPS"},
    [(0x35 /*|LC_REQ_DYLD*/ )]  = {0x35, "LC_FILESET_ENTRY"},
};

// Remove (cut out, "snip") named loader_commands from a Macho-O file.
// Try to enable success of running "codesign -s - file" afterwards.
// Note that LC_CODE_SIGNATURE should be removed before snipping:
//   codesign --remove-signature file
//
// This is EXPERIMENTAL to aid in finding a "minimal" executable
// on Apple MacOS Big Sur, particularly Apple M1 hardware (aarch64).
int
main(int argc, char const * /*const*/ *const argv, char const *const *const envp)
{
    struct stat st;
    int fd;
    int err1;
    int res2;
    int err2;
    int prot = PROT_READ | PROT_WRITE;
    int flags = MAP_FIXED | MAP_SHARED;
    void *const awant = (void *)(0x18L << 28);  // above user, below dylibs
    char *addr;

    if (argc < 3) {
        fprintf(stderr, "Usage: macho-snip file loader_cmd...\n");
        exit(1);
    }
    fd = open(argv[1], O_RDWR, 0);
    err1 = errno;
    if (fd < 0) {
        perror(argv[1]);
        fprintf(stderr, "Trying readonly...\n");
        flags = MAP_FIXED | MAP_PRIVATE;
        fd = open(argv[1], O_RDONLY, 0);
    }
    res2 = fstat(fd, &st);
    err2 = errno;

    if (fd < 0) {
        errno = err1;
        perror(argv[1]);
        exit(1);
    }
    if (0!=res2) {
        errno = err2;
        perror(argv[1]);
        exit(1);
    }
    addr = mmap(awant, st.st_size, prot, flags, fd, 0);
    if (awant!=addr) {
        perror(argv[1]);
        exit(1);
    }
    fprintf(stderr,"%zd (%#zx) bytes at %p\n", (long)st.st_size, (long)st.st_size, addr);

    unsigned long argv_done = 0;  // set of bits
    struct segment_command_64 *linkedit = 0;
    struct mach_header_64 *const mhdr = (struct mach_header_64 *)addr;
    unsigned ncmds = mhdr->ncmds;
    unsigned headway = mhdr->sizeofcmds;
    struct load_command *cmd = (struct load_command *)(1+ mhdr);
    struct load_command *cmd_next;
    unsigned delta_dataoff = 0;
    for (; ncmds; --ncmds, cmd = cmd_next) {
        unsigned end_dataoff = 0;
        unsigned end_datasize = 0;
again: ;
        fprintf(stderr, "cmd@%p %s %d(%#x)\n",
            cmd, cmd_names[cmd->cmd&0xFF].name, cmd->cmd&0xFF, cmd->cmd);
        unsigned const cmdsize = cmd->cmdsize;
        if (headway < cmdsize ) {
        }
        else {
            headway -= cmdsize;
            cmd_next = (struct load_command *)(cmdsize + (void *)cmd);
        }
        switch (cmd->cmd &~ LC_REQ_DYLD) {
            int jargv;

        case LC_SEGMENT_64: {
            struct segment_command_64 *seg = (struct segment_command_64 *)cmd;
            if (!strcmp("__LINKEDIT", seg->segname)) {
                linkedit = seg;
            }
        } break;
        case LC_CODE_SIGNATURE: {
            fprintf(stderr, "macho-snip: use 'codesign --remove-signature' to remove LC_CODE_SIGNATURE\n");
        } break;

//struct nlist_64 {
//    union {
//        uint32_t  n_strx; /* index into the string table */
//    } n_un;
//    uint8_t n_type;        /* type flag, see below */
//    uint8_t n_sect;        /* section number or NO_SECT */
//    uint16_t n_desc;       /* see <mach-o/stab.h> */
//    uint64_t n_value;      /* value of this symbol (or stab offset) */
//};
//
// The string table has an extra entry at the front: " " (one space)
// so that the first actual string has an index of 2.
// See the comment which follows the definition of struct nlist_64 in
// /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach-o/nlist.h
//
// The string table must be zero-padded to a multiple of 8 bytes.
// codesign requires that the string table must be last in __LINKEDIT:
// (__LINKEDIT.filesize + .fileoff) == (LC_SYMTAB.strsize + .stroff)
// [The LC_CODE_SIGNATURE.dataoff also aligns itself to (0 mod 16),
// which is peculiar because the data array of sha256 values (one 32-byte
// value per 4KB page) has offset (4 mod 16) instead of (0 mod 16).]

        case LC_SYMTAB: {
            fprintf(stderr, "macho-snip: LC_SYMTAB skipped\n");
            continue;
            struct symtab_command *symcmd = (struct symtab_command *)cmd;
            if ((  symcmd->strsize +    symcmd->stroff)
            != (linkedit->filesize + linkedit->fileoff)) {
                fprintf(stderr,"macho-snip: bad LC_SYMTAB string table\n");
            }
            // find beginning of last name string
            unsigned j;
            struct nlist_64 *const symp = (struct nlist_64 *)(symcmd->symoff + addr);
            char *const namp0 = symcmd->stroff + addr;
            char *namp;
            for (j=0, namp = namp0; j < symcmd->nsyms; ++j) {
                namp += 1+ strlen(namp);
                if (symp[j].n_un.n_strx != (namp - namp0)) {
                    fprintf(stderr, "macho-snip: bad .n_strx\n");
                }
            }
            unsigned pad = 7& -(unsigned long)namp;
            memset(namp, 0, pad); namp += pad;  // zero pad to (0 mod 8)
            symcmd->strsize = namp - namp0;
            linkedit->filesize = (namp - addr) - linkedit->fileoff;
            symcmd->nsyms -= 1;  // lop last symbol  FIXME: generalize
        } break;
        case LC_DYSYMTAB: {
            fprintf(stderr, "macho-snip: LD_DYSYMTAB skipped\n");
            continue;
            struct dysymtab_command *dysym = (struct dysymtab_command *)cmd;
            if (0==(dysym->nundefsym -= 1)) { // FIXME: generalize
                dysym->iundefsym = 0;
            }
        } break;

        case LC_BUILD_VERSION:
        case LC_DYLD_INFO:  // also LC_DYLD_INFO_ONLY because low 8 bits
        case LC_LOAD_DYLIB:
        case LC_LOAD_DYLINKER:
        case LC_MAIN:
        case LC_SOURCE_VERSION:
        case LC_UUID:
        {
            for (jargv = 2; jargv < argc; ++jargv) {
                if (argv[jargv] && !strcmp(cmd_names[cmd->cmd & 0xFF].name, argv[jargv])) {
                    argv_done |= 1uL << jargv;
                    fprintf(stderr, "macho-snip: %#x, %s\n",
                        cmd_names[cmd->cmd & 0xFF].val, cmd_names[cmd->cmd & 0xFF].name);
                    // EXPERIMENT:
                    if (cmd->cmd == LC_DYLD_INFO_ONLY) { // the "must process" case
                        struct dyld_info_command *dyldcmd = (struct dyld_info_command *)cmd;
                        dyldcmd->export_off = 0;
                        dyldcmd->export_size = 0;
                        goto next;  // EXPERIMENT
                    }
                    goto snip;
                }
            }
        } break;

        case LC_DATA_IN_CODE: {
        case LC_DYLD_EXPORTS_TRIE:
        case LC_DYLD_CHAINED_FIXUPS:
        case LC_DYLIB_CODE_SIGN_DRS:
        case LC_FUNCTION_STARTS:
        case LC_LINKER_OPTIMIZATION_HINT:
        case LC_SEGMENT_SPLIT_INFO: {
            for (jargv = 2; jargv < argc; ++jargv) {
                if (argv[jargv] && !strcmp(cmd_names[cmd->cmd & 0xFF].name, argv[jargv])) {
                    argv_done |= 1uL << jargv;
                    fprintf(stderr, "macho-snip: %#x, %s\n",
                        cmd_names[cmd->cmd & 0xFF].val, cmd_names[cmd->cmd & 0xFF].name);
                    goto snip_linkedit_data_command;
                }
            }
        } break;
        }
        continue;  // no changes ==> advance
snip_linkedit_data_command: ;
        struct linkedit_data_command *ldc = (struct linkedit_data_command *)cmd;
        end_datasize = ldc->datasize;
        end_dataoff  = ldc->datasize + ldc->dataoff;
        memset(addr + ldc->dataoff, 0, end_datasize);  // the linkedit_data
        if ((linkedit->fileoff + linkedit->filesize) == end_dataoff) {
            linkedit->filesize -= end_datasize;  // trim
        }
snip: ;
        memmove(cmd, cmd_next, headway);
        memset(headway + (char *)cmd, 0, cmdsize);  // space that was vacated
        cmd_next = cmd;  // we moved tail at *cmd_next to *cmd
        mhdr->sizeofcmds -= cmdsize;
        mhdr->ncmds -= 1;
        argv[jargv] = 0;  // snip only once per argv[]
        }  // switch
next: ;
    }  // ncmds
    argv_done |= (1<<1) | (1<<0);  // argv[0,1] do not name linker_commands
    if (~(~0uL << argc) != argv_done) {
        int j;
        for (j=2; j < argc; ++j) {
            if (!((1uL << j) & argv_done)) {
                fprintf(stderr, "macho-snip warning: %s not processed\n", argv[j]);
            }
        }
    }
    if (!(MAP_SHARED & flags)) {
        write(1, addr, st.st_size);
    }
    return 0;  // success
}

/* vim:set ts=4 sw=4 et: */
