/* tempname:  create a unique temporary file (or filename) for use by shell
   scripts

   Copyright (C) 2004-2026 by Brian Lindholm.  This file is part of the
   littleutils utility set.

   The tempname utility is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option) any
   later version.

   The tempname utility 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 General Public License for
   more details.

   You should have received a copy of the GNU General Public License along with
   the littleutils.  If not, see <https://www.gnu.org/licenses/>. */

#include <config.h>

#include <errno.h>
#include <fcntl.h>
#ifdef HAVE_STDIO_H
# include <stdio.h>
#endif
#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
# define OPTEND -1
#else
# define OPTEND EOF
#endif
#ifdef HAVE_GETOPT_H
# include <getopt.h>
#endif

#include "rand_funcs.h"

#ifdef __MINGW32__
extern int getopt(int argc, char * const *argv, const char *optstring);
extern char *optarg;
extern int optind;
# include <process.h>
#endif

#ifdef DJGPP
unsigned short _djstat_flags = 63;  /* speed up stat command for DJGPP */
#endif

#define GT_FILE 1
#define GT_DIR 2
#define GT_NOCREATE 3

/* help function */
static void help(FILE *where)
{
  fprintf(where, "tempname " PACKAGE_VERSION "\n"
    "usage: tempname [-c(reate_not)] [-d dirname] [-h(elp)] [-n(o_random_portion)]\n"
    "         [-q(uiet)] [-s suffix] [-v(erbose)] [-w(ildcard)] [-D(irectory)]\n"
    "         filename_prefix\n"
    "note:  using the \"-w\" option implies the \"-c\" option and the \"-n\" option,\n"
    "         the \"-n\" option also implies the \"-c\" option\n");
}

/* directory check routine */
static int good_dir(const char *path, int verbose)
{
  struct stat file_stats;
#if (!defined(__MINGW32__) && !defined(DJGPP))
  uid_t uid = (uid_t) 0;
  gid_t gid = (gid_t) 0;
#endif

  if (path == NULL) return(0);
  if (stat(path, &file_stats)) {  /* doesn't exist? */
    if (verbose) fprintf(stderr, "tempname error: %s doesn't exist\n", path);
    return(0);
  }
  if ((file_stats.st_mode & S_IFDIR) != S_IFDIR) {  /* not a directory? */
    if (verbose) fprintf(stderr, "tempname error: %s isn't a directory\n", path);
    return(0);
  }
  if (strchr(path, ' ') != NULL) {  /* contains a space character? */
    if (verbose) fprintf(stderr, "tempname error: %s contains a space character\n", path);
    return(0);
  }
#if defined(__MINGW32__)
  if ((file_stats.st_mode & (S_IREAD | S_IWRITE)) == (S_IREAD | S_IWRITE)) return(1);  /* writeable? */
  if (verbose) fprintf(stderr, "tempname error: you don't have permission to write to %s\n", path);
  return(0);
#elif defined(DJGPP)
  if ((file_stats.st_mode & S_IRWXU) == S_IRWXU) return(1);  /* writeable? */
  if (verbose) fprintf(stderr, "tempname error: you don't have permission to write to %s\n", path);
  return(0);
#else
  if ((file_stats.st_mode & S_IRWXO) == S_IRWXO) return(1);  /* writeable? */
  uid = getuid();
  if ((file_stats.st_uid == uid) && ((file_stats.st_mode & S_IRWXU) == S_IRWXU)) return(1);
  gid = getgid();
  if ((file_stats.st_gid == gid) && ((file_stats.st_mode & S_IRWXG) == S_IRWXG)) return(1);
  if (verbose) {
    if (file_stats.st_uid == uid)
      fprintf(stderr, "tempname error: permissions are incorrectly set on %s\n", path);
    else
      fprintf(stderr, "tempname error: you don't have permission to write to %s\n", path);
  }
  return(0);
#endif
}

/* special variant of mkstemp, allows X's to occur in non-end locations */
static int mkstemp_custom(char *tmpl, int kind)
{
  char *copy;
  int count, fd, rc;
  int save_errno = errno;
  size_t i, len;
  struct stat file_stats;
  /* characters used in temporary file names */
  static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

  /* determine length of template and allocate storage */
  len = strlen(tmpl);
  copy = (char *) malloc((len + 1) * sizeof(char));
  /* initialize random number generator */
  rand_seed();
  /* generate the random name */
  for (count = 0; count < TMP_MAX; ++count) {
    strcpy(copy, tmpl);
    for (i = 0; i < len; ++i) if (copy[i] == '|')
#if defined(HAVE_LRAND48)
      copy[i] = letters[(int) lrand48() % 62];
#elif defined(HAVE_RANDOM)
      copy[i] = letters[(int) random() % 62];
#else
      copy[i] = letters[(int) rand() % 62];
#endif
    switch (kind) {
      case GT_FILE:
        fd = open(copy, O_RDWR | O_CREAT | O_EXCL, 0600);
        if (fd >= 0) {
          errno = save_errno;
          strcpy(tmpl, copy);
          free(copy);
          return(fd);
        }
        else if (errno != EEXIST) {
          /* any other error will apply also to other names we might try, and there are VERY many of them, so
           * give up now */
          free(copy);
          return(-1);
        }
        break;
      case GT_DIR:
        rc = mkdir(copy, 0700);
        if (rc == 0) {
          errno = save_errno;
          strcpy(tmpl, copy);
          free(copy);
          return(0);
        }
        else if (errno != EEXIST) {
          /* any other error will apply also to other names we might try, and there are VERY many of them, so
           * give up now */
          free(copy);
          return(-1);
        }
        break;
      case GT_NOCREATE:
        rc = stat(copy, &file_stats);
        if (rc < 0) {
          if (errno == ENOENT) {
            errno = save_errno;
            strcpy(tmpl, copy);
            free(copy);
            return(0);
          }
          else {
            /* any other error will apply also to other names we might try, and there are VERY many of them, so
             * give up now */
            free(copy);
            return(-1);
          }
        }
        break;
      default:
        fprintf(stderr, "tempname assertion failure: bad switch logic\n");
        return(-2);
    }
  }
  /* tried too many times, bailing */
  errno = EEXIST;
  free(copy);
  return(-1);
}

