/* NetHack 3.7	report.c	$NHDT-Date: 1741406837 2025/03/07 20:07:17 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.16 $ */
/* Copyright (c) Kenneth Lorber, Kensington, Maryland, 2024 */
/* NetHack may be freely redistributed.  See license for details. */

#include "hack.h"

/* NB: CRASHREPORT implies PANICTRACE */

# ifndef NO_SIGNAL
#include <signal.h>
# endif
# ifndef LONG_MAX
#include <limits.h>
# endif
#include "dlb.h"
#include <fcntl.h>

#include <errno.h>
# ifdef PANICTRACE_LIBC
#include <execinfo.h>
# endif

#ifdef CRASHREPORT
# ifdef WIN32
#  define HASH_PRAGMA_START
#  define HASH_PRAGMA_END
# else
#  define HASH_PRAGMA_START           \
    _Pragma("GCC diagnostic push"); \
    _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
#  define HASH_PRAGMA_END _Pragma("GCC diagnostic pop");
# endif
# ifdef MACOS
#include <CommonCrypto/CommonDigest.h>
#  define HASH_CONTEXTPTR(CTXP) \
    unsigned char tmp[CC_MD4_DIGEST_LENGTH]; \
    CC_MD4_CTX CTXP ## _;                    \
    CC_MD4_CTX *CTXP = &CTXP ## _
#  define HASH_INIT(ctxp) !CC_MD4_Init(ctxp)
#  define HASH_UPDATE(ctx, ptr, len) !CC_MD4_Update(ctx, ptr, len)
#  define HASH_FINISH(ctxp) !CC_MD4_Final(tmp, ctxp)
#  define HASH_RESULT_SIZE(ctxp) CC_MD4_DIGEST_LENGTH
#  define HASH_RESULT(ctx, inp) *inp = (unsigned char *) ctx
#  define HASH_CLEANUP(ctxp)
#  define HASH_OFLAGS O_RDONLY
#  define HASH_BINFILE_DECL char *binfile = argv[0];
#  if (NH_DEVEL_STATUS == NH_STATUS_BETA)
#   define HASH_BINFILE() \
    if (!binfile || !*binfile) {                                        \
        /* If this triggers, investigate CFBundleGetMainBundle */       \
        /* or CFBundleCopyExecutableURL. */                             \
        raw_print(                                                      \
          "BETA warning: crashreport_init called without useful info"); \
        goto skip;                                                      \
    }
#  else /* BETA */
#  define HASH_BINFILE() \
    if (!binfile || !*binfile) {                \
        goto skip;                              \
    }
#  endif  /* BETA */
# endif  /* MACOS */

# ifdef __linux__
#include "nhmd4.h"
/* v0 is just to suppress compiler warnings about unreachable code */
#  define HASH_CONTEXTPTR(CTXP) \
    volatile int v0 = 0; \
    unsigned char tmp[NHMD4_DIGEST_LENGTH];     \
    NHMD4_CTX CTXP ## _;                        \
    NHMD4_CTX *CTXP = &CTXP ## _
#  define HASH_INIT(ctxp) (nhmd4_init(ctxp), v0)
#  define HASH_UPDATE(ctx, ptr, len) (nhmd4_update(ctx, ptr, len), v0)
#  define HASH_FINISH(ctxp) (nhmd4_final(ctxp, tmp), v0)
#  define HASH_RESULT_SIZE(ctxp) NHMD4_RESULTLEN
#  define HASH_RESULT(ctx, inp) *inp = tmp
#  define HASH_CLEANUP(ctxp)
#  define HASH_OFLAGS O_RDONLY
#  define HASH_BINFILE_DECL char binfile[PATH_MAX+1];
#  define HASH_BINFILE() \
    int len = readlink("/proc/self/exe", binfile, sizeof binfile - 1);  \
    if (len > 0) {                                                      \
        binfile[len] = '\0';                                            \
    } else {                                                            \
        goto skip;                                                      \
    }
# endif // __linux__

# ifdef WIN32
/* WIN32 takes too much code and is dependent on OS includes we can't
 * pull in here, so we call out to code in sys/windows/windsys.c */
