/*
 * Copyright (c) 2000, 2001, 2002, 2003, 2004 by Martin C. Shepherd.
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, and/or sell copies of the Software, and to permit persons
 * to whom the Software is furnished to do so, provided that the above
 * copyright notice(s) and this permission notice appear in all copies of
 * the Software and that both the above copyright notice(s) and this
 * permission notice appear in supporting documentation.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
 * OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
 * INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder
 * shall not be used in advertising or otherwise to promote the sale, use
 * or other dealings in this Software without prior written authorization
 * of the copyright holder.
 */

/*
 * If file-system access is to be excluded, this module has no function,
 * so all of its code should be excluded.
 */
#ifndef WITHOUT_FILE_SYSTEM

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "pathutil.h"

/*.......................................................................
 * Create a new PathName object.
 *
 * Output:
 *  return  PathName *  The new object, or NULL on error.
 */
PathName *_new_PathName(void)
{
  PathName *path;  /* The object to be returned */
/*
 * Allocate the container.
 */
  path = (PathName *) malloc(sizeof(PathName));
  if(!path) {
    errno = ENOMEM;
    return NULL;
  };
/*
 * Before attempting any operation that might fail, initialize the
 * container at least up to the point at which it can safely be passed
 * to _del_PathName().
 */
  path->name = NULL;
  path->dim = 0;
/*
 * Figure out the maximum length of an expanded pathname.
 */
  path->dim = _pu_pathname_dim();
  if(path->dim == 0)
    return _del_PathName(path);
/*
 * Allocate the pathname buffer.
 */
  path->name = (char *)malloc(path->dim * sizeof(char));
  if(!path->name) {
    errno = ENOMEM;
    return _del_PathName(path);
  };
  return path;
}

/*.......................................................................
 * Delete a PathName object.
 *
 * Input:
 *  path   PathName *  The object to be deleted.
 * Output:
 *  return PathName *  The deleted object (always NULL).
 */
PathName *_del_PathName(PathName *path)
{
  if(path) {
    if(path->name)
      free(path->name);
    free(path);
  };
  return NULL;
}

/*.......................................................................
 * Return the pathname to a zero-length string.
 *
 * Input:
 *  path     PathName *  The pathname container.
 * Output:
 *  return       char *  The cleared pathname buffer, or NULL on error.
 */
char *_pn_clear_path(PathName *path)
{
/*
 * Check the arguments.
 */
  if(!path) {
    errno = EINVAL;
    return NULL;
  };
  path->name[0] = '\0';
  return path->name;
}

/*.......................................................................
 * Append a string to a pathname, increasing the size of the pathname
 * buffer if needed.
 *
 * Input:
 *  path        PathName *  The pathname container.
 *  string    const char *  The string to be appended to the pathname.
 *                          Note that regardless of the slen argument,
 *                          this should be a '\0' terminated string.
 *  slen             int    The maximum number of characters to append
 *                          from string[], or -1 to append the whole
 *                          string.
 *  remove_escapes   int    If true, remove the backslashes that escape
 *                          spaces, tabs, backslashes etc..
 * Output:
 *  return          char *  The pathname string path->name[], which may
 *                          have been reallocated, or NULL if there was
 *                          insufficient memory to extend the pathname.
 */
char *_pn_append_to_path(PathName *path, const char *string, int slen,
			int remove_escapes)
{
  int pathlen;     /* The length of the pathname */
  int i;
/*
 * Check the arguments.
 */
  if(!path || !string) {
    errno = EINVAL;
    return NULL;
  };
/*
 * Get the current length of the pathname.
 */
  pathlen = strlen(path->name);
/*
 * How many characters should be appended?
 */
  if(slen < 0 || slen > strlen(string))
    slen = strlen(string);
/*
 * Resize the pathname if needed.
 */
  if(!_pn_resize_path(path, pathlen + slen))
    return NULL;
/*
 * Append the string to the output pathname, removing any escape
 * characters found therein.
 */
  if(remove_escapes) {
    int is_escape = 0;
    for(i=0; i<slen; i++) {
      is_escape = !is_escape && string[i] == '\\';
      if(!is_escape)
	path->name[pathlen++] = string[i];
    };
/*
 * Terminate the string.
 */
    path->name[pathlen] = '\0';
  } else {
/*
 * Append the string directly to the pathname.
 */
    memcpy(path->name + pathlen, string, slen);
    path->name[pathlen + slen] = '\0';
  };
  return path->name;
}

