/*
 * input - nested input source file reader
 *
 * Copyright (C) 1999-2007,2014,2018,2021-2023,2026  David I. Bell
 *
 * 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:   1990/02/15 01:48:16
 * File existed as early as:    before 1990
 *
 * Share and enjoy!  :-)        http://www.isthe.com/chongo/tech/comp/calc/
 */

/*
 * Nested input source file reader.
 * For terminal input, this also provides a simple command stack.
 */

/*
 * important <system> header includes
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdint.h>
#include <stdbool.h>

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

#if defined(_WIN32) || defined(_WIN64)
#  include <limits.h>
#  define _fullpath(f, n, s) (_fixpath(n, f), f)
#endif

/*
 * calc local src includes
 */
#include "value.h"
#include "calc.h"
#include "conf.h"
#include "hist.h"
#include "strl.h"
#include "attribute.h"
#include "errtbl.h"

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

extern int stdin_tty; /* true if stdin is a tty */
extern FILE *f_open(char *name, char *mode);
extern FILE *curstream(void);

#define TTYSIZE 8191                /* reallocation size for terminal buffers */
#define MAXDEPTH 255                /* maximum depth of input */
#define IS_READ 1                   /* reading normally */
#define IS_REREAD 2                 /* reread current character */
#define chartoint(ch) ((ch) & 0xff) /* make sure char is not negative */
#define READSET_ALLOC 8             /* readset to allocate chunk size */

typedef struct {
    int i_state;    /* state (read, reread) */
    int i_char;     /* currently read char */
    long i_line;    /* line number */
    char *i_cp;     /* pointer to string character to be read */
    char *i_str;    /* start of string copy to be read, or NULL */
    long i_num;     /* number of string characters remaining */
    char *i_ttystr; /* current character of tty line (or NULL) */
    FILE *i_fp;     /* current file for input (if not NULL) */
    char *i_name;   /* file name if known */
} INPUT;

/* files that calc has read or included */
typedef struct {
    int active;        /* != 0 => active entry, 0 => unused entry */
    char *name;        /* name used to read file */
    char *path;        /* real path used to open file */
    struct stat inode; /* inode information for file */
} READSET;

static READSET *readset = NULL; /* array of files read */
static int maxreadset = 0;      /* length of readset */

static int linesize;  /* current max size of input line */
static char *linebuf; /* current input line buffer */
static char *prompt;  /* current prompt for terminal */
static bool noprompt; /* true if should not print prompt */

static int depth;              /* current input depth */
static INPUT *cip;             /* current input source */
static INPUT inputs[MAXDEPTH]; /* input sources */

static int openfile(char *name);
static int ttychar(void);
static int isinoderead(struct stat *sbuf);
static int findfreeread(void);
static int addreadset(char *name, char *path, struct stat *sbuf);
static char *homeexpand(char *name);

/*
 * Open an input file by possibly searching through a path list
 * and also possibly applying the specified extension.  For example:
 *
 *      opensearchfile("barf", ".:/tmp", ".c", rd_once)
 *
 * searches in order for the files:
 *
 *      "./barf", "./barf.c", "/tmp/barf", and "/tmp/barf.c".
 *
 * Returns -1 if we could not open a file or error.
 * Returns 1 if file was opened and added to/updated in the readset
 * Returns 0 if file was already in the readset and reopen was 0.
 *
 * given:
 *      name            file name to be read
 *      pathlist        list of colon separated paths (or NULL)
 *      extension       extra extension to try (or NULL)
 *      rd_once         true => do not reread a file
 */
