/*
 * lib_calc - calc link library initialization and shutdown routines
 *
 * Copyright (C) 1999-2007,2018,2021-2023,2026  Landon Curt Noll
 *
 * Calc is open software; you can redistribute it and/or modify it under
 * the terms of the version 2.1 of the GNU Lesser General Public License
 * as published by the Free Software Foundation.
 *
 * Calc 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 Lesser General
 * Public License for more details.
 *
 * A copy of version 2.1 of the GNU Lesser General Public License is
 * distributed with calc under the filename COPYING-LGPL.  You should have
 * received a copy with calc; if not, write to Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Under source code control:   1996/06/17 18:06:19
 * File existed as early as:    1996
 *
 * chongo <was here> /\oo/\     http://www.isthe.com/chongo/
 * Share and enjoy!  :-)        http://www.isthe.com/chongo/tech/comp/calc/
 */

/*
 * important <system> header includes
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <setjmp.h>
#include <signal.h>
#include <sys/types.h>
#include <stdint.h>
#include <stdbool.h>
#include <termios.h>
typedef struct termios ttystruct;

/*
 * conditional <system> head includes
 */
#if !defined(_WIN32) && !defined(_WIN64)
#  include <pwd.h>
#endif

/*
 * calc local src includes
 */
#include "value.h"
#include "calc.h"
#include "lib_calc.h"
#include "conf.h"
#include "token.h"
#include "symbol.h"
#include "label.h"
#include "func.h"
#include "strl.h"
#include "have_unused.h"
#if defined(CUSTOM)
#  include "custom.h"
#endif
#include "attribute.h"
#include "errtbl.h"

#include "banned.h" /* include after all other includes */

/*
 * Common definitions
 */
int use_old_std = false;         /* true => use old classic configuration */
int abortlevel;                  /* current level of aborts */
bool inputwait;                  /* true if in a terminal input wait */
char *program = "calc";          /* our name */
char *base_name = "calc";        /* basename of our name */
char cmdbuf[MAXCMD + 1 + 1 + 1]; /* command line expression + "\n\0" + guard */
run run_state = RUN_ZERO;        /* calc startup run state */
char *script_name = NULL;        /* program name or -f filename arg or NULL */

/*
 * global permissions
 */
int allow_read = true;  /* false => may not open any files for reading */
int allow_write = true; /* false => may not open any files for writing */
int allow_exec = true;  /* false => may not execute any commands */

/*
 * global flags
 */
int p_flag = false; /* true => pipe mode */
int q_flag = false; /* true => don't execute rc files */
int u_flag = false; /* true => unbuffer stdin and stdout */
int d_flag = false; /* true => disable heading, resource_debug == 0 */
int c_flag = false; /* true => continue on error if permitted */
int i_flag = false; /* true => go interactive if permitted */
int s_flag = false; /* true => keep args as strings for argv() */

/*
 * global values
 */
char *calcpath = NULL;     /* $CALCPATH or default */
char *calcrc = NULL;       /* $CALCRC or default */
char *calcbindings = NULL; /* $CALCBINDINGS or default */
char *home = NULL;         /* $HOME or default */
char *pager = NULL;        /* $PAGER or default */
char *shell = NULL;        /* $SHELL or default */
char *calc_history = NULL; /* $CALCHISTFILE or ~/.calc_history */
char *calc_helpdir = NULL; /* $CALCHELP or /usr/local/share/calc/help */
/* $CALCCUSTOMHELP or /usr/local/share/calc/custhelp */
#if defined(CUSTOM)
char *calc_customhelpdir = NULL;
#endif
int stdin_tty = false;    /* true if stdin is a tty */
int havecommands = false; /* true if have one or more cmd args */
long stoponerror = 0;     /* >0 => stop, <0 => continue, ==0 => use -c */
bool abort_now = false;   /* true => go interactive now, if permitted */

