/* dump_setup.cc

This file is part of Cygwin.

This software is a copyrighted work licensed under the terms of the
Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
details. */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <io.h>
#include <sys/stat.h>
#include <errno.h>
#define WIN32_NO_STATUS	/* Disable status codes in winnt.h since we include
			   ntstatus.h for extended status codes below. */
#include <windows.h>
#undef WIN32_NO_STATUS
#include <winternl.h>
#include <ntstatus.h>
#include "path.h"
#include "cygcheck.h"
#include <zlib.h>

static int package_len = 20;
static unsigned int version_len = 10;


typedef struct
{
  char pkgtar[MAX_PATH + 1];
  char pkg[MAX_PATH + 1];
  char ver[MAX_PATH + 1];
  char tail[MAX_PATH + 1];
  char what[16];
} fileparse;

static int
find_tar_ext (const char *path)
{
  char *p = strchr (path, '\0') - 9;
  if (p <= path)
    return 0;
  if ((p = strstr (p, ".tar")) != NULL)
    return p - path;
  else
    return 0;
}

/* Parse a filename into package, version, and extension components. */
int
parse_filename (const char *in_fn, fileparse& f)
{
  char *p, *ver;
  char fn[strlen (in_fn) + 1];

  strcpy (fn, in_fn);
  int n = find_tar_ext (fn);

  if (!n)
    return 0;

  strcpy (f.tail, fn + n);
  fn[n] = '\0';
  f.pkg[0] = f.what[0] = '\0';
  p = fn;
  for (ver = p; *ver; ver++)
    if (*ver != '-')
      continue;
    else if (isdigit (ver[1]))
      {
	*ver++ = '\0';
	strcpy (f.pkg, p);
	break;
      }
    else if (strcasecmp (ver, "-src") == 0 ||
	     strcasecmp (ver, "-patch") == 0)
      {
	size_t len;

	*ver++ = '\0';
	strcpy (f.pkg, p);
	strcpy (f.what, strlwr (ver));
	strcpy (f.pkgtar, p);
	len = strlen (f.pkgtar);
	strncpy (f.pkgtar + len, f.tail, sizeof (f.pkgtar) - len);
	f.pkgtar[MAX_PATH] = '\0';
	ver = strchr (ver, '\0');
	break;
      }

  if (!f.pkg[0])
    strcpy (f.pkg, p);

  if (!f.what[0])
    {
      int n;
      p = strchr (ver, '\0');
      strcpy (f.pkgtar, in_fn);
      if ((p -= 4) >= ver && strcasecmp (p, "-src") == 0)
	n = 4;
      else if ((p -= 2) >= ver && strcasecmp (p, "-patch") == 0)
	n = 6;
      else
	n = 0;
      if (n)
	{
	  strcpy (f.what, p + 1);
	  *p = '\0';
	  p = f.pkgtar + (p - fn) + n;
	  memmove (p - 4, p, strlen (p));
	}
    }

  strcpy (f.ver, *ver ? ver : "0.0");
  return 1;
}

static bool
dump_file (const char *msg, const char *fn)
{
  char buf[4096];
  bool printed = false;
  bool found = false;
  size_t len = strlen (fn);
  char *path = cygpath ("/etc/setup/setup.rc", NULL);
  FILE *fp = fopen (path, "rt");

  if (fp)
    {
      while (fgets (buf, 4096, fp))
      	{
	  if (found)
	    {
	      char *bufp = buf;

	      if (*bufp == '\t')
	      	++bufp;
	      char *p = strchr (bufp, '\0');
	      printf ("%s%s%s", msg, bufp,
				(p == bufp) || p[-1] != '\n' ? "\n" : "");
	      printed = true;
	      break;
	    }
	  if (!strncmp (buf, fn, len) && buf[len] == '\n')
	    found = true;
	}
      fclose (fp);
    }
  return printed;
}

extern "C" {
int
compar (const void *a, const void *b)
{
  const pkgver *pa = (const pkgver *) a;
  const pkgver *pb = (const pkgver *) b;
  return strcasecmp (pa->name, pb->name);
}
}

int
match_argv (char **argv, const char *name)
{
  if (!argv || !*argv)
    return -1;
  for (char **a = argv; *a; a++)
    if (strcasecmp (*a, name) == 0)
      return a - argv + 1;
  return 0;
}