#  define HASH_CONTEXTPTR(CTXP)
#  define HASH_INIT(ctxp) win32_cr_helper('i', ctxp, NULL, 0)
#  define HASH_UPDATE(ctxp, ptr, len) win32_cr_helper('u', ctxp, ptr, len)
#  define HASH_FINISH(ctxp) win32_cr_helper('f', ctxp, NULL, 0)
#  define HASH_CLEANUP(ctxp) win32_cr_helper('c', ctxp, NULL, 0)
#  define HASH_RESULT_SIZE(ctxp) win32_cr_helper('s', ctxp, NULL, 0)
#  define HASH_RESULT(ctxp, inp) win32_cr_helper('r', ctxp, inp, 0)
#  define HASH_OFLAGS _O_RDONLY | _O_BINARY
#  define HASH_BINFILE_DECL char *binfile;
#  define HASH_BINFILE() \
    if (win32_cr_helper('b', NULL, &binfile, 0)) {      \
        goto skip;                                      \
    }
# endif // WIN32

/* Binary ID - Use only as a hint to contact.html for recognizing our own
   binaries.  This is easily spoofed! */
static char bid[40];

/* ARGSUSED */
void
crashreport_init(int argc UNUSED, char *argv[] UNUSED)
{
    static int once = 0;
    if (once++) /* NetHackW.exe calls us twice */
        return;
    HASH_BINFILE_DECL;
    HASH_PRAGMA_START
    HASH_CONTEXTPTR(ctxp);
    if (HASH_INIT(ctxp))
        goto skip;
    HASH_BINFILE(); /* Does "goto skip" on error. */

    int fd = open(binfile, HASH_OFLAGS, 0);
    if (fd == -1) {
# ifdef BETA
        raw_printf("open e=%s", strerror(errno));
# endif
        goto skip;
    }

    int segsize;
    unsigned char segment[4096];

    while (0 < (segsize = read(fd, segment, sizeof segment))) {
        if (HASH_UPDATE(ctxp, segment, segsize))
            goto skip;
    }
    if (segsize < 0) {
        close(fd);
        goto skip;
    }
    if (HASH_FINISH(ctxp))
        goto skip;
    close(fd);

    static const char hexdigits[] = "0123456789abcdef";
    char *p = bid;
    unsigned char *in;
    HASH_RESULT(ctxp, &in);
    uint8 cnt = (uint8) HASH_RESULT_SIZE(ctxp);
    /* Just in case, make sure not to overflow the bid buffer.
       Divide size by 2 because each octet in the hash uses two slots
       in bid[] when formatted as a pair of hexadecimal digits. */
    if (cnt >= (uint8) sizeof bid / 2)
        cnt = (uint8) sizeof bid / 2 - 1;
    while (cnt) {
        /* sprintf(p, "%02x", *in++), p += 2; */
        *p++ = hexdigits[(*in >> 4) & 0x0f];
        *p++ = hexdigits[*in++ & 0x0f];
        --cnt;
    }
    *p = '\0';
    HASH_CLEANUP(ctxp);
    return;

 skip:
    Strcpy(bid, "unknown");
    HASH_CLEANUP(ctxp);
    HASH_PRAGMA_END
}

#undef HASH_CONTEXTPTR
#undef HASH_INIT
#undef HASH_UPDATE
#undef HASH_FINISH
#undef HASH_CLEANUP
#undef HASH_RESULT
#undef HASH_RESULT_SIZE
#undef HASH_PRAGMA_START
#undef HASH_PRAGMA_END
#undef HASH_BINFILE_DECL
#undef HASH_BINFILE

void
crashreport_bidshow(void)
{
# if defined(WIN32) && !defined(WIN32CON)
    if (0 == win32_cr_helper('D', ctxp, bid, 0))
# endif
    {
        raw_print(bid);
# ifdef WIN32notyet
        wait_synch();
# endif
    }
}

/* Build a URL with a query string and try to launch a new browser window
 * to report from panic() or impossible().  Requires libc support for
 * the stacktrace.  Uses memory on the stack to avoid memory allocation
 * (on most platforms) (but libc can still do anything it wants). */

// No theoretial limit for URL length but reality is messy.
// This should work on all modern platforms.
# ifndef MAX_URL
# define MAX_URL 8192
# endif
# ifndef SWR_FRAMES
# define SWR_FRAMES 20
# endif

// mark holds the initial eos; if we can't get the value in
// then we can remove the whole item if desired.  For other
// semantics, caller can handle mark.
#define SWR_ADD(str) \
    utmp = strlen(str);                         \
    mark = uend;                                \
    if (utmp >= urem)                           \
        goto full;                              \
    memcpy(uend, str, utmp);                    \
    uend += utmp; urem -= utmp;                 \
    *uend = '\0';