/* non-zero => calc_scanerr_jmpbuf ready */
int calc_use_scanerr_jmpbuf = 0;
/* for scan and parse errors */
jmp_buf calc_scanerr_jmpbuf;

/* non-zero => use calc_use_matherr_jmpbuf */
int calc_use_matherr_jmpbuf = 0;
/* math_error() control jump point when calc_use_matherr_jmpbuf != 0 */
jmp_buf calc_matherr_jmpbuf;

/* last calc error message, also parse/scan errors use this buffer */
char calc_err_msg[MAXERROR + 1];
/* 0 ==> do not print parse/scan errors */
int calc_print_scanerr_msg = 1;

/* last parse/scan warning message */
char calc_warn_msg[MAXERROR + 1];
/* 0 ==> do not print parse/scan warnings */
int calc_print_scanwarn_msg = 1;
/* number of parse/scan warnings found */
unsigned long calc_warn_cnt = 0;

int argc_value = 0;       /* count of argv[] strings for argv() builtin */
char **argv_value = NULL; /* argv[] strings for argv() builtin */

int no_env = false;   /* true (-e) => ignore env vars on startup */
long errmax = ERRMAX; /* if >= 0,  maximum value for errcount */

NUMBER *epsilon_default; /* default allowed error for float calcs */

char *calc_debug = NULL;     /* !=NULL => value of config("calc_debug") */
char *resource_debug = NULL; /* !=NULL => config("resource_debug") value */
char *user_debug = NULL;     /* !=NULL => value of config("user_debug") */

/*
 * initialization functions
 */
extern void math_setfp(FILE *fp);
extern void file_init(void);
extern void zio_init(void);
extern void initialize(void);
extern void reinitialize(void);

/*
 * static declarations
 */
static bool init_done = false;     /* true => we already initialized */
static int *fd_setup = NULL;       /* fd's setup for interaction or -1 */
static int fd_setup_len = 0;       /* number of fd's in fd_setup */
static ttystruct *fd_orig = NULL;  /* fd original state */
static ttystruct *fd_cur = NULL;   /* fd current state */
static void initenv(void);         /* setup calc environment */
static int find_tty_state(int fd); /* find slot for saved tty state */
static bool initialized = false;   /* true => initialize() has been run */
static char *xdg_base(char *env, int mode);

/*
 * libcalc_call_me_first - users of libcalc.a must call this function first!
 *
 * Anything that uses libcalc.a MUST call this function first before doing
 * any other libcalc.a processing.
 */