/*.......................................................................
 * Prepend a string to a pathname, increasing the size of the pathname
 * buffer if needed.
 *
 * Input:
 *  path        PathName *  The pathname container.
 *  string    const char *  The string to be prepended to the pathname.
 *                          Note that regardless of the slen argument,
 *                          this should be a '\0' terminated string.
 *  slen             int    The maximum number of characters to prepend
 *                          from string[], or -1 to append the whole
 *                          string.
 *  remove_escapes   int    If true, remove the backslashes that escape
 *                          spaces, tabs, backslashes etc..
 * Output:
 *  return          char *  The pathname string path->name[], which may
 *                          have been reallocated, or NULL if there was
 *                          insufficient memory to extend the pathname.
 */
char *_pn_prepend_to_path(PathName *path, const char *string, int slen,
			  int remove_escapes)
{
  int pathlen;     /* The length of the pathname */
  int shift;       /* The number of characters to shift the suffix by */
  int i,j;
/*
 * Check the arguments.
 */
  if(!path || !string) {
    errno = EINVAL;
    return NULL;
  };
/*
 * Get the current length of the pathname.
 */
  pathlen = strlen(path->name);
/*
 * How many characters should be appended?
 */
  if(slen < 0 || slen > strlen(string))
    slen = strlen(string);
/*
 * Work out how far we need to shift the original path string to make
 * way for the new prefix. When removing escape characters, we need
 * final length of the new prefix, after unescaped backslashes have
 * been removed.
 */
  if(remove_escapes) {
    int is_escape = 0;
    for(shift=0,i=0; i<slen; i++) {
      is_escape = !is_escape && string[i] == '\\';
      if(!is_escape)
	shift++;
    };
  } else {
    shift = slen;
  };
/*
 * Resize the pathname if needed.
 */
  if(!_pn_resize_path(path, pathlen + shift))
    return NULL;
/*
 * Make room for the prefix at the beginning of the string.
 */
  memmove(path->name + shift, path->name, pathlen+1);
/*
 * Copy the new prefix into the vacated space at the beginning of the
 * output pathname, removing any escape characters if needed.
 */
  if(remove_escapes) {
    int is_escape = 0;
    for(i=j=0; i<slen; i++) {
      is_escape = !is_escape && string[i] == '\\';
      if(!is_escape)
	path->name[j++] = string[i];
    };
  } else {
    memcpy(path->name, string, slen);
  };
  return path->name;
}

/*.......................................................................
 * If needed reallocate a given pathname buffer to allow a string of
 * a given length to be stored in it.
 *
 * Input:
 *  path     PathName *  The pathname container object.
 *  length     size_t    The required length of the pathname buffer,
 *                       not including the terminating '\0'.
 * Output:
 *  return       char *  The pathname buffer, or NULL if there was
 *                       insufficient memory.
 */
char *_pn_resize_path(PathName *path, size_t length)
{
/*
 * Check the arguments.
 */
  if(!path) {
    errno = EINVAL;
    return NULL;
  };
/*
 * If the pathname buffer isn't large enough to accomodate a string
 * of the specified length, attempt to reallocate it with the new
 * size, plus space for a terminating '\0'. Also add a bit of
 * head room to prevent too many reallocations if the initial length
 * turned out to be very optimistic.
 */
  if(length + 1 > path->dim) {
    size_t dim =  length + 1 + PN_PATHNAME_INC;
    char *name = (char *) realloc(path->name, dim);
    if(!name)
      return NULL;
    path->name = name;
    path->dim = dim;
  };
  return path->name;
}

/*.......................................................................
 * Estimate the largest amount of space needed to store a pathname.
 *
 * Output:
 *  return size_t   The number of bytes needed, including space for the
 *                  terminating '\0'.
 */
size_t _pu_pathname_dim(void)
{
  int maxlen;   /* The return value excluding space for the '\0' */
/*
 * If the POSIX PATH_MAX macro is defined in limits.h, use it.
 */
#ifdef PATH_MAX
  maxlen = PATH_MAX;
/*
 * If we have pathconf, use it.
 */
#elif defined(_PC_PATH_MAX)
  errno = 0;
  maxlen = pathconf(FS_ROOT_DIR, _PC_PATH_MAX);
  if(maxlen <= 0 || errno)
    maxlen = MAX_PATHLEN_FALLBACK;
/*
 * None of the above approaches worked, so substitute our fallback
 * guess.
 */
#else
    maxlen = MAX_PATHLEN_FALLBACK;
#endif
/*
 * Return the amount of space needed to accomodate a pathname plus
 * a terminating '\0'.
 */
  return maxlen + 1;
}

/*.......................................................................
 * Return non-zero if the specified path name refers to a directory.
 *
 * Input:
 *  pathname  const char *  The path to test.
 * Output:
 *  return           int    0 - Not a directory.
 *                          1 - pathname[] refers to a directory.
 */