static bool
could_not_access (int verbose, char *filename, char *package, const char *type)
{
  switch (errno)
    {
      case ENOTDIR:
	break;
      case ENOENT:
	if (verbose)
	  printf ("Missing %s: /%s from package %s\n",
		  type, filename, package);
	return true;
      case EACCES:
	if (verbose)
	  printf ("Unable to access %s /%s from package %s\n",
		  type, filename, package);
	return true;
    }
  return false;
}

static const WCHAR tfx_chars[] = {
	    0, 0xf000 |   1, 0xf000 |   2, 0xf000 |   3,
 0xf000 |   4, 0xf000 |   5, 0xf000 |   6, 0xf000 |   7,
 0xf000 |   8, 0xf000 |   9, 0xf000 |  10, 0xf000 |  11,
 0xf000 |  12, 0xf000 |  13, 0xf000 |  14, 0xf000 |  15,
 0xf000 |  16, 0xf000 |  17, 0xf000 |  18, 0xf000 |  19,
 0xf000 |  20, 0xf000 |  21, 0xf000 |  22, 0xf000 |  23,
 0xf000 |  24, 0xf000 |  25, 0xf000 |  26, 0xf000 |  27,
 0xf000 |  28, 0xf000 |  29, 0xf000 |  30, 0xf000 |  31,
	  ' ',          '!', 0xf000 | '"',          '#',
	  '$',          '%',          '&',           39,
	  '(',          ')', 0xf000 | '*',          '+',
	  ',',          '-',          '.',          '\\',
	  '0',          '1',          '2',          '3',
	  '4',          '5',          '6',          '7',
	  '8',          '9', 0xf000 | ':',          ';',
 0xf000 | '<',          '=', 0xf000 | '>', 0xf000 | '?',
	  '@',          'A',          'B',          'C',
	  'D',          'E',          'F',          'G',
	  'H',          'I',          'J',          'K',
	  'L',          'M',          'N',          'O',
	  'P',          'Q',          'R',          'S',
	  'T',          'U',          'V',          'W',
	  'X',          'Y',          'Z',          '[',
	  '\\',          ']',          '^',          '_',
	  '`',          'a',          'b',          'c',
	  'd',          'e',          'f',          'g',
	  'h',          'i',          'j',          'k',
	  'l',          'm',          'n',          'o',
	  'p',          'q',          'r',          's',
	  't',          'u',          'v',          'w',
	  'x',          'y',          'z',          '{',
 0xf000 | '|',          '}',          '~',          127
};

static void
transform_chars (PWCHAR path, PWCHAR path_end)
{
  for (; path <= path_end; ++path)
    if (*path < 128)
      *path = tfx_chars[*path];
}

extern "C" NTAPI NTSTATUS NtQueryAttributesFile (POBJECT_ATTRIBUTES,
						 PFILE_BASIC_INFORMATION);

/* This function checks for file existance and fills the stat structure
   with only the required mode info.  We're using a native NT function
   here, otherwise we wouldn't be able to check for files with special
   characters not valid in Win32, and espacially not valid using the
   ANSI API. */