void
libcalc_call_me_first(void)
{
    char *p;

    /*
     * do nothing if we are initialized already
     */
    if (init_done) {
        return;
    }

    /*
     * Disable SIGPIPE so that the pipe to the help file pager will
     * not stop calc.
     */
#if !defined(_WIN32) && !defined(_WIN64)
    (void)signal(SIGPIPE, SIG_IGN);
#endif

    /*
     * determine the basename
     */
    if (program != NULL) {
        p = strrchr(program, '/');
#if defined(_WIN32) || defined(_WIN64)
        if (p == NULL) {
            p = strrchr(program, '\\');
        }
#endif
        if (p == NULL) {
            base_name = program;
        } else {
            base_name = p + 1;
        }
    }

    /*
     * initialize old and new configuration values
     */
    newstd.epsilon = &_qonesqbase_; /* magic to fake early str2q() */
    if (program == NULL) {
        fprintf(stderr, "libcalc_call_me_first: FATAL: "
                        "program is NULL\n");
        exit(80);
    }
    newstd.program = strdup(program);
    if (newstd.program == NULL) {
        fprintf(stderr, "libcalc_call_me_first: FATAL: "
                        "cannot strdup program string\n");
        exit(81);
    }
    if (base_name == NULL) {
        fprintf(stderr, "libcalc_call_me_first: FATAL: "
                        "base_name is NULL\n");
        exit(82);
    }
    newstd.base_name = strdup(base_name);
    if (newstd.base_name == NULL) {
        fprintf(stderr, "libcalc_call_me_first: FATAL: "
                        "cannot strdup base_name string\n");
        exit(83);
    }
    p = version();
    if (p == NULL) {
        fprintf(stderr, "libcalc_call_me_first: FATAL: "
                        "version() returned NULL\n");
        exit(84);
    }
    newstd.version = strdup(p);
    if (newstd.version == NULL) {
        fprintf(stderr, "libcalc_call_me_first: FATAL: "
                        "cannot strdup return from version()\n");
        exit(85);
    }
    conf = config_copy(&newstd); /* more magic to fake early str2q() */
    conf->tab_ok = false;
    newstd.epsilon = str2q(EPSILON_DEFAULT);
    oldstd.epsilon = str2q(EPSILON_DEFAULT);

    /*
     * make newstd our default config, unless -O
     */
    config_free(conf);
    if (use_old_std) {
        conf = config_copy(&oldstd);
    } else {
        conf = config_copy(&newstd);
    }

    /*
     * -d turns off resource_debug and tilde
     */
    if (d_flag) {
        conf->resource_debug = 0;
        conf->tilde_ok = 0;
        conf->tilde_space = 0;
    }

    /*
     * -p turns off tab
     */
    if (p_flag) {
        conf->tab_ok = 0;
    }

    /*
     * -D flags can change calc_debug, resource_debug of user_debug
     */
    if (calc_debug) {
        conf->calc_debug = strtol(calc_debug, NULL, 0);
    }
    if (resource_debug) {
        conf->resource_debug = strtol(resource_debug, NULL, 0);
    }
    if (user_debug) {
        conf->user_debug = strtol(user_debug, NULL, 0);
    }

    /*
     * initialize
     */
    initialize();

    /*
     * verify error_table[] array and setup private_error_alias[] array
     */
    verify_error_table();

    /*
     * ready to rock & roll ..
     */
    if (conf->calc_debug & CALCDBG_RUNSTATE) {
        printf("libcalc_call_me_first: run_state from %s to %s\n", run_state_name(run_state), run_state_name(RUN_BEGIN));
    }
    run_state = RUN_BEGIN;
    init_done = true;
    return;
}

/*
 * initialize - perform the required calc initializations
 */
void
initialize(void)
{
    /*
     * firewall
     */
    if (initialized) {
        return;
    }

    /*
     * ZVALUE io initialization
     */
    zio_init();

    /*
     * process the environment
     */
    initenv();

    /*
     * initialize I/O
     */
    file_init();

    /*
     * initialize file I/O
     */
    resetinput();

    /*
     * initialize calc internal data structures
     */
    inittokens();
    initglobals();
    initfunctions();
    initstack();

    /*
     * initialize calc math
     */
    math_cleardiversions();
    math_setfp(stdout);
    math_setmode(MODE_INITIAL);
    math_setdigits(DISPLAY_DEFAULT);
    conf->maxprint = MAXPRINT_DEFAULT;

#if defined(CUSTOM)

    /*
     * initialize custom registers
     */
    if (allow_custom) {
        init_custreg();
    }

#endif

    /*
     * on exit, call libcalc_call_me_last()
     */
    atexit(libcalc_call_me_last);

    /*
     * note that we are done
     */
    initialized = true;
}

/*
 * reinitialize - reinitialize after a longjmp
 */
void
reinitialize(void)
{
    /*
     * process commands (from stdin, not the command line)
     */
    abortlevel = 0;
    _math_abort_ = false;
    inputwait = false;
    math_cleardiversions();
    math_setfp(stdout);
    resetscopes();
    resetinput();
    inittokens();
    (void)openterminal();
}

/*
 * cvmalloc_error - for users of the SGI Workshop Debugger
 *
 * usage:
 *      message - error message passed along via libmalloc_cv.a
 */