// NB: on overflow this rolls us back to mark, so if we don't
//     want to roll back to the last SWR_ADD, update mark before
//     calling this macro.
#define SWR_ADD_URIcoded(str) \
    if (swr_add_uricoded(str, &uend, &urem, mark))      \
        goto full;

/* On overflow, truncate to markp (but only if markp != NULL). */
boolean
swr_add_uricoded(
    const char *in,
    char **out,
    int *remaining,
    char *markp)
{
    while (*in) {
        if (isalnum(*in) || strchr("_-.~", *in)) {
            **out = *in;
            (*out)++;
            (*remaining)--;
        } else if (*in == ' ') {
            **out = '+';
            (*out)++;
            (*remaining)--;
        } else {
            if (*remaining <= 3) {
                if (markp)
                    *out = markp, *remaining = 0;
                **out = '\0';
                return TRUE;
            } else {
                char chr[40]; /* [4] should suffice */
                int x;

                Sprintf(chr, "%%%02X", *in);
                x = (int) strlen(chr);
                if (x <= *remaining) {
                    Strcpy(*out, chr);
                    *out += x;
                    *remaining -= x;
                }
            }
        }
        in++;
        if (!*remaining) {
            if (markp)
                *out = markp, *remaining = 0;
            **out = '\0';
            return TRUE;
        }
        **out = '\0';
    }
    return FALSE; /* normal return */
}

static char url[MAX_URL];   // XXX too bad this isn't allocated as needed
static int urem = MAX_URL;  // adjusted for gc.crash_urlmax below
static char *uend = url;
static int utmp;            // used inside macros
static char *mark;          // holds previous terminator (generally)

boolean
submit_web_report(int cos, const char *msg, const char *why)
{
    urem = (gc.crash_urlmax < 0 || gc.crash_urlmax > MAX_URL)
           ? MAX_URL : min(MAX_URL,gc.crash_urlmax);
    char temp[200];
    char temp2[200];
    int countpp = 0; /* pre and post traceback lines */
//  URL loaded for creating reports to the NetHack DevTeam
// CRASHREPORTURL=https://nethack.org/links/cr-37BETA.html
    if (!sysopt.crashreporturl)
        return FALSE;
    SWR_ADD(sysopt.crashreporturl);
    /*
     * Note: all snprintf() calls here changed to sprintf() to avoid
     *       complaints from static analyzer.  All but one were unnecessary
     *       since they were formatting int or unsigned into a large buffer.
     */
        /* cos - operation, v - version */
    Sprintf(temp, "?cos=%d&v=1", cos);
    SWR_ADD(temp);

        /* msg==NULL for #bugreport */
    if (msg) {
        SWR_ADD("&subject=");
        Sprintf(temp, "%.40s report for NetHack %.40s",
                msg, version_string(temp2, sizeof temp2 ));
        SWR_ADD_URIcoded(temp);
    }

    SWR_ADD("&gitver=");
    SWR_ADD_URIcoded(getversionstring(temp2, sizeof temp2));

    if (gc.crash_name) {
        SWR_ADD("&name=");
        SWR_ADD_URIcoded(gc.crash_name);
    }

    if(gc.crash_email) {
        SWR_ADD("&email=");
        SWR_ADD_URIcoded(gc.crash_email);
    }
            // hardware: leave for user
            // software: leave for user
            // comments: leave for user

    SWR_ADD("&details=");
    if (why) {
        SWR_ADD_URIcoded(why);
        SWR_ADD_URIcoded("\n");
        mark = uend;
        countpp++;
    }

    SWR_ADD_URIcoded("bid: ");
    SWR_ADD_URIcoded(bid);
    SWR_ADD_URIcoded("\n");
    mark = uend;
    countpp++;

    int count = 0;
    if (cos == 1) {
# ifdef WIN32
        count = win32_cr_gettrace(SWR_FRAMES, uend, MAX_URL - (uend - url));
        uend = eos(url);
# else
        void *bt[SWR_FRAMES];
        int x;
        char **info;

        count = backtrace(bt, SIZE(bt));
        info = backtrace_symbols(bt, count);
        for (x = 0; x < count; x++) {
            copynchars(temp, info[x], (int) sizeof temp - 1 - 1); /* \n\0 */
            /* try to remove up to 16 blank spaces by removing 8 twice */
            (void) strsubst(temp, "        ", "");
            (void) strsubst(temp, "        ", "");
            (void) strncat(temp, "\n", sizeof temp - 1);
#  if 0 // __linux__
// not needed for MacOS
// XXX is it actually needed for linux?  TBD
            Sprintf(temp2, "[%02lu]\n", (unsigned long) x);
            uend--;    // remove the \n we added above
            SWR_ADD_URIcoded(temp2);
#  endif // linux
            SWR_ADD_URIcoded(temp);
            mark = uend;
        }
# endif  // !WIN32
    }

# ifdef DUMPLOG_CORE
        // config.h turns this on, but make it easy to turn off if needed
    if (cos == 1) {
        int k;
        SWR_ADD_URIcoded("Latest messages:\n");
        mark=uend;
        countpp++;
        for (k = 0; k < 5; k++) {
            const char *line = get_saved_pline(k);
            if (!line)
                break;
            SWR_ADD_URIcoded(line);
            SWR_ADD_URIcoded("\n");
            countpp++;
            mark = uend;
        }
    }
# endif /* DUMPLOG_CORE */

                // detailrows: Guess since we can't know the
                //     width of the window.
        SWR_ADD("&detailrows=");
        Sprintf(temp, "%d", min(count + countpp, 30));
        SWR_ADD_URIcoded(temp);

 full:
        ;
//printf("URL=%ld '%s'\n",strlen(url),url);
# ifdef WIN32
        int *rv = win32_cr_shellexecute(url);
// XXX TESTING
printf("ShellExecute returned: %p\n",rv);   // >32 is ok
# else  /* !WIN32 */
        const char *xargv[] = {
            CRASHREPORT,
            url,
            NULL
        };
        int pid = fork();
        extern char **environ;
        if (pid == 0) {
            char err[400];
#  ifdef CRASHREPORT_EXEC_NOSTDERR
            int devnull;
                /* Keep the output clean - firefox spews useless errors on
                 * my system. */
            (void) close(2);
            devnull =  open("/dev/null", O_WRONLY);
#  endif

            (void) execve(CRASHREPORT, (char * const *) xargv, environ);
            Sprintf(err, "Can't start " CRASHREPORT ": %.*s",
                    (int) (sizeof err
                           - sizeof "Can't start " CRASHREPORT ": "),
                    strerror(errno));
            raw_print(err);
#  ifdef CRASHREPORT_EXEC_NOSTDERR
            (void) close(devnull);
#  endif
            exit(1);
        } else {
            int status;
            errno = 0;
            (void) waitpid(pid, &status, 0);
            if (status) {         /* XXX check could be more precise */
#  ifdef BETA
                    /* Not useful at the moment. XXX */
                char err[100];

                Sprintf(err, "pid=%d e=%d status=%0x", wpid, errno, status);
                raw_print(err);
#  endif
                return FALSE;
            }
        }
        /* free(info);   -- Don't risk it. */
# endif  /* !WIN32 */
        return TRUE;
}