int _pu_path_is_dir(const char *pathname)
{
  struct stat statbuf;    /* The file-statistics return buffer */
/*
 * Look up the file attributes.
 */
  if(stat(pathname, &statbuf) < 0)
    return 0;
/*
 * Is the file a directory?
 */
  return S_ISDIR(statbuf.st_mode) != 0;
}

/*.......................................................................
 * Return non-zero if the specified path name refers to a regular file.
 *
 * Input:
 *  pathname  const char *  The path to test.
 * Output:
 *  return           int    0 - Not a regular file.
 *                          1 - pathname[] refers to a regular file.
 */
int _pu_path_is_file(const char *pathname)
{
  struct stat statbuf;    /* The file-statistics return buffer */
/*
 * Look up the file attributes.
 */
  if(stat(pathname, &statbuf) < 0)
    return 0;
/*
 * Is the file a regular file?
 */
  return S_ISREG(statbuf.st_mode) != 0;
}

/*.......................................................................
 * Return non-zero if the specified path name refers to an executable.
 *
 * Input:
 *  pathname  const char *  The path to test.
 * Output:
 *  return           int    0 - Not an executable file.
 *                          1 - pathname[] refers to an executable file.
 */
int _pu_path_is_exe(const char *pathname)
{
  struct stat statbuf;    /* The file-statistics return buffer */
/*
 * Look up the file attributes.
 */
  if(stat(pathname, &statbuf) < 0)
    return 0;
/*
 * Is the file a regular file which is executable by the current user.
 */
  return S_ISREG(statbuf.st_mode) != 0 &&
    (statbuf.st_mode & (S_IXOTH | S_IXGRP | S_IXUSR)) &&
    access(pathname, X_OK) == 0;
}

/*.......................................................................
 * Search backwards for the potential start of a filename. This
 * looks backwards from the specified index in a given string,
 * stopping at the first unescaped space or the start of the line.
 *
 * Input:
 *  string  const char *  The string to search backwards in.
 *  back_from      int    The index of the first character in string[]
 *                        that follows the pathname.
 * Output:
 *  return        char *  The pointer to the first character of
 *                        the potential pathname, or NULL on error.
 */
char *_pu_start_of_path(const char *string, int back_from)
{
  int i, j;
/*
 * Check the arguments.
 */
  if(!string || back_from < 0) {
    errno = EINVAL;
    return NULL;
  };
/*
 * Search backwards from the specified index.
 */
  for(i=back_from-1; i>=0; i--) {
    int c = string[i];
/*
 * Stop on unescaped spaces.
 */
    if(isspace((int)(unsigned char)c)) {
/*
 * The space can't be escaped if we are at the start of the line.
 */
      if(i==0)
        break;
/*
 * Find the extent of the escape characters which precedes the space.
 */
      for(j=i-1; j>=0 && string[j]=='\\'; j--)
	;
/*
 * If there isn't an odd number of escape characters before the space,
 * then the space isn't escaped.
 */
      if((i - 1 - j) % 2 == 0)
	break;
    };
  };
  return (char *)string + i + 1;
}

/*.......................................................................
 * Find the length of a potential filename starting from a given
 * point. This looks forwards from the specified index in a given string,
 * stopping at the first unescaped space or the end of the line.
 *
 * Input:
 *  string   const char *  The string to search backwards in.
 *  start_from      int    The index of the first character of the pathname
 *                         in string[].
 * Output:
 *  return         char *  The pointer to the character that follows
 *                         the potential pathname, or NULL on error.
 */
char *_pu_end_of_path(const char *string, int start_from)
{
  int c;             /* The character being examined */
  int escaped = 0;   /* True when the next character is escaped */
  int i;
/*
 * Check the arguments.
 */
  if(!string || start_from < 0) {
    errno = EINVAL;
    return NULL;
  };
/*
 * Search forwards from the specified index.
 */
  for(i=start_from; (c=string[i]) != '\0'; i++) {
    if(escaped) {
      escaped = 0;
    } else if(isspace(c)) {
      break;
    } else if(c == '\\') {
      escaped = 1;
    };
  };
  return (char *)string + i;
}

/*.......................................................................
 * Return non-zero if the specified path name refers to an existing file.
 *
 * Input:
 *  pathname   const char *  The path to test.
 * Output:
 *  return            int    0 - The file doesn't exist.
 *                           1 - The file does exist.
 */
int _pu_file_exists(const char *pathname)
{
  struct stat statbuf;
  return stat(pathname, &statbuf) == 0;
}

#endif  /* ifndef WITHOUT_FILE_SYSTEM */
