/* filesys.c -- filesystem specific functions.

   Copyright 1993-2023 Free Software Foundation, Inc.

   This program 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 of the License, or
   (at your option) any later version.

   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

   Originally written by Brian Fox. */

#include "info.h"
#include "tilde.h"
#include "filesys.h"
#include "tag.h"
#include "session.h"

/* Local to this file. */
static char *info_file_in_path (char *filename, struct stat *finfo);
char *info_add_extension (char *dirname, char *fname,
                                 struct stat *finfo);

static char *filesys_read_compressed (char *pathname, size_t *filesize);

/* Return the command string that would be used to decompress FILENAME. */
static char *filesys_decompressor_for_file (char *filename);
static int compressed_filename_p (char *filename);

typedef struct
{
  char *suffix;
  char *decompressor;
} COMPRESSION_ALIST;

static char *info_suffixes[] = {
  ".info",
  "-info",
  ".inf",       /* 8+3 file on filesystem which supports long file names */
#ifdef __MSDOS__
  /* 8+3 file names strike again...  */
  ".in",        /* for .inz, .igz etc. */
  ".i",
#endif
  "",
  NULL
};

static COMPRESSION_ALIST compress_suffixes[] = {
#if STRIP_DOT_EXE
  { ".gz", "gunzip" },
  { ".lz", "lunzip" },
#else
  { ".gz", "gzip -d" },
  { ".lz", "lzip -d" },
#endif
  { ".xz", "unxz" },
  { ".bz2", "bunzip2" },
  { ".z", "gunzip" },
  { ".lzma", "unlzma" },
  { ".Z", "uncompress" },
  { ".zst", "unzstd --rm -q" },
  { ".Y", "unyabba" },
#ifdef __MSDOS__
  { "gz", "gunzip" },
  { "z", "gunzip" },
#endif
  { NULL, NULL }
};

/* Look for the filename PARTIAL in INFOPATH in order to find the correct file.
   Return file name and set *FINFO with information about file.  If it
   can't find the file, it returns NULL, and sets filesys_error_number.
   Return value should be freed by caller. */
char *
info_find_fullpath (char *partial, struct stat *finfo)
{
  char *fullpath = 0;
  struct stat dummy;

  debug(1, (_("looking for file \"%s\""), partial));

  if (!finfo)
    finfo = &dummy;

  filesys_error_number = 0;

  if (!partial || !*partial)
    return 0;
  
  /* IS_SLASH and IS_ABSOLUTE defined in ../system.h. */

  /* If path is absolute already, see if it needs an extension. */
  if (IS_ABSOLUTE (partial)
      || partial[0] == '.' && IS_SLASH(partial[1]))
    {
      fullpath = info_add_extension (0, partial, finfo);
    }

  /* Tilde expansion.  Could come from user input in echo area. */
  else if (partial[0] == '~')
    {
      partial = tilde_expand_word (partial);
      fullpath = info_add_extension (0, partial, finfo);
    }

  /* If just a simple name element, look for it in the path. */
  else
    fullpath = info_file_in_path (partial, finfo);

  if (!fullpath)
    filesys_error_number = ENOENT;

  return fullpath;
}

/* Scan the directories in search path looking for FILENAME.  If we find
   one that is a regular file, return it as a new string.  Otherwise, return
   a NULL pointer.  Set *FINFO with information about file. */