int
opensearchfile(char *name, char *pathlist, char *extension, int rd_once)
{
    int i;
    char *cp;
    char *path;          /* name being searched for */
    size_t path_alloc;   /* length of malloced path */
    struct stat statbuf; /* stat of the path */
    size_t namelen;      /* length of name */
    size_t extlen;       /* length of the extension if non-NULL or 0 */
    size_t pathlen;      /* length of the pathlist if non-NULL or 0 */

    /* firewall */
    if (name == NULL) {
        math_error("NULL name given to opensearchfile");
        not_reached();
    }

    /*
     * We ignore the pathlist of the file is absolute, dot-relative
     * or tilde-relative or if there is no search path.
     */
    if (name[0] == PATHCHAR || name[0] == HOMECHAR || (name[0] == DOTCHAR && name[1] == '\0') ||
        (name[0] == DOTCHAR && name[1] == DOTCHAR && name[2] == '\0') || (name[0] == DOTCHAR && name[1] == PATHCHAR) ||
        (name[0] == DOTCHAR && name[1] == DOTCHAR && name[2] == PATHCHAR) || pathlist == NULL) {
        pathlist = "";
    }

    /*
     * allocate storage for the longest name being searched for
     *
     * We will allocate more than we could ever want/need.
     * The longest we could ever need would be:
     *
     *      pathlist (as a single long string)
     *      /
     *      name
     *      .
     *      extension
     *      \0
     *      guard byte
     */
    namelen = strlen(name);
    if (extension != NULL) {
        extlen = strlen(extension);
    } else {
        extlen = 0;
    }
    pathlen = strlen(pathlist);
    path_alloc = pathlen + 1 + 1 + namelen + 1 + extlen + 1 + 1 + 1;
    path = calloc(path_alloc + 1, 1);
    if (path == NULL) {
        math_error("Cannot allocate filename path buffer");
        not_reached();
    }
    path[0] = '\0';          /* paranoia */
    path[path_alloc] = '\0'; /* paranoia */

    /*
     * Don't try the extension if the filename already contains it.
     */
    if (extension != NULL && namelen >= extlen && strcmp(&name[namelen - extlen], extension) == 0) {
        extension = NULL;
    }

    /*
     * Search through the path list for the file
     */
    pathlist--;
    do {
        pathlist++;
        cp = path;
        while (*pathlist && (*pathlist != LISTCHAR)) {
            *cp++ = *pathlist++;
        }
        if (cp != path) {
            *cp++ = PATHCHAR;
        }
        strlcpy(cp, name, namelen + 1);
        i = openfile(path);
        if ((i < 0) && (extension != NULL && extension[0] != '\0')) {
            strlcat(path, extension, path_alloc + 1);
            i = openfile(path);
        }
    } while ((i < 0) && *pathlist);

    /* examine what our search produced */
    if (i < 0) {
        free(path);
        return i;
    }
    if (cip->i_fp == NULL) {
        /* cannot find a file to open */
        free(path);
        return -3;
    }
    if (fstat(fileno(cip->i_fp), &statbuf) < 0) {
        /* unable to fstat the open file */
        free(path);
        return -4;
    }

    /* note if we will reopen a file and if that is allowed */
    if (rd_once == true && isinoderead(&statbuf) >= 0) {
        /* file is in readset and reopen is false */
        closeinput();
        free(path);
        return 1;
    }

    /* add this name to the readset if allowed */
    if (addreadset(name, path, &statbuf) < 0) {
        /* cannot add to readset */
        closeinput();
        free(path);
        return -1;
    }

    /* file was added to/updated in readset */
    free(path);
    return 0;
}

/*
 * f_pathopen - open an absolute or relative filename along a search path
 *
 * Open a file by possibly searching through a path list.  For example:
 *
 *      f_pathopen("curds", ".:/tmp:~/pub", "r", NULL)
 *
 * searches in order for a file that it can open for reading:
 *
 *      "./curds", "/tmp/curds", "~/pub/curds"
 *
 *      NOTE: ~ is expanded accordingly (see homeexpand() below).
 *
 * However is the file is /absolue/path/name or a ./dot/relative/name, or
 * a ~/home/dir/name, or a ~user/home/name, then the pathlist is ignored
 * we just attempt to open name.
 *
 * and opens the first one that exists and allows the mode.
 *
 *      name            file name to be read
 *      mode            fopen() mode argument
 *                        (one of "r", "w", "a", "r+", "w+", "a+")
 *      pathlist        list of colon separated paths (or NULL)
 *      openpath        if non-NULL, and file was opened, set to malloced
 *                        path used to open
 *
 * returns:
 *      open file stream, NULL ==> file was not found or error
 *      If file was open and openpath was non-NULL, changed to point
 *      to path used to open
 */