void
cvmalloc_error(char *message)
{
    /* firewall */
    if (message == NULL) {
        fprintf(stderr, "cvmalloc_error message is NULL\n");
        return;
    }

    /* print message and return */
    fprintf(stderr, "cvmalloc_error: %s\n", message);
    return;
}

/*
 * initenv - obtain various environment variable values.
 *
 * If these environment variables do not exist, or if they are an empty string,
 * use XDG defaults when the relevant XDG directory is usable, otherwise use
 * the compiled default values as found in calc.h.  For calc_history, favour
 * preserving an existing ~/.calc_history over using the XDG state directory:
 *
 *      $CALCPATH               set calcpath
 *      $CALCRC                 set calcrc
 *      $CALCBINDINGS           set calcbindings
 *      $HOME                   set home
 *      $PAGER                  set pager
 *      $SHELL                  set shell
 *
 *      $CALCHISTFILE           set calc_history
 *                              NOTE: calc_history may be set to NULL by this function.
 *
 *      $CALCHELP               set calc_helpdir
 *
 *      $CALCCUSTOMHELP         set calc_customhelpdir
 *                              NOTE: Only set if CUSTOM is defined.
 *
 * If $HOME does not exist, or is empty, use the home directory information
 * the from the password file if possible, otherwise use "." (dot).
 *
 * In all cases EXCEPT calc_history, the obtained value will NOT be NULL.
 * In the case of calc_history == NULL, the calc history file will be
 * set to "~/.calc_history" when hist_init() is called later on.
 *
 * In all cases, the obtained value is a strdup-ed value so that the
 * final call to libcalc_call_me_last() can free the non-NULL string.
 */