char *
info_file_find_next_in_path (char *filename, int *path_index, struct stat *finfo)
{
  struct stat dummy;

  /* Used for output of stat in case the caller doesn't care about
     its value. */
  if (!finfo)
    finfo = &dummy;

  /* Reject ridiculous cases up front, to prevent infinite recursion
     later on.  E.g., someone might say "info '(.)foo'"...  */
  if (!*filename || STREQ (filename, ".") || STREQ (filename, ".."))
    return NULL;

  while (1)
    {
      char *dirname, *with_extension = 0;

      dirname = infopath_next (path_index);
      if (!dirname)
        break;

      debug(1, (_("looking for file %s in %s"), filename, dirname));

      /* Expand a leading tilde if one is present. */
      if (*dirname == '~')
        {
          char *expanded_dirname = tilde_expand_word (dirname);
          dirname = expanded_dirname;
        }

      with_extension = info_add_extension (dirname, filename, finfo);

      if (with_extension)
        {
          if (!IS_ABSOLUTE (with_extension))
            {
              /* Prefix "./" to it. */
              char *s;
              xasprintf (&s, "%s%s", "./", with_extension);
              free (with_extension);
              return s;
            }
          else
            return with_extension;
        }
    }
  return NULL;
}

/* Return full path of first Info file known as FILENAME in
   search path.  If relative to current directory, precede it with './'. */
static char *
info_file_in_path (char *filename, struct stat *finfo)
{
  int i = 0;
  return info_file_find_next_in_path (filename, &i, finfo);
}

/* Check if TRY_FILENAME exists, possibly compressed.  If so, return
   filename in TRY_FILENAME. */
char *
info_check_compressed (char *try_filename, struct stat *finfo)
{
  int statable = (stat (try_filename, finfo) == 0);

  if (statable)
    {
      if (S_ISREG (finfo->st_mode))
        {
          debug(1, (_("found file %s"), try_filename));
          return try_filename;
        }
    }
  else
    {
      /* Add various compression suffixes to the name to see if
         the file is present in compressed format. */
      register int j, pre_compress_suffix_length;

      pre_compress_suffix_length = strlen (try_filename);

      for (j = 0; compress_suffixes[j].suffix; j++)
        {
          strcpy (try_filename + pre_compress_suffix_length,
                  compress_suffixes[j].suffix);

          statable = (stat (try_filename, finfo) == 0);
          if (statable && (S_ISREG (finfo->st_mode)))
            {
              debug(1, (_("found file %s"), try_filename));
              return try_filename;
            }
        }
    }
  return 0;
}

/* Look for a file called FILENAME in a directory called DIRNAME, adding file
   extensions if necessary.  FILENAME can be an absolute path or a path
   relative to the current directory, in which case DIRNAME should be
   null.  Return it as a new string; otherwise return a NULL pointer. */
char *
info_add_extension (char *dirname, char *filename, struct stat *finfo)
{
  char *try_filename;
  register int i, pre_suffix_length = 0;
  struct stat dummy;

  if (!finfo)
    finfo = &dummy;

  if (dirname)
    pre_suffix_length += strlen (dirname);

  pre_suffix_length += strlen (filename);

  /* Add enough space for any file extensions at end. */
  try_filename = xmalloc (pre_suffix_length + 30);
  try_filename[0] = '\0';

  if (dirname)
    {
      strcpy (try_filename, dirname);
      if (!IS_SLASH (try_filename[(strlen (try_filename)) - 1]))
        {
          strcat (try_filename, "/");
          pre_suffix_length++;
        }
    }

  strcat (try_filename, filename);

  for (i = 0; info_suffixes[i]; i++)
    {
      char *result;
      strcpy (try_filename + pre_suffix_length, info_suffixes[i]);

      result = info_check_compressed (try_filename, finfo);
      if (result)
        return result;
    }
  /* Nothing was found. */
  free (try_filename);
  return 0;
}

#if defined (__MSDOS__) || defined (__MINGW32__)
/* Given a chunk of text and its length, convert all CRLF pairs at every
   end-of-line into a single Newline character.  Return the length of
   produced text.

   This is required because the rest of code is too entrenched in having
   a single newline at each EOL; in particular, searching for various
   Info headers and cookies can become extremely tricky if that assumption
   breaks. */
static long
convert_eols (char *text, long int textlen)
{
  register char *s = text;
  register char *d = text;

  while (textlen--)
    {
      if (*s == '\r' && textlen && s[1] == '\n')
       {
         s++;
         textlen--;
       }
      *d++ = *s++;
    }

  return d - text;
}
#endif