/* main program */
int main(int argc, char *argv[])
{
  char *newname, *path, *suffix;
  int add_random, add_suffix, add_wild, argo, create, directory, file_id, i, length, opt, path_ok, rc, user_dir,
    verbose;

  /* parse options */
  add_random = 1;
  add_suffix = 0;
  add_wild = 0;
  create = 1;
  directory = 0;
  path = "";
  user_dir = 0;
  suffix = "";
  verbose = 0;
  while ((opt = getopt(argc, argv, "cd:hnqs:vwD")) != OPTEND) switch (opt) {
    case 'c':
      create = 0;
      break;
    case 'd':
      user_dir = 1;
      path = optarg;
      break;
    case 'h':
      help(stdout);
      return(0);
    case 'n':
      add_random = 0;
      create = 0;
      break;
    case 'q':
      verbose = -1;
      break;
    case 's':
      add_suffix = 1;
      suffix = optarg;
      break;
    case 'v':
      verbose = 1;
      break;
    case 'w':
      add_wild = 1;
      add_random = 0;
      create = 0;
      break;
    case 'D':
      directory = 1;
      break;
    case '?':
      help(stderr);
      return(1);
  }
  /* if no arguments given, print help */
  argo = argc - optind;
  if (argo == 0) {
    help(stdout);
    return(0);
  }
  /* if incorrect number of arguments given, print help */
  if (argo != 1) {
    help(stderr);
    return(1);
  }
  /* find a writeable path */
  path_ok = 0;
  if (user_dir) {
    if (verbose == -1)
      path_ok = good_dir(path, 0);
    else
      path_ok = good_dir(path, 1);
  }
  if (!path_ok) {
    path = getenv("TMPDIR");
    path_ok = good_dir(path, verbose);
  }
  if (!path_ok) {
    path = getenv("TEMP");
    path_ok = good_dir(path, verbose);
  }
  if (!path_ok) {
    path = getenv("TMP");
    path_ok = good_dir(path, verbose);
  }
  if (!path_ok) {
    path = "/tmp";
    path_ok = good_dir(path, verbose);
  }
  if (!path_ok) {
    path = "/var/tmp";
    path_ok = good_dir(path, verbose);
  }
  if (!path_ok) {
    path = ".";
    path_ok = good_dir(path, verbose);
  }
  if (!path_ok) {
    fprintf(stderr, "tempname error: can't find writeable directory\n");
    return(2);
  }
  /* append a filename prefix, unique letter block, and optional suffix */
  length = (int) strlen(path) + (int) strlen(argv[optind]) + (int) strlen(suffix) + 10;
  newname = (char *) malloc(length * sizeof(char));
  strcpy(newname, "");
  strcat(newname, path);
#if (defined(__MINGW32__) || defined(DJGPP))
  if (newname[strlen(newname) - 1] != '\\') strcat(newname, "\\");
#else
  if (newname[strlen(newname) - 1] != '/') strcat(newname, "/");
#endif
  strcat(newname, argv[optind]);
  if (add_wild)
    strcat(newname, "_*");
  else if (add_random)
    strcat(newname, "_||||||||");  /* use | instead of X in case path contains X */
  if (add_suffix) strcat(newname, suffix);
  /* replace spaces with underscores to reduce risk of errors in upstream scripts */
  for (i = 0; i <= strlen(newname); i++) if (newname[i] == ' ') newname[i] = '_';
  /* scramble unique letter block and optionally create temporary file */
  if (add_random) {
    if (directory) {
      rc = mkstemp_custom(newname, GT_DIR);
      if (rc < 0) {
        fprintf(stderr, "tempname error: can't open a tempdir for writing\n");
        return(3);
      }
    }
    else if (create) {
      file_id = mkstemp_custom(newname, GT_FILE);
      if (file_id > 0)
        close(file_id);
      else {
        fprintf(stderr, "tempname error: can't open a tempfile for writing\n");
        return(3);
      }
    }
    else {
      rc = mkstemp_custom(newname, GT_NOCREATE);
      if (rc < 0) {
        fprintf(stderr, "tempname error: can't create a tempname for writing\n");
        return(3);
      }
    }
  }
  /* print out filename of temporary file */
  if (strlen(newname) == 0) {
    fprintf(stderr, "tempname error: can't form unique filename\n");
    free(newname);
    return(3);
  }
  else
    fprintf(stdout, "%s\n", newname);
  /* free memory and indicate successful finish */
  free(newname);
  return(0);
}