static void
initenv(void)
{
#if !defined(_WIN32) && !defined(_WIN64)
    struct passwd *ent; /* our password entry */
#endif
    char *c;
    char *legacy_history;
    char *xdg_config;
    char *xdg_data;
    size_t len;

    /* determine the $CALCPATH value */
    c = (no_env ? NULL : getenv(CALCPATH));
    if (c != NULL && *c != '\0') {
        calcpath = strdup(c);
    } else if ((xdg_data = xdg_base("XDG_DATA_HOME", X_OK)) != NULL && (xdg_config = xdg_base("XDG_CONFIG_HOME", X_OK)) != NULL) {
        len = strlen(xdg_data) + strlen(xdg_config) + sizeof(".:./cal:/calc:/calc:") + strlen(DEFAULTCALCPATH_SYS);
        calcpath = malloc(len);
        if (calcpath == NULL) {
            math_error("Unable to allocate string for calcpath");
            not_reached();
        }
        snprintf(calcpath, len, ".:./cal:%s/calc:%s/calc:%s", xdg_data, xdg_config, DEFAULTCALCPATH_SYS);
    } else {
        calcpath = strdup(DEFAULTCALCPATH);
    }
    if (calcpath == NULL) {
        math_error("Unable to allocate string for calcpath");
        not_reached();
    }

    /* determine the $CALCRC value */
    c = (no_env ? NULL : getenv(CALCRC));
    if (c != NULL && *c != '\0') {
        calcrc = strdup(c);
    } else if ((xdg_config = xdg_base("XDG_CONFIG_HOME", X_OK)) != NULL) {
        len = strlen(xdg_config) + sizeof("./.calcinit:/calc/calcrc:") + strlen(DEFAULTCALCRC_SYS);
        calcrc = malloc(len);
        if (calcrc == NULL) {
            math_error("Unable to allocate string for calcrc");
            not_reached();
        }
        snprintf(calcrc, len, "./.calcinit:%s/calc/calcrc:%s", xdg_config, DEFAULTCALCRC_SYS);
    } else {
        calcrc = strdup(DEFAULTCALCRC);
    }
    if (calcrc == NULL) {
        math_error("Unable to allocate string for calcrc");
        not_reached();
    }
    if (strlen(calcrc) > MAX_CALCRC) {
        math_error("The $CALCRC variable is longer than %d chars", MAX_CALCRC);
        not_reached();
    }

    /* determine the $CALCBINDINGS value */
    c = (no_env ? NULL : getenv(CALCBINDINGS));
    calcbindings = (c ? strdup(c) : NULL);
    if (calcbindings == NULL || *calcbindings == '\0') {
        calcbindings = strdup(DEFAULTCALCBINDINGS);
    }
    if (calcbindings == NULL) {
        math_error("Unable to allocate string for calcbindings");
        not_reached();
    }

    /* determine the $HOME value */
    c = (no_env ? NULL : getenv(HOME));
    home = (c ? strdup(c) : NULL);
#if defined(_WIN32) || defined(_WIN64)

    if (home == NULL || home[0] == '\0') {
        /* free home if it was previously allocated, but empty */
        if (home != NULL) {
            free(home);
            home = NULL;
        }
        /* just assume . is home if all else fails */
        home = strdup(".");
    }

#else

    if (home == NULL || home[0] == '\0') {
        /* free home if it was previously allocated, but empty */
        if (home != NULL) {
            free(home);
            home = NULL;
        }
        /* try using the home directory of current effective UID */
        ent = (struct passwd *)getpwuid(geteuid());
        if (ent == NULL || ent->pw_dir == NULL || ent->pw_dir[0] == '\0') {
            /* just assume . is home if all else fails */
            home = strdup(".");
        } else {
            /* use home directory of current effective UID */
            home = strdup(ent->pw_dir);
        }
    }

#endif
    if (home == NULL) {
        math_error("Unable to allocate string for home");
        not_reached();
    }

    /* determine the $PAGER value */
    c = (no_env ? NULL : getenv(PAGER));
    pager = (c ? strdup(c) : NULL);
    if (pager == NULL || *pager == '\0') {
        pager = strdup(DEFAULTCALCPAGER);
    }
    if (pager == NULL) {
        math_error("Unable to allocate string for pager");
        not_reached();
    }

    /* determine the $SHELL value */
    c = (no_env ? NULL : getenv(SHELL));
    shell = (c ? strdup(c) : NULL);
    if (shell == NULL || *shell == '\0') {
        shell = strdup(DEFAULTSHELL);
    }
    if (shell == NULL) {
        math_error("Unable to allocate string for shell");
        not_reached();
    }

    /* determine the $CALCHISTFILE value */
    c = (no_env ? NULL : getenv(CALCHISTFILE));
    if (c != NULL && *c != '\0') {
        calc_history = strdup(c);
    } else if (home != NULL) {
        len = strlen(home) + sizeof("/.calc_history");
        legacy_history = malloc(len);
        if (legacy_history == NULL) {
            math_error("Unable to allocate string for legacy_history");
            not_reached();
        }
        snprintf(legacy_history, len, "%s/.calc_history", home);
        if (access(legacy_history, F_OK) == 0) {
            calc_history = legacy_history;
        } else {
            free(legacy_history);
        }
    }
    if (calc_history == NULL && (c = xdg_base("XDG_STATE_HOME", X_OK | W_OK)) != NULL) {
        len = strlen(c) + sizeof("/calc/history");
        calc_history = malloc(len);
        if (calc_history == NULL) {
            math_error("Unable to allocate string for calc_history");
            not_reached();
        }
        snprintf(calc_history, len, "%s/calc/history", c);
    }
    /* NOTE: calc_history may be set to NULL in which case hist_init() may set it later on */

    /* determine the $CALCHELP value */
    c = (no_env ? NULL : getenv(CALCHELP));
    calc_helpdir = (c ? strdup(c) : NULL);
    if (calc_helpdir == NULL || *calc_helpdir == '\0') {
        /* will use /usr/local/share/calc/help */
        calc_helpdir = strdup(HELPDIR);
    }
    if (calc_helpdir == NULL) {
        math_error("Unable to allocate string for calc_helpdir");
        not_reached();
    }

#if defined(CUSTOM)

    /* determine the $CALCCUSTOMHELP value */
    c = (no_env ? NULL : getenv(CALCCUSTOMHELP));
    calc_customhelpdir = (c ? strdup(c) : NULL);
    if (calc_customhelpdir == NULL || *calc_customhelpdir == '\0') {
        /* will use /usr/local/share/calc/custhelp */
        calc_customhelpdir = strdup(CUSTOMHELPDIR);
    }
    if (calc_customhelpdir == NULL) {
        math_error("Unable to allocate string for calc_customhelpdir");
        not_reached();
    }

#endif
}