FILE *
f_pathopen(char *name, char *mode, char *pathlist, char **openpath)
{
    char *cp;
    char *path;     /* name being searched for */
    size_t namelen; /* length of name */
    size_t pathlen; /* length of the pathlist if non-NULL or 0 */
    FILE *ret;      /* return open stream or NULL */

    /* firewall */
    if (name == NULL) {
        math_error("NULL name given to f_pathopen");
        not_reached();
    }
    if (mode == NULL) {
        math_error("NULL mode given to f_pathopen");
        not_reached();
    }

    /*
     * We ignore the pathlist of the file is absolute, dot-relative
     * or tilde-relative or if there is no search path.
     */
    if (name[0] == PATHCHAR || name[0] == HOMECHAR || (name[0] == DOTCHAR && name[1] == '\0') ||
        (name[0] == DOTCHAR && name[1] == DOTCHAR && name[2] == '\0') || (name[0] == DOTCHAR && name[1] == PATHCHAR) ||
        (name[0] == DOTCHAR && name[1] == DOTCHAR && name[2] == PATHCHAR) || pathlist == NULL) {
        pathlist = "";
    }

    /*
     * allocate storage for the longest name being searched for
     *
     * We will allocate more than we could ever want/need.
     * The longest we could ever need would be:
     *
     *      pathlist (as a single long string)
     *      /
     *      name
     *      \0
     *      guard byte
     */
    namelen = strlen(name);
    pathlen = strlen(pathlist);
    path = calloc(pathlen + 1 + 1 + namelen + 1 + 1 + 1, 1);
    if (path == NULL) {
        math_error("Cannot allocate f_pathopen buffer");
        not_reached();
    }

    /*
     * Search through the path list for the file
     */
    pathlist--;
    do {
        pathlist++;
        cp = path;
        while (*pathlist && (*pathlist != LISTCHAR)) {
            *cp++ = *pathlist++;
        }
        if (cp != path) {
            *cp++ = PATHCHAR;
        }
        strlcpy(cp, name, namelen + 1);
        ret = f_open(path, mode);
    } while ((ret == NULL) && *pathlist);

    /* if caller wants to know the path, malloc it and return it */
    if (openpath != NULL && ret != NULL) {
        if (path[0] == HOMECHAR) {
            *openpath = homeexpand(path);
        } else {
            *openpath = strdup(path);
        }
        if (*openpath == NULL) {
            free(path);
            if ((conf->calc_debug & CALCDBG_TTY) && ret == stdin) {
                printf("f_pathopen: closing stdin "
                       "on malloc return error\n");
            }
            fclose(ret);
            math_error("cannot malloc return openpath buffer");
            not_reached();
        }
    }
    free(path);

    /* return open file or NULL */
    return ret;
}

/*
 * Given a filename with a leading ~, expand it into a home directory for
 * that user.  This function will malloc the space for the expanded path.
 *
 * If the path is just ~, or begins with ~/, expand it to the home
 * directory of the current user.  If the environment variable $HOME
 * is known, it will be used, otherwise the password file will be
 * consulted.
 *
 * If the path is just ~username, or ~username/, expand it to the home
 * directory of that user by looking it up in the password file.
 *
 * If the password file must be consulted and the username is not found
 * a NULL pointer is returned.
 *
 * given:
 *      name            a filename with a leading ~
 */
static char *
homeexpand(char *name)
{
#if defined(_WIN32) || defined(_WIN64)

    return NULL;

#else

    struct passwd *ent;  /* password entry */
    char *home2;         /* fullpath of the home directory */
    char *fullpath;      /* the malloced expanded path */
    char *after;         /* after the ~user or ~ */
    char *username;      /* extracted username */
    size_t fullpath_len; /* length of fullpath */
    size_t snprintf_len; /* malloced snprintf buffer length */

    /* firewall */
    if (name[0] != HOMECHAR) {
        return NULL;
    }

    /*
     * obtain the home directory component
     */
    switch (name[1]) {
    case PATHCHAR: /* ~/... */
    case '\0':     /* ~ */
        home2 = home;
        after = name + 1;
        break;
    default: /* ~username or ~username/... */

        /* extract the username after the ~ */
        after = (char *)strchr(name + 2, PATHCHAR);
        if (after == NULL) {
            /* path is just ~username */
            ent = (struct passwd *)getpwnam(name + 1);
            if (ent == NULL) {
                /* unknown user */
                return NULL;
            }
            /* just malloc the home directory and return it */
            fullpath_len = strlen(ent->pw_dir);
            fullpath = (char *)calloc(fullpath_len + 1, 1);
            if (fullpath == NULL) {
                return NULL;
            }
            strlcpy(fullpath, ent->pw_dir, fullpath_len + 1);
            return fullpath;
        }
        username = (char *)calloc(after - name + 1 + 1, 1);
        if (username == NULL) {
            /* failed to malloc username */
            return NULL;
        }
        strlcpy(username, name + 1, after - name + 1 + 1);

        /* get that user's home directory */
        ent = (struct passwd *)getpwnam(username);
        free(username);
        if (ent == NULL) {
            /* unknown user */
            return NULL;
        }
        home2 = ent->pw_dir;
        break;
    }

    /*
     * build the fullpath given the home directory
     */
    snprintf_len = strlen(home2) + strlen(after) + 1;
    fullpath = (char *)calloc(snprintf_len + 1, 1);
    if (fullpath == NULL) {
        return NULL;
    }
    snprintf(fullpath, snprintf_len, "%s%s", home2, after);
    fullpath[snprintf_len] = '\0'; /* paranoia */
    return fullpath;

#endif
}