/* Read the contents of PATHNAME, returning a buffer with the contents of
   that file in it, and returning the size of that buffer in FILESIZE.
   If the file turns out to be compressed, set IS_COMPRESSED to non-zero.
   If the file cannot be read, set filesys_error_number and return a NULL
   pointer.  Set *FINFO with information about file. */
char *
filesys_read_info_file (char *pathname, size_t *filesize,
			struct stat *finfo, int *is_compressed)
{
  size_t fsize;
  char *contents;

  fsize = filesys_error_number = 0;

  stat (pathname, finfo);
  fsize = (long) finfo->st_size;

  if (compressed_filename_p (pathname))
    {
      *is_compressed = 1;
      contents = filesys_read_compressed (pathname, &fsize);
    }
  else
    {
      int descriptor;

      *is_compressed = 0;
      descriptor = open (pathname, O_RDONLY | O_BINARY, 0666);

      /* If the file couldn't be opened, give up. */
      if (descriptor < 0)
        {
          filesys_error_number = errno;
          return NULL;
        }

      /* Try to read the contents of this file. */
      contents = xmalloc (1 + fsize);
      if ((read (descriptor, contents, fsize)) != fsize)
        {
	  filesys_error_number = errno;
	  close (descriptor);
	  free (contents);
	  return NULL;
        }
      contents[fsize] = 0;
      close (descriptor);
    }

#if defined (__MSDOS__) || defined (__MINGW32__)
  /* Old versions of makeinfo on MS-DOS or MS-Windows generated Info files
     with CR-LF line endings which are only counted as one byte in the file
     tag table.  Convert any of these DOS-style CRLF EOLs into Unix-style NL
     so that these files can be read correctly on such operating systems.

     Don't do this on GNU/Linux (or other Unix-type operating system), so
     as not to encourage Info files with CR-LF line endings to be distributed
     widely beyond their native operating system, which would cause only
     problems.  (If someone really needs to, they can convert the line endings
     themselves with a separate program.)
     Also, this will allow any Info files that contain any CR-LF endings by
     mistake to work as expected (except on MS-DOS/Windows). */

  fsize = convert_eols (contents, fsize);

  /* EOL conversion can shrink the text quite a bit.  We don't
     want to waste storage.  */
  contents = xrealloc (contents, 1 + fsize);
  contents[fsize] = '\0';
#endif

  *filesize = fsize;

  return contents;
}

/* Typically, pipe buffers are 4k. */
#define BASIC_PIPE_BUFFER (4 * 1024)

/* We use some large multiple of that. */
#define FILESYS_PIPE_BUFFER_SIZE (16 * BASIC_PIPE_BUFFER)

static char *
filesys_read_compressed (char *pathname, size_t *filesize)
{
  FILE *stream;
  char *command, *decompressor;
  char *contents = NULL;

  *filesize = filesys_error_number = 0;

  decompressor = filesys_decompressor_for_file (pathname);

  if (!decompressor)
    return NULL;

  command = xmalloc (15 + strlen (pathname) + strlen (decompressor));
  /* Explicit .exe suffix makes the diagnostics of `popen'
     better on systems where COMMAND.COM is the stock shell.  */
  sprintf (command, "%s%s < %s",
	   decompressor, STRIP_DOT_EXE ? ".exe" : "", pathname);

  if (info_windows_initialized_p)
    {
      char *temp;

      temp = xmalloc (5 + strlen (command));
      sprintf (temp, "%s...", command);
      message_in_echo_area ("%s", temp);
      free (temp);
    }

  stream = popen (command, FOPEN_RBIN);
  free (command);

  /* Read chunks from this file until there are none left to read. */
  if (stream)
    {
      size_t offset, size;
      char *chunk;
    
      offset = size = 0;
      chunk = xmalloc (FILESYS_PIPE_BUFFER_SIZE);

      while (1)
        {
          size_t bytes_read;

          bytes_read = fread (chunk, 1, FILESYS_PIPE_BUFFER_SIZE, stream);

          if (bytes_read + offset >= size)
            contents = xrealloc
              (contents, size += (2 * FILESYS_PIPE_BUFFER_SIZE));

          memcpy (contents + offset, chunk, bytes_read);
          offset += bytes_read;
          if (bytes_read != FILESYS_PIPE_BUFFER_SIZE)
            break;
        }

      free (chunk);
      if (pclose (stream) == -1)
	{
	  if (contents)
	    free (contents);
	  contents = NULL;
	  filesys_error_number = errno;
	}
      else
	{
	  contents = xrealloc (contents, 1 + offset);
	  contents[offset] = '\0';
	  *filesize = offset;
	}
    }
  else
    {
      filesys_error_number = errno;
    }

  if (info_windows_initialized_p)
    unmessage_in_echo_area ();
  return contents;
}

