#include "mktmpdir.h"
#include "sha1.h"

#ifdef O_BINARY
#  define OPEN_O_BINARY O_BINARY
#else
#  define OPEN_O_BINARY 0
#endif

#ifndef P_tmpdir
#define P_tmpdir "/tmp"
#endif

/* NOTE: This code is #include'd both from a plain C program (boot.c)
 * and our custom Perl interpreter (main.c). In the latter case,
 * lstat() or stat() may be #define'd as calls into PerlIO and
 * expect pointer to a Stat_t as second parameter, rather than a pointer
 * to a struct stat. Try to distinguish these cases by checking
 * whether Stat_t is defined. */
#ifndef Stat_t
#define Stat_t struct stat
#endif

static char PAR_MAGIC[] = "\nPAR.pm\n";
#define magic_size 8

/* "\0CACHE" or "\0CLEAN" */
#define cache_marker_size 6

/* size of a pack("N") number */
#define FILE_offset_size 4

#define cache_name_size 40

#define CHECK(code, msg) if (!(code)) { par_die(msg); }


static int isWritableDir(const char* val)
{
    Stat_t statbuf;

    return par_lstat(val, &statbuf) == 0 &&
           ( S_ISDIR(statbuf.st_mode) || S_ISLNK(statbuf.st_mode) ) &&
           access(val, W_OK) == 0;
}

#ifndef WIN32
/* check that:
 * - val is a directory (and not a symlink)
 * - val is owned by the user
 * - val has mode 0700
 */
static int isSafeDir(const char* val)
{
    Stat_t statbuf;

    return par_lstat(val, &statbuf) == 0 &&
           S_ISDIR(statbuf.st_mode) &&
           statbuf.st_uid == getuid() &&
           (statbuf.st_mode & 0777) == 0700;
}
#endif

void par_setup_libpath( const char * stmpdir )
{
    const char *ldlibpthname = stringify(LDLIBPTHNAME);
    const char *val;

    if ( (val = par_getenv(ldlibpthname)) == NULL || strlen(val) == 0 ) {
        par_setenv(ldlibpthname, stmpdir);
    }
    else {
        /* prepend stmpdir to (value of) environment variable */
       char *new_val = malloc(
            strlen(stmpdir) + strlen(path_sep) + strlen(val) + 1);
        sprintf(
            new_val, "%s%s%s",
            stmpdir, path_sep, val);
        par_setenv(ldlibpthname, new_val);
    }
}

static void *par_memrmem(void* haystack, size_t haystacklen, void* needle, size_t needlelen)
{
    char *hs = haystack;
    char *p;
    if (haystacklen < needlelen)
        return NULL;
    for (p = hs + haystacklen - needlelen; p >= hs; p--)
        if (memcmp(p, needle, needlelen) == 0)
            return p;
    return NULL;
}

static off_t find_par_magic(int fd)
{
#define CHUNK_SIZE (64 * 1024)
    char buf[CHUNK_SIZE + magic_size];
    off_t pos;
    int len;
    char *p;
    off_t file_size = lseek(fd, 0, 2);

    for (pos = (file_size-1) - (file_size-1) % CHUNK_SIZE;
         pos >= 0;
         pos -= CHUNK_SIZE) {
        CHECK(lseek(fd, pos, 0) != -1, "lseek failed");
        len = read(fd, buf, CHUNK_SIZE + magic_size);
        CHECK(len != -1, "read failed");
        p = par_memrmem(buf, len, PAR_MAGIC, magic_size);
        if (p)
            return pos + (p - buf);
    }
    return -1;
#undef CHUNK_SIZE
}