/*
 * f_open - ~-expand a filename and fopen() it
 *
 * given:
 *      name            the filename to open
 *      mode            fopen() mode argument
 *                          (one of "r", "w", "a", "r+", "w+", "a+")
 */
FILE *
f_open(char *name, char *mode)
{
    FILE *fp;       /* open file descriptor */
    char *fullname; /* file name with HOMECHAR expansion */

    /*
     * be sore we are allowed to open a file in this mode
     */
    if (!allow_read && !allow_write) {
        /* no reads and no writes means no opens! */
        if (run_state > RUN_BEGIN) {
            fprintf(stderr, "open of %s mode %s - %s\n", name, mode, "open for read or write disallowed by -m\n");
        }
        return NULL;
    } else if (!allow_read && strchr(mode, 'r') != NULL) {
        /* reading new files disallowed */
        if (run_state > RUN_BEGIN) {
            fprintf(stderr, "open of %s mode %s - %s\n", name, mode, "open for read disallowed by -m\n");
        }
        return NULL;
    } else if (!allow_write && (strchr(mode, 'w') != NULL || strchr(mode, 'a') != NULL || strchr(mode, '+') != NULL)) {
        /* writing new files disallowed */
        if (run_state > RUN_BEGIN) {
            fprintf(stderr, "open of %s mode %s - %s\n", name, mode, "open for write disallowed by -m\n");
        }
        return NULL;
    }

    /*
     * expand ~ if needed
     */
    if (name[0] == HOMECHAR) {
        fullname = homeexpand(name);
        if (fullname == NULL) {
            return NULL;
        }
        fp = fopen(fullname, mode);
        free(fullname);
    } else {
        fp = fopen(name, mode);
    }
    return fp;
}

/*
 * Setup for reading from a input file.
 * Returns -1 if file could not be opened.
 *
 * given:
 *      name            file name to be read
 */
static int
openfile(char *name)
{
    FILE *fp; /* open file descriptor */
    size_t namelen;

    if (depth >= MAXDEPTH) {
        return -2;
    }
    fp = f_open(name, "r");
    if (fp == NULL) {
        return -1;
    }
    cip = inputs + depth++;
    cip->i_state = IS_READ;
    cip->i_char = '\0';
    cip->i_str = NULL;
    cip->i_ttystr = NULL;
    cip->i_fp = fp;
    cip->i_line = 1;
    namelen = strlen(name);
    cip->i_name = (char *)calloc(namelen + 1, 1);
    if (cip->i_name == NULL) {
        return -1;
    }
    strlcpy(cip->i_name, name, namelen + 1);
    return 0;
}

/*
 * Return the current input file stream, or NULL if none.
 */
FILE *
curstream(void)
{
    if (depth <= 0 || depth > MAXDEPTH) {
        return NULL;
    }
    return cip->i_fp;
}

/*
 * Open a string for scanning, num characters to be read.
 * String is copied into local memory so it can be trashed afterwards.
 * Returns -1 if cannot open string.
 *
 * given:
 *      str             string to be opened
 *      num             length of string to open
 */
int
openstring(char *str, size_t num)
{
    char *cp; /* copied string */

    if ((depth >= MAXDEPTH) || (str == NULL)) {
        return -2;
    }
    cp = (char *)calloc(num + 1, 1);
    if (cp == NULL) {
        return -1;
    }
    strlcpy(cp, str, num + 1);
    cip = inputs + depth++;
    cip->i_state = IS_READ;
    cip->i_char = '\0';
    cip->i_cp = cp;
    cip->i_str = cp;
    cip->i_num = num;
    cip->i_fp = NULL;
    cip->i_name = NULL;
    cip->i_ttystr = NULL;
    cip->i_line = 1;
    return 0;
}