/*
 * libcalc_call_me_last - users of libcalc.a can call this function when done
 *
 * Anything that uses libcalc.a can call this function after they are
 * completely finished with libcalc.a processing.  The only effect of
 * this function is to free storage that might otherwise go unused.
 *
 * This function also frees any non-NULL values set by initenv().
 *
 * NOTE: If, for any reason, you need to do more libcalc.a processing,
 *       then you will need to call libcalc_call_me_first() again.
 */
void
libcalc_call_me_last(void)
{
    int i;

    /*
     * firewall
     */
    if (init_done == false) {
        return;
    }

    /*
     * free the configuration
     */
    config_free(conf);

    /*
     * free Blum generator state
     */
    random_libcalc_cleanup();

    /*
     * free script_name
     */
    if (script_name != NULL) {
        free(script_name);
        script_name = NULL;
    }

    /*
     * restore all changed descriptor states
     */
    for (i = 0; i < fd_setup_len; ++i) {
        if (fd_setup[i] >= 0) {
            if (conf->calc_debug & CALCDBG_TTY) {
                printf("libcalc_call_me_last: fd %d "
                       "not in original state, "
                       "restoring it",
                       fd_setup[i]);
            }
            orig_tty(fd_setup[i]);
        }
    }

    /*
     * free all globals
     */
    freeglobals();

    /*
     * free non-NULL strdup values set by initenv()
     */
    if (calcpath != NULL) {
        free(calcpath);
        calcpath = NULL;
    }
    if (calcrc != NULL) {
        free(calcrc);
        calcrc = NULL;
    }
    if (calcbindings != NULL) {
        free(calcbindings);
        calcbindings = NULL;
    }
    if (home != NULL) {
        free(home);
        home = NULL;
    }
    if (pager != NULL) {
        free(pager);
        pager = NULL;
    }
    if (shell != NULL) {
        free(shell);
        shell = NULL;
    }
    /*
     * IMPORT NOTE: Do NOT free calc_history here !!!
     *
     * If you do, this will cause the history you have made to be lost!
     *
     * FYI In hist.c:
     *
     * The hist_finish() function, which is invoked due to a call to
     * atexit(hist_finish) in hist_init() will take care of freeing
     * the calc_history storage, if it was allocated in initenv().
     */
    if (calc_helpdir != NULL) {
        free(calc_helpdir);
        calc_helpdir = NULL;
    }
#if defined(CUSTOM)

    if (calc_customhelpdir != NULL) {
        free(calc_customhelpdir);
        calc_customhelpdir = NULL;
    }

#endif

    /*
     * all done
     */
    init_done = false;
    return;
}

/*
 * run_state_name - return a constant string given a run_state
 */
char *
run_state_name(run state)
{
    switch (state) {
    case RUN_ZERO:
        return "ZERO";
    case RUN_BEGIN:
        return "BEGIN";
    case RUN_RCFILES:
        return "RCFILES";
    case RUN_PRE_CMD_ARGS:
        return "PRE_CMD_ARGS";
    case RUN_CMD_ARGS:
        return "CMD_ARGS";
    case RUN_PRE_TOP_LEVEL:
        return "PRE_TOP_LEVEL";
    case RUN_TOP_LEVEL:
        return "TOP_LEVEL";
    case RUN_EXIT:
        return "EXIT";
    case RUN_EXIT_WITH_ERROR:
        return "EXIT_WITH_ERROR";
    }
    return "RUN_invalid";
}