int
dobugreport(void)
{
    if (!submit_web_report(2, NULL, "#bugreport command")) {
        pline("Unable to send bug report.  Please visit %s instead.",
              (sysopt.crashreporturl && *sysopt.crashreporturl)
              ? sysopt.crashreporturl
              : DEVTEAM_URL
        );
    }
    return ECMD_OK;
}

#undef SWR_ADD
#undef SWR_ADD_URIcoded
#undef SWR_FRAMES
#undef SWR_HDR
#undef SWR_LINES

#endif /* CRASHREPORT */

#ifdef PANICTRACE

/*ARGSUSED*/
boolean
NH_panictrace_libc(void)
{
# if 0       /* XXX how did this get left here? */
    if (submit_web_report("Panic", why))
        return TRUE;
# endif

# ifdef PANICTRACE_LIBC
    void *bt[20];
    int count, x;
    char **info, buf[BUFSZ];

    raw_print("  Generating more information you may report:\n");
    count = backtrace(bt, SIZE(bt));
    info = backtrace_symbols(bt, count);
    for (x = 0; x < count; x++) {
        copynchars(buf, info[x], (int) sizeof buf - 1);
        /* try to remove up to 16 blank spaces by removing 8 twice */
        (void) strsubst(buf, "        ", "");
        (void) strsubst(buf, "        ", "");
        raw_printf("[%02lu] %s", (unsigned long) x, buf);
    }
    /* free(info);   -- Don't risk it. */
    return TRUE;
# else
    return FALSE;
# endif /* !PANICTRACE_LIBC */
}

/*
 *   fooPATH  file system path for foo
 *   fooVAR   (possibly const) variable containing fooPATH
 */