/*
 * Set to read input from the terminal.
 * Returns -1 if there is no more depth for input.
 */
int
openterminal(void)
{
    if (depth >= MAXDEPTH) {
        return -2;
    }
    cip = inputs + depth++;
    cip->i_state = IS_READ;
    cip->i_char = '\0';
    cip->i_str = NULL;
    cip->i_ttystr = NULL;
    cip->i_fp = NULL;
    cip->i_name = NULL;
    cip->i_line = 1;
    return 0;
}

/*
 * Close the current input source.
 */
void
closeinput(void)
{
    if (depth <= 0) {
        return;
    }
    if (cip->i_str) {
        free(cip->i_str);
    }
    if (cip->i_fp) {
        if ((conf->calc_debug & CALCDBG_TTY) && cip->i_fp == stdin) {
            printf("closeinput: closing stdin "
                   "at depth: %d\n",
                   depth);
        }
        fclose(cip->i_fp);
    }
    if (cip->i_name) {
        free(cip->i_name);
    }
    depth--;
    cip = depth ? &inputs[depth - 1] : NULL;
}

/*
 * Reset the input sources back to the initial state.
 */
void
resetinput(void)
{
    while (depth > 0) {
        closeinput();
    }
    noprompt = false;
}

/*
 * Set the prompt for terminal input.
 */
void
setprompt(char *str)
{
    prompt = str;
    noprompt = false;
}

/*
 * Read the next character from the current input source.
 * End of file closes current input source, and returns EOF character.
 */
int
nextchar(void)
{
    int ch; /* current input character */

    if (depth == 0) { /* input finished */
        return EOF;
    }
    if (cip->i_state == IS_REREAD) { /* rereading current char */
        ch = cip->i_char;
        cip->i_state = IS_READ;
        if (ch == '\n') {
            cip->i_line++;
        }
        return ch;
    }
    if (cip->i_str) { /* from string */
        if (cip->i_num) {
            ch = chartoint(*cip->i_cp++);
            cip->i_num--;
        } else {
            ch = EOF;
        }
    } else if (cip->i_fp) { /* from file */
        ch = fgetc(cip->i_fp);
    } else if (!stdin_tty) { /* from file */
        ch = fgetc(stdin);
    } else { /* from terminal */
        ch = ttychar();
    }
    if (depth > 0) {
        cip->i_char = ch; /* save for rereads */
    }
    if (ch == '\n') {
        cip->i_line++;
    }
    return ch;
}

/*
 * Read in the next line of input from the current input source.
 * The line is terminated with a null character, and does not contain
 * the final newline character.  The returned string is only valid
 * until the next such call, and so must be copied if necessary.
 * Returns NULL on end of file.
 */
char *
nextline(void)
{
    char *cp;
    int ch;
    int len;

    cp = linebuf;
    if (linesize == 0) {
        cp = (char *)calloc(TTYSIZE + 1, 1);
        if (cp == NULL) {
            math_error("Cannot allocate line buffer");
            not_reached();
        }
        linebuf = cp;
        linesize = TTYSIZE;
    }
    len = 0;
    for (;;) {
        noprompt = true;
        ch = nextchar();
        noprompt = false;
        if (ch == EOF) {
            return NULL;
        }
        if (ch == '\0') {
            continue;
        }
        if (ch == '\n') {
            break;
        }
        if (len >= linesize) {
            cp = (char *)realloc(cp, linesize + TTYSIZE + 1);
            if (cp == NULL) {
                math_error("Cannot realloc line buffer");
                not_reached();
            }
            linebuf = cp;
            linesize += TTYSIZE;
        }
        cp[len++] = (char)ch;
    }
    cp[len] = '\0';
    return linebuf;
}

/*
 * Read the next character from the terminal.
 * The routines in the history module are called so that the user
 * can use a command history and emacs-like editing of the line.
 */