/*
 * calc_strdup - calc interface to provide or simulate strdup()
 */
char *
calc_strdup(const char *s1)
{
#if defined(HAVE_STRDUP)

    return strdup(s1);

#else

    char *ret;     /* return string */
    size_t s1_len; /* length of string to duplicate */

    /*
     * firewall
     */
    if (s1 == NULL) {
        return NULL;
    }

    /*
     * allocate duplicate storage
     */
    s1_len = strlen(s1);
    ret = (char *)calloc(s1_len + 1, 1);

    /*
     * if we have storage, duplicate the string
     */
    if (ret != NULL) {
        strlcpy(ret, s1, s1_len + 1);
    }

    /*
     * return the new string, or NULL if malloc failed
     */
    return ret;

#endif
}

/*
 * find_tty_state - establish a new tty state
 *
 * Given:
 *      fd      file descriptor to establish a new tty state for
 *
 * Returns:
 *      indx    The index into fd_setup[], fd_orig[] and fd_cur[] to use or -1
 */
static int
find_tty_state(int fd)
{
    int *new_fd_setup;      /* new fd_setup array */
    ttystruct *new_fd_orig; /* new fd_orig array */
    ttystruct *new_fd_cur;  /* new fd_cur array */
    int i;

    /*
     * firewall: must be > 0
     */
    if (fd < 0) {
        /* bad descriptor */
        return -1;
    }

    /*
     * case: need to initially malloc some state
     */
    if (fd_setup_len <= 0 || fd_setup == NULL || fd_orig == NULL) {

        /* setup for a single descriptor */
        fd_setup = (int *)calloc(1, sizeof(fd_setup[0]));
        if (fd_setup == NULL) {
            return -1;
        }
        fd_setup[0] = -1;
        fd_orig = (ttystruct *)calloc(1, sizeof(fd_orig[0]));
        if (fd_orig == NULL) {
            return -1;
        }
        fd_cur = (ttystruct *)calloc(1, sizeof(fd_cur[0]));
        if (fd_cur == NULL) {
            return -1;
        }
        fd_setup_len = 1;
    }

    /*
     * look for an existing tty state for the descriptor
     */
    for (i = 0; i < fd_setup_len; ++i) {

        /* case: found existing tty state, return index */
        if (fd_setup[i] == fd) {
            return i;
        }
    }

    /*
     * no tty state exists for the descriptor, look for empty slot
     */
    for (i = 0; i < fd_setup_len; ++i) {

        /* case: found an empty slot, so return it */
        if (fd_setup[i] < 0) {
            return i;
        }
    }

    /*
     * no empty slots exist, realloc another slot
     */
    /* expand fd_orig as an original pre-modified copy of fd_setup */
    new_fd_orig = (ttystruct *)realloc(fd_orig, sizeof(fd_orig[0]) * (fd_setup_len + 1));
    if (new_fd_orig == NULL) {
        return -1;
    }
    fd_orig = new_fd_orig;
    memcpy(fd_orig, fd_setup, sizeof(fd_orig[0]) * (fd_setup_len + 1));

    /* expand fd_setup */
    new_fd_setup = (int *)realloc(fd_setup, sizeof(fd_setup[0]) * (fd_setup_len + 1));
    if (new_fd_setup == NULL) {
        return -1;
    }
    fd_setup = new_fd_setup;
    new_fd_setup[fd_setup_len] = -1;

    /* expand fd_cur */
    new_fd_cur = (ttystruct *)realloc(fd_cur, sizeof(fd_cur[0]) * (fd_setup_len + 1));
    if (new_fd_cur == NULL) {
        return -1;
    }
    fd_cur = new_fd_cur;

    /* expand fd setup length */
    ++fd_setup_len;

    /* return the new slot */
    return fd_setup_len - 1;
}