char *par_mktmpdir ( char **argv ) {
    int i;
    const char *tmpdir = NULL;
    const char *key = NULL , *val = NULL;

    /* NOTE: all arrays below are NULL terminated */
    const char *temp_dirs[] = {
        P_tmpdir,
#ifdef WIN32
        "C:\\TEMP",
#endif
        ".", NULL };
    const char *temp_keys[] = { "PAR_TMPDIR", "TMPDIR", "TEMPDIR",
                                 "TEMP", "TMP", NULL };
    const char *user_keys[] = { "USER", "USERNAME", NULL };

    const char *subdirbuf_prefix = "par-";
    const char *subdirbuf_suffix = "";

    char *progname = NULL, *username = NULL;
    char *stmpdir = NULL, *top_tmpdir = NULL;
    int f, stmp_len = 0;
    char sha1[cache_name_size + 1];

    if ( (val = par_getenv("PAR_TEMP")) && strlen(val) ) {
        par_setup_libpath(val);
        return strdup(val);
    }

#ifdef WIN32
    {
        DWORD buflen = MAXPATHLEN;
        username = malloc(MAXPATHLEN);
        GetUserName((LPTSTR)username, &buflen);
        // FIXME this is uncondifionally overwritten below - WTF?
    }
#endif

    /* Determine username */
    username = get_username_from_getpwuid();
    if ( !username ) { /* fall back to env vars */
        for ( i = 0 ; username == NULL && (key = user_keys[i]); i++) {
            if ( (val = par_getenv(key)) && strlen(val) )
                username = strdup(val);
        }
    }
    if ( username == NULL )
        username = "SYSTEM";

    /* sanitize username: encode all bytes as 2 hex digits */
    {
        char *hexname = malloc(2 * strlen(username) + 1);
        char *u, *h;
        for ( u = username, h = hexname ; *u != '\0' ; u++, h += 2)
            sprintf(h, "%02x", *(unsigned char*)u);
        username = hexname;
    }

    /* Try temp environment variables */
    for ( i = 0 ; tmpdir == NULL && (key = temp_keys[i]); i++ ) {
        if ( (val = par_getenv(key)) && strlen(val) && isWritableDir(val) ) {
            tmpdir = strdup(val);
            break;
        }
    }

#ifdef WIN32
    /* Try the windows temp directory */
    if ( tmpdir == NULL && (val = par_getenv("WinDir")) && strlen(val) ) {
        char* p = malloc(strlen(val) + 5 + 1);
        sprintf(p, "%s\\temp", val);
        if (isWritableDir(p)) {
            tmpdir = p;
        } else {
            free(p);
        }
    }
#endif

    /* Try default locations */
    for ( i = 0 ; tmpdir == NULL && (val = temp_dirs[i]) && strlen(val) ; i++ ) {
        if ( isWritableDir(val) ) {
            tmpdir = strdup(val);
        }
    }

    /* "$TEMP/par-$USER" */
    stmp_len =
        strlen(tmpdir) +
        strlen(subdirbuf_prefix) +
        strlen(username) +
        strlen(subdirbuf_suffix) + 1024;

    /* stmpdir is what we are going to return;
       top_tmpdir is the top $TEMP/par-$USER, needed to build stmpdir.
       NOTE: We need 2 buffers because snprintf() can't write to a buffer
       it is also reading from. */
    top_tmpdir = malloc( stmp_len );
    sprintf(top_tmpdir, "%s%s%s%s", tmpdir, dir_sep, subdirbuf_prefix, username);
#ifdef WIN32
    _mkdir(top_tmpdir);         /* FIXME bail if error (other than EEXIST) */
#else
    {
        if (mkdir(top_tmpdir, 0700) == -1 && errno != EEXIST) {
            fprintf(stderr, "%s: creation of private subdirectory %s failed (errno=%i)\n",
                    argv[0], top_tmpdir, errno);
            return NULL;
        }

        if (!isSafeDir(top_tmpdir)) {
            fprintf(stderr, "%s: private subdirectory %s is unsafe (please remove it and retry your operation)\n",
                    argv[0], top_tmpdir);
            return NULL;
        }
    }
#endif

    stmpdir = malloc( stmp_len );

    /* Doesn't really work - XXX */
    val = par_getenv( "PATH" );
    if (val != NULL)
        progname = par_findprog(argv[0], val);
    if (progname == NULL)
        progname = argv[0];

    /* If invoked as "/usr/bin/parl foo.par myscript.pl" then progname should
     * be ".../parl", and we don't want to base our checksum on that, but
     * rather on "foo.par".
     */
    {
#ifdef WIN32
#define STREQ(a,b) (strcasecmp(a,b) == 0)
#else
#define STREQ(a,b) (strcmp(a,b) == 0)
#endif
        const char *parl_exe = stringify(PARL_EXE);
	int prog_len = strlen(progname);
	int parl_len = strlen(parl_exe);

	if (prog_len >= parl_len
	    && STREQ(progname + prog_len - parl_len, parl_exe)
	    && (prog_len == parl_len || progname[prog_len - parl_len - 1] == dir_sep[0])
	    && argv[1]
	    && strlen(argv[1]) >= 4
	    && STREQ(argv[1] + strlen(argv[1]) - 4, ".par"))
		progname = argv[1];
#undef STREQ
    }

    int use_cache = 0;
    if ( !par_env_clean() && (f = open( progname, O_RDONLY | OPEN_O_BINARY ))) {
        off_t pos = find_par_magic(f);
        char buf[cache_marker_size];

        if (pos >= 0) {
            /* back up over pack(N) number and "\0CACHE" (or "\0CLEAN") */
            pos -= FILE_offset_size + cache_marker_size;
            lseek(f, pos, 0);
            CHECK(read(f, buf, cache_marker_size) == cache_marker_size, "short read");
            if (memcmp(buf, "\0CACHE", cache_marker_size) == 0) {
                use_cache = 1;
                /* back up over pre-computed cache_name */
                pos -= cache_name_size;
                lseek(f, pos, 0);
                CHECK(read(f, sha1, cache_name_size) == cache_name_size, "short read");
                sha1[cache_name_size] = '\0';
            }
            else if (memcmp(buf, "\0CLEAN", cache_marker_size) == 0) {
                use_cache = 0;
            }
        }
    }
    if (use_cache) {
        /* "$TEMP/par-$USER/cache-$SHA1" */
        sprintf(
            stmpdir,
            "%s%scache-%s%s",
            top_tmpdir, dir_sep, sha1, subdirbuf_suffix
        );
    }
    else {
        /* "$TEMP/par-$USER/temp-$PID" */
        par_setenv("PAR_CLEAN", "1");
        sprintf(
            stmpdir,
            "%s%stemp-%u%s",
            top_tmpdir, dir_sep, getpid(), subdirbuf_suffix
        );

        /* Ensure we pick an unused directory each time.  If the directory
           already exists when we try to create it, bump a counter and try
           "$TEMP/par-$USER/temp-$PID-$i". This will guard against cases where
           a prior invocation crashed leaving garbage in a temp directory that
           might interfere. */
        int i = 0;
        while (my_mkdir(stmpdir, 0700) == -1 && errno == EEXIST) {
            sprintf(
                stmpdir,
                "%s%stemp-%u-%u%s",
                top_tmpdir, dir_sep, getpid(), ++i, subdirbuf_suffix
                );
        }
    }

    free(top_tmpdir);

    /* set dynamic loading path */
    par_setenv("PAR_TEMP", stmpdir);

    par_setup_libpath( stmpdir );

    return stmpdir;
}