static int
ttychar(void)
{
    int ch;  /* current char */
    int len; /* length of current command */
    static char charbuf[256 * 1024];

    /*
     * If we have more to read from the saved command line, then do that.
     * When we see a newline character, then clear the pointer so we will
     * read a new line on the next call.
     */
    if (cip->i_ttystr) {
        ch = chartoint(*cip->i_ttystr++);
        if (ch == '\n') {
            cip->i_ttystr = NULL;
        }
        return ch;
    }

    /*
     * We need another complete line.
     */
    abortlevel = 0;
    inputwait = true;
    len = hist_getline(noprompt ? "" : prompt, charbuf, sizeof(charbuf));
    if (len == 0) {
        inputwait = false;
        return EOF;
    }
    inputwait = false;

    /*
     * Handle shell escape if present
     */
    if (charbuf[0] == '!') { /* do a shell command */
        char *cmd;
        int ret;

        cmd = charbuf + 1;
        if (*cmd == '\0' || *cmd == '\n') {
            cmd = (shell == NULL) ? DEFAULTSHELL : shell;
        }
        if (allow_exec) {
            if (conf->calc_debug & CALCDBG_SYSTEM) {
                printf("%s\n", cmd);
            }
            ret = system(cmd);
            if (ret < 0) {
                fprintf(stderr, "error in cmd: %s\n", cmd);
            }
        } else {
            fprintf(stderr, "execution disallowed by -m flag\n");
        }
        return '\n';
    }
    hist_saveline(charbuf, len);

    /*
     * Return the first character of the line, and set up to
     * return the rest of it with later calls.
     */
    ch = chartoint(charbuf[0]);
    if (ch != '\n') {
        cip->i_ttystr = charbuf + 1;
    }
    return ch;
}

/*
 * Return whether or not the input source is the terminal.
 */
bool
inputisterminal(void)
{
    return ((depth <= 0) || ((cip->i_str == NULL) && (cip->i_fp == NULL)));
}

/*
 * Return depth of current input source
 */
int
inputlevel(void)
{
    return depth - 1;
}

/*
 * Return the name of the current input file.
 * Returns NULL for terminal or strings.
 */
char *
inputname(void)
{
    if (depth <= 0) {
        return NULL;
    }
    return cip->i_name;
}

/*
 * Return the current line number.
 */
long
linenumber(void)
{
    if (depth > 0) {
        return cip->i_line;
    }
    return 1;
}

/*
 * Restore the next character to be read again on the next nextchar call.
 */
void
reread(void)
{
    if ((depth <= 0) || (cip->i_state == IS_REREAD)) {
        return;
    }
    cip->i_state = IS_REREAD;
    if (cip->i_char == '\n') {
        cip->i_line--;
    }
}

/*
 * Process all startup files found in the $CALCRC path.
 */
void
runrcfiles(void)
{
    char path[MAX_CALCRC + 1 + 1]; /* name being searched for */
    char *cp;
    char *p;

    /* execute each file in the list */
    while (calcrc != NULL && *calcrc) {
        cp = calcrc;
        calcrc = (char *)strchr(calcrc + 1, LISTCHAR);

        /* load file name into the path */
        if (calcrc == NULL) {
            strlcpy(path, cp, MAX_CALCRC + 1);
        } else {
            strlcpy(path, cp, calcrc - cp + 1);
        }

        /* find the start of the path */
        p = (path[0] == ':') ? path + 1 : path;
        if (p[0] == '\0') {
            continue;
        }

        /* process the current file in the list */
        if (openfile(p) < 0) {
            /* Unable to open rcfile */
            if (c_flag && !d_flag) {
                fprintf(stderr, "Unable to open rcfile \"%s\"\n", p);
            }
            continue;
        }
        getcommands(false);
        closeinput();
    }
}

/*
 * isinoderead - determine if we have read a given dev/inode
 *
 * This function returns the index of the readset element that matches
 * a given device/inode, -1 otherwise.
 *
 *
 * _WIN32 and _WIN64 NOTE:
 *
 *      This function likely will not work under _WIN32 or _WIN64.
 *
 *      The sbuf->st_ino is always zero because the FAT and NTFS filesystems
 *      do not support inodes.  Those filesystems don't support links, which
 *      is why you need this function under UNIX. For _WIN32 or _WIN64, use
 *      _fullpath() to determine if you have already opened a file.
 *
 * given:
 *      sbuf            stat of the inode in question
 */

