/*
 * File/directory test program for CUPS.
 *
 * Copyright © 2020-2024 by OpenPrinting.
 * Copyright © 2007-2018 by Apple Inc.
 * Copyright © 1997-2007 by Easy Software Products.
 *
 * Licensed under Apache License v2.0.  See the file "LICENSE" for more
 * information.
 */

/*
 * Include necessary headers...
 */

#include "string-private.h"
#include "debug-private.h"
#include "file.h"
#include "dir.h"
#include <stdlib.h>
#include <time.h>
#ifdef _WIN32
#  include <io.h>
#else
#  include <unistd.h>
#endif /* _WIN32 */
#include <fcntl.h>


/*
 * Local functions...
 */

static int	count_lines(cups_file_t *fp);
static int	random_tests(void);
static int	read_write_tests(int compression);


/*
 * 'main()' - Main entry.
 */

int					/* O - Exit status */
main(int  argc,				/* I - Number of command-line arguments */
     char *argv[])			/* I - Command-line arguments */
{
  int		status;			/* Exit status */
  int		i;			/* Looping var */
  char		filename[1024];		/* Filename buffer */
  cups_file_t	*fp;			/* File pointer */
#ifndef _WIN32
  int		fds[2];			/* Open file descriptors */
  cups_file_t	*fdfile;		/* File opened with cupsFileOpenFd() */
#endif /* !_WIN32 */
  int		count;			/* Number of lines in file */


  if (argc == 1)
  {
   /*
    * Do uncompressed file tests...
    */

    status = read_write_tests(0);

#ifdef HAVE_LIBZ
   /*
    * Do compressed file tests...
    */

    putchar('\n');

    status += read_write_tests(1);
#endif /* HAVE_LIBZ */

   /*
    * Do uncompressed random I/O tests...
    */

    status += random_tests();

#ifndef _WIN32
   /*
    * Test fdopen and close without reading...
    */

    pipe(fds);
    close(fds[1]);

    fputs("\ncupsFileOpenFd(fd, \"r\"): ", stdout);
    fflush(stdout);

    if ((fdfile = cupsFileOpenFd(fds[0], "r")) == NULL)
    {
      puts("FAIL");
      status ++;
    }
    else
    {
     /*
      * Able to open file, now close without reading.  If we don't return
      * before the alarm fires, that is a failure and we will crash on the
      * alarm signal...
      */

      puts("PASS");
      fputs("cupsFileClose(no read): ", stdout);
      fflush(stdout);

      alarm(5);
      cupsFileClose(fdfile);
      alarm(0);

      puts("PASS");
    }
#endif /* !_WIN32 */

   /*
    * Count lines in test file, rewind, then count again.
    */

    fputs("\ncupsFileOpen(\"testfile.txt\", \"r\"): ", stdout);

    if ((fp = cupsFileOpen("testfile.txt", "r")) == NULL)
    {
      puts("FAIL");
      status ++;
    }
    else
    {
      puts("PASS");
      fputs("cupsFileGets: ", stdout);

      if ((count = count_lines(fp)) != 477)
      {
        printf("FAIL (got %d lines, expected 477)\n", count);
	status ++;
      }
      else
      {
        puts("PASS");
	fputs("cupsFileRewind: ", stdout);

	if (cupsFileRewind(fp) != 0)
	{
	  puts("FAIL");
	  status ++;
	}
	else
	{
	  puts("PASS");
	  fputs("cupsFileGets: ", stdout);

	  if ((count = count_lines(fp)) != 477)
	  {
	    printf("FAIL (got %d lines, expected 477)\n", count);
	    status ++;
	  }
	  else
	    puts("PASS");
        }
      }

      cupsFileClose(fp);
    }

   /*
    * Test path functions...
    */

    fputs("\ncupsFileFind: ", stdout);
#ifdef _WIN32
    if (cupsFileFind("notepad.exe", "C:/WINDOWS", 1, filename, sizeof(filename)) &&
	cupsFileFind("notepad.exe", "C:/WINDOWS;C:/WINDOWS/SYSTEM32", 1, filename, sizeof(filename)))
#else
    if (cupsFileFind("cat", "/bin", 1, filename, sizeof(filename)) &&
	cupsFileFind("cat", "/bin:/usr/bin", 1, filename, sizeof(filename)))
#endif /* _WIN32 */
      printf("PASS (%s)\n", filename);
    else
    {
      puts("FAIL");
      status ++;
    }

   /*
    * Test directory functions...
    */

    fputs("\nCreating test directory \"test.d\"...\n", stdout);
    fputs("mkdir(test.d): ", stdout);
    if (mkdir("test.d", 0777))
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }
    else
    {
      int		num_files;	// Number of files seen
      cups_dir_t	*dir;		// Directory pointer
      cups_dentry_t	*dent;		// Directory entry

      puts("PASS");

      fputs("cupsDirOpen(test.d): ", stdout);
      if ((dir = cupsDirOpen("test.d")) == NULL)
      {
        printf("FAIL (%s)\n", strerror(errno));
        status ++;
      }
      else
      {
        puts("PASS");
        fputs("cupsDirRead: ", stdout);
        if ((dent = cupsDirRead(dir)) != NULL)
        {
          printf("FAIL (Got '%s', expected NULL)\n", dent->filename);
          status ++;
        }
        else
          puts("PASS");

        cupsDirClose(dir);
      }

      // Create some files...
      for (i = 0; i < 10; i ++)
      {
        snprintf(filename, sizeof(filename), "test.d/testfile%d.txt", i);
        printf("cupsFileOpen(%s): ", filename);
        if ((fp = cupsFileOpen(filename, "w")) == NULL)
        {
          printf("FAIL (%s)\n", strerror(errno));
          status ++;
          break;
        }
        else
        {
          puts("PASS");
          cupsFilePuts(fp, "This is a test.\n");
          cupsFileClose(fp);
        }
      }

      if (i >= 10)
      {
	fputs("cupsDirOpen(test.d): ", stdout);
	if ((dir = cupsDirOpen("test.d")) == NULL)
	{
	  printf("FAIL (%s)\n", strerror(errno));
	  status ++;
	}
	else
	{
	  puts("PASS");
	  fputs("cupsDirRead: ", stdout);
	  for (num_files = 0; (dent = cupsDirRead(dir)) != NULL; num_files ++)
	    printf("%s ", dent->filename);
	  if (num_files != 10)
	  {
	    printf("FAIL (Got %d files, expected 10)\n", num_files);
	    status ++;
	  }
	  else
	    puts("PASS");

	  cupsDirClose(dir);
	}
      }

      // Cleanup
      for (i = 0; i < 10; i ++)
      {
        snprintf(filename, sizeof(filename), "test.d/testfile%d.txt", i);
        unlink(filename);
      }

      rmdir("test.d");
    }

   /*
    * Summarize the results and return...
    */

    if (!status)
      puts("\nALL TESTS PASSED!");
    else
      printf("\n%d TEST(S) FAILED!\n", status);
  }
  else
  {
   /*
    * Cat the filename on the command-line...
    */

    char	line[8192];		/* Line from file */

    if ((fp = cupsFileOpen(argv[1], "r")) == NULL)
    {
      perror(argv[1]);
      status = 1;
    }
    else if (argc == 2)
    {
      status = 0;

      while (cupsFileGets(fp, line, sizeof(line)))
        puts(line);

      if (!cupsFileEOF(fp))
        perror(argv[1]);

      cupsFileClose(fp);
    }
    else
    {
      status = 0;
      ssize_t bytes;

      while ((bytes = cupsFileRead(fp, line, sizeof(line))) > 0)
        printf("%s: %d bytes\n", argv[1], (int)bytes);

      if (cupsFileEOF(fp))
        printf("%s: EOF\n", argv[1]);
      else
        perror(argv[1]);

      cupsFileClose(fp);
    }
  }

  return (status);
}