#ifdef WIN32
static void par_rmtmpdir ( char *stmpdir ) {
    struct _finddata_t cur_file;
    int subsub_len;
    char *subsubdir;
    char *slashdot;
    intptr_t hFile;
    int tries = 0;
    HMODULE dll;

    if ((stmpdir == NULL) || !strlen(stmpdir)) return;

    subsub_len = strlen(stmpdir) + 258;
    subsubdir = malloc( subsub_len );

    sprintf(subsubdir, "%s\\*.*", stmpdir);

    hFile = _findfirst( subsubdir, &cur_file );
    if ( hFile == -1 ) return;

    do {
        if (!strstr(cur_file.name, "\\")) {
            sprintf(subsubdir, "%s\\%s", stmpdir, cur_file.name);
        }
        else {
            sprintf(subsubdir, "%s", cur_file.name);
        }

        if (!(slashdot = strstr(subsubdir, "\\.")) || (strcmp(slashdot,"\\.") && strcmp(slashdot,"\\.."))) {
            if ((cur_file.attrib & _A_SUBDIR)) {
                par_rmtmpdir( subsubdir );
            }
            else {
                dll = GetModuleHandle(cur_file.name);
                tries = 0;
                while ( _unlink(subsubdir) && ( tries++ < 10 ) ) {
                    if ( dll ) FreeLibrary(dll);
                };
            }
        }
    } while ( _findnext( hFile, &cur_file ) == 0 );

    _findclose(hFile);
    _rmdir(stmpdir);
}

#else
static void par_rmtmpdir ( char *stmpdir ) {
    DIR *partmp_dirp;
    Direntry_t *dp;
    char *subsubdir = NULL;
    int  subsub_len;
    Stat_t stbuf;

    /* remove temporary PAR directory */
    if (!stmpdir || !*stmpdir) return;

    partmp_dirp = opendir(stmpdir);

    if ( partmp_dirp == NULL ) return;

    while ( ( dp = readdir(partmp_dirp) ) != NULL ) {
        if ( strcmp (dp->d_name, ".") != 0 && strcmp (dp->d_name, "..") != 0 )
        {
            subsub_len = strlen(stmpdir) + 1 + strlen(dp->d_name) + 1;
            subsubdir = malloc( subsub_len);
            sprintf(subsubdir, "%s/%s", stmpdir, dp->d_name);
            if (stat(subsubdir, &stbuf) != -1 && S_ISDIR(stbuf.st_mode)) {
                par_rmtmpdir(subsubdir);
            }
            else {
                unlink(subsubdir);
            }
            free(subsubdir);
            subsubdir = NULL;
        }
    }

    closedir(partmp_dirp);
    rmdir(stmpdir);
}
#endif

void par_cleanup (char *stmpdir) {
    char *dirname, *basename;
    if ( par_env_clean() && stmpdir != NULL && strlen(stmpdir)) {
        dirname = par_dirname(stmpdir);
        basename = par_basename(dirname);
        if ( strstr(basename, "par-") == basename ) {
            par_rmtmpdir(stmpdir);
            /* Don't try to remove dirname because this will introduce a race
               with other applications that are trying to start. */
        }
    }
}