static int
simple_nt_stat (const char *filename, struct stat *st)
{
  size_t len = mbstowcs (NULL, filename, 0) + 1;
  WCHAR path[len + 8];	/* Enough space for the NT prefix */
  PWCHAR p = path;
  UNICODE_STRING upath;
  OBJECT_ATTRIBUTES attr;
  FILE_BASIC_INFORMATION fbi;
  NTSTATUS status;

  wcscpy (p, L"\\??\\");
  p += 4;
  if (filename[0] == '\\' && filename[1] == '\\')
    {
      wcscpy (p, L"UNC");
      p += 3;
      p += mbstowcs (p, filename + 1, len);
    }
  else
    p += mbstowcs (p, filename, len);
  /* Remove trailing backslashes.  NT functions don't like them. */
  if (p[-1] == L'\\')
    *--p = L'\0';
  /* Skip prefix and drive, otherwise question marks and colons are converted
     as well. */
  transform_chars (path + 7, p);
  RtlInitUnicodeString (&upath, path);
  InitializeObjectAttributes (&attr, &upath, OBJ_CASE_INSENSITIVE, NULL, NULL);
  status = NtQueryAttributesFile (&attr, &fbi);
  if (NT_SUCCESS (status))
    {
      st->st_mode = (fbi.FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
		    ? S_IFDIR : S_IFREG;
      return 0;
    }
  if (status == STATUS_OBJECT_PATH_NOT_FOUND
      || status == STATUS_OBJECT_NAME_INVALID
      || status == STATUS_BAD_NETWORK_PATH
      || status == STATUS_BAD_NETWORK_NAME
      || status == STATUS_NO_MEDIA_IN_DEVICE
      || status == STATUS_OBJECT_NAME_NOT_FOUND
      || status == STATUS_NO_SUCH_FILE)
    errno = ENOENT;
  else
    errno = EACCES;
  return -1;
}

static bool
directory_exists (int verbose, char *filename, char *package)
{
  struct stat status;
  if (simple_nt_stat(cygpath("/", filename, NULL), &status))
    {
      if (could_not_access (verbose, filename, package, "directory"))
	return false;
    }
  else if (!S_ISDIR(status.st_mode))
    {
      if (verbose)
	printf ("Directory/file mismatch: /%s from package %s\n", filename, package);
      return false;
    }
  return true;
}

static bool
file_exists (int verbose, char *filename, const char *alt, char *package)
{
  struct stat status;
  if (simple_nt_stat(cygpath("/", filename, NULL), &status) &&
      (!alt || simple_nt_stat(cygpath("/", filename, alt, NULL), &status)))
    {
      if (could_not_access (verbose, filename, package, "file"))
	return false;
    }
  else if (!S_ISREG(status.st_mode))
    {
      if (verbose)
	printf ("File type mismatch: /%s from package %s\n", filename, package);
      return false;
    }
  return true;
}

static gzFile
open_package_list (char *package)
{
  char filelist[MAX_PATH + 1] = "/etc/setup/";
  strcat (strcat (filelist, package), ".lst.gz");
  if (!file_exists (false, filelist + 1, NULL, NULL))
    return NULL;

  gzFile fp;
#ifndef ZLIB_VERSION
  fp = NULL;
#else
  char *fn = cygpath (filelist, NULL);
  fp = gzopen (fn, "rb9");
  free (fn);
#endif

  return fp;
}

static bool
check_package_files (int verbose, char *package)
{
  gzFile fp = open_package_list (package);
  if (!fp)
    {
      if (verbose)
	printf ("Empty package %s\n", package);
      return true;
    }

  bool result = true;
  char buf[MAX_PATH + 1];
  while (gzgets (fp, buf, MAX_PATH))
    {
      char *filename = strtok(buf, "\n");

      if (*filename == '/')
	++filename;
      else if (!strncmp (filename, "./", 2))
	filename += 2;

      if (filename[strlen (filename) - 1] == '/')
	{
	  if (!directory_exists (verbose, filename, package))
	    result = false;
	}
      else if (strstr (filename, "/postinstall/"))
	{
	  if (!file_exists (verbose, filename, ".done", package))
	    result = false;
	}
      else
	{
	  if (!file_exists (verbose, filename, ".lnk", package))
	    result = false;
	}
    }

  gzclose (fp);
  return result;
}

/**
 * Returns a calloc'd sorted list of packages or NULL if no info.
 * The last entry in the list is {NULL,NULL}.
 */
pkgver *
get_installed_packages (char **argv, size_t *count)
{
  char *setup = cygpath ("/etc/setup/installed.db", NULL);
  FILE *fp = fopen (setup, "rt");

  if (fp == NULL)
    return NULL;

  int nlines;
  nlines = 0;
  char buf[4096];
  while (fgets (buf, 4096, fp))
    nlines += 2;	/* potentially binary + source */
  if (!nlines)
    {
      fclose (fp);
      return NULL;
    }
  rewind (fp);

  pkgver *packages;

  packages = (pkgver *) calloc (nlines + 1, sizeof(packages[0]));
  int n;
  for (n = 0; fgets (buf, 4096, fp) && n < nlines;)
    {
      char *package = strtok (buf, " ");
      if (!package || !*package || !match_argv (argv, package))
	continue;
      for (int i = 0; i < 2; i++)
	{
	  fileparse f;
	  char *tar = strtok (NULL, " ");
	  if (!tar || !*tar || !parse_filename (tar, f))
	    break;

	  int len = strlen (package);
	  if (f.what[0])
	    len += strlen (f.what) + 1;
	  if (len > package_len)
	    package_len = len;
	  packages[n].name = (char *) malloc (len + 1);
	  strcpy (packages[n].name, package);
	  if (f.what[0])
	    strcat (strcat (packages[n].name, "-"), f.what);
	  packages[n].ver = strdup (f.ver);
	  if (strlen(f.ver) > version_len)
	    version_len = strlen(f.ver);
	  n++;
	  if (strtok (NULL, " ") == NULL)
	    break;
	}
    }

  packages[n].name = packages[n].ver = NULL;

  qsort (packages, n, sizeof (packages[0]), compar);

  fclose (fp);

  if (count)
    *count = n;

  return packages;
}

void
dump_setup (int verbose, char **argv, bool check_files, bool names_only)
{
  pkgver *packages = get_installed_packages (argv);

  if (!names_only)
    puts ("Cygwin Package Information");

  if (packages == NULL)
    {
      puts ("No setup information found");
      return;
    }

  if (verbose)
    {
      bool need_nl = dump_file ("Last downloaded files to: ", "last-cache");
      if (dump_file ("Last downloaded files from: ", "last-mirror") || need_nl)
	puts ("");
    }

  if (!names_only)
    printf ("%-*s %-*s%s\n", package_len, "Package",
	    check_files ? version_len : 7, "Version",
	    check_files ? "	Status" : "");
  for (int i = 0; packages[i].name; i++)
    {
      if (names_only)
	printf ("%s\n", packages[i].name);
      else if (check_files)
	printf ("%-*s %-*s%s\n", package_len, packages[i].name,
		version_len, packages[i].ver,
		check_package_files (verbose, packages[i].name)
		  ? "     OK" : "     Incomplete");
      else
	printf ("%-*s %s\n", package_len, packages[i].name, packages[i].ver);
      fflush(stdout);
    }

  free (packages);

  return;
}

void
package_list (int verbose, char **argv)
{
  pkgver *packages = get_installed_packages (argv);
  if (packages == NULL)
    {
      puts ("No setup information found");
      return;
    }

  for (int i = 0; packages[i].name; i++)
    {
      gzFile fp = open_package_list (packages[i].name);
      if (!fp)
	{
	  if (verbose)
	    printf ("Can't open file list /etc/setup/%s.lst.gz for package %s\n",
		packages[i].name, packages[i].name);
	  continue;
	}

      if (verbose)
	printf ("Package: %s-%s\n", packages[i].name, packages[i].ver);

      char buf[MAX_PATH + 1];
      while (gzgets (fp, buf, MAX_PATH))
	{
	  char *lastchar = strchr(buf, '\n');
	  if (lastchar[-1] != '/')
	    printf ("%s/%s", (verbose?"    ":""), buf);
	}

      gzclose (fp);
    }

  free (packages);

  return;
}

void
package_find (int verbose, char **argv)
{
  pkgver *packages = get_installed_packages (NULL);
  if (packages == NULL)
    {
      puts ("No setup information found");
      return;
    }

  for (int i = 0; packages[i].name; i++)
    {
      gzFile fp = open_package_list (packages[i].name);
      if (!fp)
	continue;

      char buf[MAX_PATH + 2];
      buf[0] = '/';
      while (gzgets (fp, buf + 1, MAX_PATH))
	{
	  char *filename = strtok(buf, "\n");
	  int flen = strlen (filename);
	  if (filename[flen - 1] != '/')
	    {
	      // FIXME: verify that /bin is mounted on /usr/bin; ditto for /lib
	      bool is_alias = !strncmp(filename, "/usr/bin/", 9) ||
			      !strncmp(filename, "/usr/lib/", 9);
	      int a = match_argv (argv, filename);
	      if (!a && is_alias)
		a = match_argv (argv, filename + 4);
	      if (!a && !strcmp(filename + flen - 4, ".exe"))
		{
		  filename[flen - 4] = '\0';
		  a = match_argv (argv, filename);
		}
	      if (!a && is_alias)
		a = match_argv (argv, filename + 4);
	      if (a > 0)
		{
		  if (verbose)
		    printf ("%s: found in package ", filename);
		  printf ("%s-%s\n", packages[i].name, packages[i].ver);
		}
	    }
	}

      gzclose (fp);
    }

  free (packages);

  return;
}