/*
 * 'count_lines()' - Count the number of lines in a file.
 */

static int				/* O - Number of lines */
count_lines(cups_file_t *fp)		/* I - File to read from */
{
  int	count;				/* Number of lines */
  char	line[1024];			/* Line buffer */


  for (count = 0; cupsFileGets(fp, line, sizeof(line)); count ++);

  return (count);
}


/*
 * 'random_tests()' - Do random access tests.
 */

static int				/* O - Status */
random_tests(void)
{
  int		status,			/* Status of tests */
		pass,			/* Current pass */
		count,			/* Number of records read */
		record,			/* Current record */
		num_records;		/* Number of records */
  off_t		pos;			/* Position in file */
  ssize_t	expected;		/* Expected position in file */
  cups_file_t	*fp;			/* File */
  char		buffer[512];		/* Data buffer */


 /*
  * Run 4 passes, each time appending to a data file and then reopening the
  * file for reading to validate random records in the file.
  */

  for (status = 0, pass = 0; pass < 4; pass ++)
  {
   /*
    * cupsFileOpen(append)
    */

    printf("\ncupsFileOpen(append %d): ", pass);

    if ((fp = cupsFileOpen("testfile.dat", "a")) == NULL)
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
      break;
    }
    else
      puts("PASS");

   /*
    * cupsFileTell()
    */

    expected = 256 * (ssize_t)sizeof(buffer) * pass;

    fputs("cupsFileTell(): ", stdout);
    if ((pos = cupsFileTell(fp)) != (off_t)expected)
    {
      printf("FAIL (" CUPS_LLFMT " instead of " CUPS_LLFMT ")\n",
	     CUPS_LLCAST pos, CUPS_LLCAST expected);
      status ++;
      break;
    }
    else
      puts("PASS");

   /*
    * cupsFileWrite()
    */

    fputs("cupsFileWrite(256 512-byte records): ", stdout);
    for (record = 0; record < 256; record ++)
    {
      memset(buffer, record, sizeof(buffer));
      if (cupsFileWrite(fp, buffer, sizeof(buffer)) < (ssize_t)sizeof(buffer))
        break;
    }

    if (record < 256)
    {
      printf("FAIL (%d: %s)\n", record, strerror(errno));
      status ++;
      break;
    }
    else
      puts("PASS");

   /*
    * cupsFileTell()
    */

    expected += 256 * (ssize_t)sizeof(buffer);

    fputs("cupsFileTell(): ", stdout);
    if ((pos = cupsFileTell(fp)) != (off_t)expected)
    {
      printf("FAIL (" CUPS_LLFMT " instead of " CUPS_LLFMT ")\n",
             CUPS_LLCAST pos, CUPS_LLCAST expected);
      status ++;
      break;
    }
    else
      puts("PASS");

    cupsFileClose(fp);

   /*
    * cupsFileOpen(read)
    */

    printf("\ncupsFileOpen(read %d): ", pass);

    if ((fp = cupsFileOpen("testfile.dat", "r")) == NULL)
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
      break;
    }
    else
      puts("PASS");

   /*
    * cupsFileSeek, cupsFileRead
    */

    fputs("cupsFileSeek(), cupsFileRead(): ", stdout);

    for (num_records = (pass + 1) * 256, count = (pass + 1) * 256, record = ((int)CUPS_RAND() & 65535) % num_records;
         count > 0;
	 count --, record = (record + ((int)CUPS_RAND() & 31) - 16 + num_records) % num_records)
    {
     /*
      * The last record is always the first...
      */

      if (count == 1)
        record = 0;

     /*
      * Try reading the data for the specified record, and validate the
      * contents...
      */

      expected = (ssize_t)sizeof(buffer) * record;

      if ((pos = cupsFileSeek(fp, expected)) != expected)
      {
        printf("FAIL (" CUPS_LLFMT " instead of " CUPS_LLFMT ")\n",
	       CUPS_LLCAST pos, CUPS_LLCAST expected);
        status ++;
	break;
      }
      else
      {
	if (cupsFileRead(fp, buffer, sizeof(buffer)) != sizeof(buffer))
	{
	  printf("FAIL (%s)\n", strerror(errno));
	  status ++;
	  break;
	}
	else if ((buffer[0] & 255) != (record & 255) ||
	         memcmp(buffer, buffer + 1, sizeof(buffer) - 1))
	{
	  printf("FAIL (Bad Data - %d instead of %d)\n", buffer[0] & 255,
	         record & 255);
	  status ++;
	  break;
	}
      }
    }

    if (count == 0)
      puts("PASS");

    cupsFileClose(fp);
  }

 /*
  * Remove the test file...
  */

  unlink("testfile.dat");

 /*
  * Return the test status...
  */

  return (status);
}