# ifdef PANICTRACE_GDB
#  ifdef SYSCF
#  define GDBVAR sysopt.gdbpath
#  define GREPVAR sysopt.greppath
#  else /* SYSCF */
#  define GDBVAR GDBPATH
#  define GREPVAR GREPPATH
#  endif /* SYSCF */
# endif /* PANICTRACE_GDB */

boolean
NH_panictrace_gdb(void)
{
# ifdef PANICTRACE_GDB
    /* A (more) generic method to get a stack trace - invoke
     * gdb on ourself. */
    const char *gdbpath = GDBVAR;
    const char *greppath = GREPVAR;
    char buf[BUFSZ];
    FILE *gdb;

    if (gdbpath == NULL || gdbpath[0] == 0)
        return FALSE;
    if (greppath == NULL || greppath[0] == 0)
        return FALSE;

    Snprintf(buf, sizeof buf, "%s -n -q %s %d 2>&1 | %s '^#'",
             gdbpath, ARGV0, getpid(), greppath);
    gdb = popen(buf, "w");
    if (gdb) {
        raw_print("  Generating more information you may report:\n");
        (void) fprintf(gdb, "bt\nquit\ny");
        (void) fflush(gdb);
        sleep(4); /* ugly */
        (void) pclose(gdb);
        return TRUE;
    } else {
        return FALSE;
    }
# else
    return FALSE;
# endif /* !PANICTRACE_GDB */
}

#ifdef DUMPLOG_CORE
#define USED_if_dumplog
#else
#define USED_if_dumplog UNUSED
#endif

/* lineno==0 gives the most recent message (e.g.
   "Do you want to call panic..." if called from #panic) */
const char *
get_saved_pline(int lineno USED_if_dumplog)
{
#ifdef DUMPLOG_CORE
    int p;
    int limit = DUMPLOG_MSG_COUNT;

    if (lineno >= DUMPLOG_MSG_COUNT)
        return NULL;
    p = (gs.saved_pline_index - 1) % DUMPLOG_MSG_COUNT;

    while (limit--) {
        if (gs.saved_plines[p]) { /* valid line */
            if (lineno--) {
                p = (p - 1 + DUMPLOG_MSG_COUNT) % DUMPLOG_MSG_COUNT;
            } else {
                return gs.saved_plines[p];
            }
        }
    }
#endif /* DUMPLOG_CORE */
    return NULL;
}

#undef USED_if_dumplog

# ifndef NO_SIGNAL
/* called as signal() handler, so sent at least one arg */
/*ARGUSED*/
void
panictrace_handler(int sig_unused UNUSED)
{
#define SIG_MSG "\nSignal received.\n"
    int f2;

#  ifdef CURSES_GRAPHICS
    if (iflags.window_inited && WINDOWPORT(curses)) {
        extern void curses_uncurse_terminal(void); /* wincurs.h */

        /* it is risky calling this during a program-terminating signal,
           but without it the subsequent backtrace is useless because
           that ends up being scrawled all over the screen; call is
           here rather than in NH_abort() because panic() calls both
           exit_nhwindows(), which makes this same call under curses,
           then NH_abort() and we don't want to call this twice */
        curses_uncurse_terminal();
    }
#  endif

    f2 = (int) write(2, SIG_MSG, sizeof SIG_MSG - 1);
    nhUse(f2);  /* what could we do if write to fd#2 (stderr) fails  */
    NH_abort(NULL); /* ... and we're already in the process of quitting? */
}

void
panictrace_setsignals(boolean set)
{
#define SETSIGNAL(sig) \
    (void) signal(sig, set ? (SIG_RET_TYPE) panictrace_handler : SIG_DFL);
# ifdef SIGILL
    SETSIGNAL(SIGILL);
# endif
# ifdef SIGTRAP
    SETSIGNAL(SIGTRAP);
# endif
# ifdef SIGIOT
    SETSIGNAL(SIGIOT);
# endif
# ifdef SIGBUS
    SETSIGNAL(SIGBUS);
# endif
# ifdef SIGFPE
    SETSIGNAL(SIGFPE);
# endif
# ifdef SIGSEGV
    SETSIGNAL(SIGSEGV);
# endif
# ifdef SIGSTKFLT
    SETSIGNAL(SIGSTKFLT);
# endif
# ifdef SIGSYS
    SETSIGNAL(SIGSYS);
# endif
# ifdef SIGEMT
    SETSIGNAL(SIGEMT);
# endif
#undef SETSIGNAL
}
# endif /* NO_SIGNAL */
#endif /* PANICTRACE */

/*
 * FIXME: this should have a lot of '#undef's for onefile support.
 */

/*report.c*/