/*
 * calc_tty - setup a file descriptor for calc's interactive use
 *
 * Calc wants, in effect, cbreak on and echo off.
 *
 * Given:
 *      fd      the descriptor for calc's interactive use
 *
 * Returns:
 *      true    state change was successful
 *      false   unable to change state of descriptor for interactive use
 */
bool
calc_tty(int fd)
{
    int slot; /* the saved descriptor slot or -1 */

    /*
     * grab the saved slot for this descriptor
     */
    slot = find_tty_state(fd);
    if (slot < 0) {
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("calc_tty: Cannot get saved descriptor slot\n");
        }
        return false;
    }

    /*
     * use termios tty state method
     */
    if (fd_setup[slot] < 0 && tcgetattr(fd, fd_orig + slot) < 0) {
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("calc_tty: Cannot tcgetattr fd %d\n", fd);
        }
        return false;
    }
    /* setup for new state */
    fd_cur[slot] = fd_orig[slot];
    fd_cur[slot].c_lflag &= ~(ECHO | ECHOE | ECHOK);
    fd_cur[slot].c_iflag |= ISTRIP;
    fd_cur[slot].c_lflag &= ~ICANON;
    fd_cur[slot].c_cc[VMIN] = 1;
    fd_cur[slot].c_cc[VTIME] = 0;
    /* enable new state */
    if (tcsetattr(fd, TCSANOW, fd_cur + slot) < 0) {
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("calc_tty: Cannot tcsetattr fd %d\n", fd);
        }
        return false;
    }
    if (conf->calc_debug & CALCDBG_TTY) {
        printf("calc_tty: stty -ECHO -ECHOE -ECHOK -ICANON +ISTRIP "
               "VMIN=1 VTIME=0: fd %d\n",
               fd);
    }

    /*
     * note that the tty slot is on use
     */
    fd_setup[slot] = fd;
    return true;
}

/*
 * orig_tty - restore the original state of a file descriptor
 *
 * This routine will restore the state of a descriptor to its calc
 * startup value if it was set for interactive use by calc_tty().
 *
 * Given:
 *      fd      the descriptor to restore
 *
 * Returns:
 *      true    restore was successful
 *      false   unable to restore descriptor to original state
 */
bool
orig_tty(int fd)
{
    int slot; /* the saved descriptor slot or -1 */

    /*
     * find the saved slot for this descriptor
     */
    slot = find_tty_state(fd);
    if (slot < 0) {
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("orig_tty: Cannot get saved descriptor slot\n");
        }
        return false;
    }

    /*
     * do nothing if no state was saved for this descriptor
     */
    if (fd_setup[slot] < 0) {
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("orig_tty: no state saved for fd %d\n", fd);
        }
        return false;
    }

    /*
     * assume termios tty state method
     */
    (void)tcsetattr(fd, TCSANOW, fd_orig + slot);
    if (conf->calc_debug & CALCDBG_TTY) {
        printf("orig_tty: TCSANOW restored fd %d\n", fd);
    }

    /*
     * note new current state
     */
    fd_cur[slot] = fd_orig[slot];

    /*
     * Since current state is the original state, we can free up
     * this slot.  This also prevents functions such as the
     * libcalc_call_me_last() function from re-restoring it.
     */
    fd_setup[slot] = -1;
    return true;
}

/*
 * xdg_base - return an XDG base directory if it names a usable absolute path.
 */
#if defined(_WIN32) || defined(_WIN64)

static char *
xdg_base(char *UNUSED(env), int UNUSED(mode))
{
    return NULL;
}

#else

static char *
xdg_base(char *env, int mode)
{
    char *base;

    base = (no_env ? NULL : getenv(env));
    if (base == NULL || *base == '\0' || base[0] != PATHCHAR || access(base, mode) != 0) {
        return NULL;
    }
    return base;
}

#endif