/*
 * 'read_write_tests()' - Perform read/write tests.
 */

static int				/* O - Status */
read_write_tests(int compression)	/* I - Use compression? */
{
  int		i;			/* Looping var */
  cups_file_t	*fp;			/* File */
  int		status;			/* Exit status */
  char		line[1024],		/* Line from file */
		*value;			/* Directive value from line */
  int		linenum;		/* Line number */
  unsigned char	readbuf[8192],		/* Read buffer */
		writebuf[8192];		/* Write buffer */
  int		byte;			/* Byte from file */
  ssize_t	bytes;			/* Number of bytes read/written */
  off_t		length;			/* Length of file */
  static const char *partial_line = "partial line";
					/* Partial line */
  static const char * const values[] =	/* cupsFileGet/PutConf values */
  {
    "simple",
    "'single-quoted value'",
    "\"double-quoted value\"",
    "value with # comment",
    "value with \n newline",
    "value with \r carriage return",
    "value with \\ backslash",
    "pathological\r\nvalue\\#with comment"
  };


 /*
  * No errors so far...
  */

  status = 0;

 /*
  * Initialize the write buffer with random data...
  */

  CUPS_SRAND((unsigned)time(NULL));

  for (i = 0; i < (int)sizeof(writebuf); i ++)
    writebuf[i] = (unsigned char)CUPS_RAND();

 /*
  * cupsFileOpen(write)
  */

  printf("cupsFileOpen(write%s): ", compression ? " compressed" : "");

  fp = cupsFileOpen(compression ? "testfile.dat.gz" : "testfile.dat",
                    compression ? "w9" : "w");
  if (fp)
  {
    puts("PASS");

   /*
    * cupsFileCompression()
    */

    fputs("cupsFileCompression(): ", stdout);

    if (cupsFileCompression(fp) == compression)
      puts("PASS");
    else
    {
      printf("FAIL (Got %d, expected %d)\n", cupsFileCompression(fp),
             compression);
      status ++;
    }

   /*
    * cupsFilePuts()
    */

    fputs("cupsFilePuts(): ", stdout);

    if (cupsFilePuts(fp, "# Hello, World\n") > 0)
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFilePrintf()
    */

    fputs("cupsFilePrintf(): ", stdout);

    for (i = 0; i < 1000; i ++)
      if (cupsFilePrintf(fp, "TestLine %03d\n", i) < 0)
        break;

    if (i >= 1000)
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFilePutConf()
    */

    fputs("cupsFilePutConf(): ", stdout);
    for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); i ++)
    {
      if (cupsFilePutConf(fp, "TestConf", values[i]) < 0)
        break;
    }

    if (i >= (int)(sizeof(values) / sizeof(values[0])))
    {
      puts("PASS");
    }
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFilePutChar()
    */

    fputs("cupsFilePutChar(): ", stdout);

    for (i = 0; i < 256; i ++)
      if (cupsFilePutChar(fp, i) < 0)
        break;

    if (i >= 256)
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFileWrite()
    */

    fputs("cupsFileWrite(): ", stdout);

    for (i = 0; i < 10000; i ++)
      if (cupsFileWrite(fp, (char *)writebuf, sizeof(writebuf)) < 0)
        break;

    if (i >= 10000)
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFilePuts() with partial line...
    */

    fputs("cupsFilePuts(\"partial line\"): ", stdout);

    if (cupsFilePuts(fp, partial_line) > 0)
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFileTell()
    */

    fputs("cupsFileTell(): ", stdout);

    if ((length = cupsFileTell(fp)) == 81933542)
      puts("PASS");
    else
    {
      printf("FAIL (" CUPS_LLFMT " instead of 81933542)\n", CUPS_LLCAST length);
      status ++;
    }

   /*
    * cupsFileClose()
    */

    fputs("cupsFileClose(): ", stdout);

    if (!cupsFileClose(fp))
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }
  }
  else
  {
    printf("FAIL (%s)\n", strerror(errno));
    status ++;
  }

 /*
  * cupsFileOpen(read)
  */

  fputs("\ncupsFileOpen(read): ", stdout);

  fp = cupsFileOpen(compression ? "testfile.dat.gz" : "testfile.dat", "r");
  if (fp)
  {
    puts("PASS");

   /*
    * cupsFileGets()
    */

    fputs("cupsFileGets(): ", stdout);

    if (cupsFileGets(fp, line, sizeof(line)))
    {
      if (line[0] == '#')
        puts("PASS");
      else
      {
        printf("FAIL (Got line \"%s\", expected comment line)\n", line);
	status ++;
      }
    }
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFileCompression()
    */

    fputs("cupsFileCompression(): ", stdout);

    if (cupsFileCompression(fp) == compression)
      puts("PASS");
    else
    {
      printf("FAIL (Got %d, expected %d)\n", cupsFileCompression(fp),
             compression);
      status ++;
    }

   /*
    * cupsFileGetConf()
    */

    linenum = 1;

    fputs("cupsFileGetConf(TestLine): ", stdout);

    for (i = 0, value = NULL; i < 1000; i ++)
      if (!cupsFileGetConf(fp, line, sizeof(line), &value, &linenum))
        break;
      else if (_cups_strcasecmp(line, "TestLine") || !value || atoi(value) != i ||
               linenum != (i + 2))
        break;

    if (i >= 1000)
      puts("PASS");
    else if (line[0])
    {
      printf("FAIL (Line %d, directive \"%s\", value \"%s\")\n", linenum,
             line, value ? value : "(null)");
      status ++;
    }
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

    fputs("cupsFileGetConf(TestConf): ", stdout);

    for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); i ++)
    {
      if (!cupsFileGetConf(fp, line, sizeof(line), &value, &linenum))
      {
        printf("FAIL (%s)\n", strerror(errno));
        status ++;
        break;
      }
      else if (_cups_strcasecmp(line, "TestConf") || !value || strcmp(value, values[i]))
      {
        printf("FAIL (Expected 'TestConf %s', got '%s %s')\n", values[i], line, value);
        status ++;
        break;
      }
    }

    if (i >= (int)(sizeof(values) / sizeof(values[0])))
      puts("PASS");

   /*
    * cupsFileGetChar()
    */

    fputs("cupsFileGetChar(): ", stdout);

    for (i = 0, byte = 0; i < 256; i ++)
      if ((byte = cupsFileGetChar(fp)) != i)
        break;

    if (i >= 256)
      puts("PASS");
    else if (byte >= 0)
    {
      printf("FAIL (Got %d, expected %d)\n", byte, i);
      status ++;
    }
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFileRead()
    */

    fputs("cupsFileRead(): ", stdout);

    for (i = 0, bytes = 0; i < 10000; i ++)
      if ((bytes = cupsFileRead(fp, (char *)readbuf, sizeof(readbuf))) < 0)
        break;
      else if (memcmp(readbuf, writebuf, sizeof(readbuf)))
        break;

    if (i >= 10000)
      puts("PASS");
    else if (bytes > 0)
    {
      printf("FAIL (Pass %d, ", i);

      for (i = 0; i < (int)sizeof(readbuf); i ++)
        if (readbuf[i] != writebuf[i])
	  break;

      printf("match failed at offset %d - got %02X, expected %02X)\n",
             i, readbuf[i], writebuf[i]);
    }
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }

   /*
    * cupsFileGetChar() with partial line...
    */

    fputs("cupsFileGetChar(partial line): ", stdout);

    for (i = 0; i < (int)strlen(partial_line); i ++)
      if ((byte = cupsFileGetChar(fp)) < 0)
        break;
      else if (byte != partial_line[i])
        break;

    if (!partial_line[i])
      puts("PASS");
    else
    {
      printf("FAIL (got '%c', expected '%c')\n", byte, partial_line[i]);
      status ++;
    }

   /*
    * cupsFileTell()
    */

    fputs("cupsFileTell(): ", stdout);

    if ((length = cupsFileTell(fp)) == 81933542)
      puts("PASS");
    else
    {
      printf("FAIL (" CUPS_LLFMT " instead of 81933542)\n", CUPS_LLCAST length);
      status ++;
    }

   /*
    * cupsFileClose()
    */

    fputs("cupsFileClose(): ", stdout);

    if (!cupsFileClose(fp))
      puts("PASS");
    else
    {
      printf("FAIL (%s)\n", strerror(errno));
      status ++;
    }
  }
  else
  {
    printf("FAIL (%s)\n", strerror(errno));
    status ++;
  }

 /*
  * Remove the test file...
  */

  if (!status)
    unlink(compression ? "testfile.dat.gz" : "testfile.dat");

 /*
  * Return the test status...
  */

  return (status);
}