static int
isinoderead(struct stat *sbuf)
{
    int i;

    /* deal with the empty case */
    if (readset == NULL || maxreadset <= 0) {
        /* readset is empty */
        return -1;
    }

    /* scan the entire readset */
    for (i = 0; i < maxreadset; ++i) {
#if defined(_WIN32) || defined(_WIN64)

        char tmp[MAX_PATH + 1];
        tmp[MAX_PATH] = '\0';
        if (_fullpath(tmp, cip->i_name, MAX_PATH) && readset[i].active && strcasecmp(readset[i].path, tmp) == 0) {
            /* found a match */
            return i;
        }
#else

        if (readset[i].active && sbuf->st_dev == readset[i].inode.st_dev && sbuf->st_ino == readset[i].inode.st_ino) {
            /* found a match */
            return i;
        }

#endif
    }

    /* no match found */
    return -1;
}

/*
 * findfreeread - find the next free readset element
 *
 * This function will return the index of the next free readset element.
 * If needed, this function will allocate new readset elements.
 *
 * This function returns the index of the next free element, or -1.
 */
static int
findfreeread(void)
{
    int i;

    /* deal with an empty readset case */
    if (readset == NULL || maxreadset <= 0) {

        /* malloc a new readset */
        readset = (READSET *)calloc(READSET_ALLOC + 1, sizeof(READSET));
        if (readset == NULL) {
            return -1;
        }
        maxreadset = READSET_ALLOC;
        for (i = 0; i < READSET_ALLOC; ++i) {
            readset[i].active = 0;
        }

        /* return first entry */
        return 0;
    }

    /* try to find a free readset entry */
    for (i = 0; i < maxreadset; ++i) {
        if (readset[i].active == 0) {
            /* found a free readset entry */
            return i;
        }
    }

    /* all readset entries are in use, allocate more */
    readset = (READSET *)realloc(readset, (maxreadset + READSET_ALLOC) * sizeof(READSET));
    if (readset == NULL) {
        return -1;
    }
    for (i = 0; i < READSET_ALLOC; ++i) {
        readset[i + maxreadset].active = 0;
    }
    maxreadset += READSET_ALLOC;

    /* return the first newly allocated free entry */
    return maxreadset - READSET_ALLOC;
}

/*
 * addreadset - add a entry to the readset array if it is not already there
 *
 * This function attempts to add a file into the readset.  If the readset
 * has an entry with a matching dev/inode, then that entry is updated with
 * the new name and path.  If no such readset entry is found, a new entry
 * is added.
 *
 * This function returns the index of the readset entry, or -1 if error.
 *
 * given:
 *      name            name given to read or include
 *      path            full pathname of file
 *      sbuf            stat of the path
 */
static int
addreadset(char *name, char *path, struct stat *sbuf)
{
    int ret;         /* index to return */
    size_t name_len; /* length of read set name */
    size_t path_len; /* length of path to read set name */

    /* find the inode */
    ret = isinoderead(sbuf);
    if (ret < 0) {
        /* not in readset, find a free node */
        ret = findfreeread();
        if (ret < 0) {
            /* cannot find/form a free readset entry */
            return -1;
        }
    } else {
        /* found an readset entry, free old readset data */
        if (readset[ret].name != NULL) {
            free(readset[ret].name);
        }
        if (readset[ret].path != NULL) {
            free(readset[ret].path);
        }
    }

    /* load our information into the readset entry */
    name_len = strlen(name);
    readset[ret].name = (char *)calloc(name_len + 1, 1);
    if (readset[ret].name == NULL) {
        return -1;
    }
    strlcpy(readset[ret].name, name, name_len + 1);
#if defined(_WIN32) || defined(_WIN64)

    /*
     * For _WIN32 or _WIN64, _fullpath expands the path to a fully qualified
     * path name, which under _WIN32 or _WIN64 FAT and NTFS is unique, just
     * like UNIX inodes. _fullpath also allocated the memory for
     * this new longer path name.
     */
    {
        readset[ret].path = _fullpath(NULL, path, MAX_PATH);
        if (readset[ret].path == NULL) {
            return -1;
        }
    }

#else

    path_len = strlen(path);
    readset[ret].path = (char *)calloc(path_len + 1, 1);
    if (readset[ret].path == NULL) {
        return -1;
    }
    strlcpy(readset[ret].path, path, path_len + 1);

#endif
    readset[ret].inode = *sbuf;
    readset[ret].active = 1;

    /* return index of the newly added entry */
    return ret;
}