/* Return non-zero if FILENAME belongs to a compressed file. */
static int
compressed_filename_p (char *filename)
{
  char *decompressor;

  /* Find the final extension of this filename, and see if it matches one
     of our known ones. */
  decompressor = filesys_decompressor_for_file (filename);

  if (decompressor)
    return 1;
  else
    return 0;
}

/* Return the command string that would be used to decompress FILENAME. */
static char *
filesys_decompressor_for_file (char *filename)
{
  register int i;
  char *extension = NULL;

  /* Find the final extension of FILENAME, and see if it appears in our
     list of known compression extensions. */
  for (i = strlen (filename) - 1; i > 0; i--)
    if (filename[i] == '.')
      {
        extension = filename + i;
        break;
      }

  if (!extension)
    return NULL;

  for (i = 0; compress_suffixes[i].suffix; i++)
    if (FILENAME_CMP (extension, compress_suffixes[i].suffix) == 0)
      return compress_suffixes[i].decompressor;

#if defined (__MSDOS__)
  /* If no other suffix matched, allow any extension which ends
     with `z' to be decompressed by gunzip.  Due to limited 8+3 DOS
     file namespace, we can expect many such cases, and supporting
     every weird suffix thus produced would be a pain.  */
  if (extension[strlen (extension) - 1] == 'z' ||
      extension[strlen (extension) - 1] == 'Z')
    return "gunzip";
#endif

  return NULL;
}

/* The number of the most recent file system error. */
int filesys_error_number = 0;

/* A function which returns a pointer to a static buffer containing
   an error message for FILENAME and ERROR_NUM. */
static char *errmsg_buf = NULL;
static int errmsg_buf_size = 0;

/* Return string for ERROR_NUM when opening file.  Return value should not
   be freed by caller. */
char *
filesys_error_string (char *filename, int error_num)
{
  int len;
  const char *result;

  if (error_num == 0)
    return NULL;

  result = strerror (error_num);

  len = 4 + strlen (filename) + strlen (result);
  if (len >= errmsg_buf_size)
    errmsg_buf = xrealloc (errmsg_buf, (errmsg_buf_size = 2 + len));

  sprintf (errmsg_buf, "%s: %s", filename, result);
  return errmsg_buf;
}


/* Check for "dir" with all the possible info and compression suffixes,
   in combination.  */

int
is_dir_name (char *filename)
{
  unsigned i;

  for (i = 0; info_suffixes[i]; i++)
    {
      unsigned c;
      char trydir[50];
      strcpy (trydir, "dir");
      strcat (trydir, info_suffixes[i]);
      
      if (mbscasecmp (filename, trydir) == 0)
        return 1;

      for (c = 0; compress_suffixes[c].suffix; c++)
        {
          char dir_compressed[50]; /* can be short */
          strcpy (dir_compressed, trydir); 
          strcat (dir_compressed, compress_suffixes[c].suffix);
          if (mbscasecmp (filename, dir_compressed) == 0)
            return 1;
        }
    }  

  return 0;
}
