/* timepng.c
 *
 * Copyright (c) 2013,2016 John Cunningham Bowler
 *
 * This code is released under the libpng license.
 * For conditions of distribution and use, see the disclaimer
 * and license in png.h
 *
 * Load an arbitrary number of PNG files (from the command line, or, if there
 * are no arguments on the command line, from stdin) then run a time test by
 * reading each file by row or by image (possibly with transforms in the latter
 * case).  The only output is a time as a floating point number of seconds with
 * 9 decimal digits.
 */
#define _POSIX_C_SOURCE 199309L /* for clock_gettime */

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

#include <time.h>

#if defined(HAVE_CONFIG_H) && !defined(PNG_NO_CONFIG_H)
#  include <config.h>
#endif

/* Define the following to use this test against your installed libpng, rather
 * than the one being built here:
 */
#ifdef PNG_FREESTANDING_TESTS
#  include <png.h>
#else
#  include "../../png.h"
#endif

/* The following is to support direct compilation of this file as C++ */
#ifdef __cplusplus
#  define voidcast(type, value) static_cast<type>(value)
#else
#  define voidcast(type, value) (value)
#endif /* __cplusplus */

/* 'CLOCK_PROCESS_CPUTIME_ID' is one of the clock timers for clock_gettime.  It
 * need not be supported even when clock_gettime is available.  It returns the
 * 'CPU' time the process has consumed.  'CPU' time is assumed to include time
 * when the CPU is actually blocked by a pending cache fill but not time
 * waiting for page faults.  The attempt is to get a measure of the actual time
 * the implementation takes to read a PNG ignoring the potentially very large IO
 * overhead.
 */
#if defined (CLOCK_PROCESS_CPUTIME_ID) && defined(PNG_STDIO_SUPPORTED) &&\
    defined(PNG_EASY_ACCESS_SUPPORTED) &&\
    (PNG_LIBPNG_VER >= 10700 ? defined(PNG_READ_PNG_SUPPORTED) :\
     defined (PNG_SEQUENTIAL_READ_SUPPORTED) &&\
     defined(PNG_INFO_IMAGE_SUPPORTED))

typedef struct
{
   FILE *input;
   FILE *output;
}  io_data;

static PNG_CALLBACK(void, read_and_copy,
      (png_structp png_ptr, png_bytep buffer, size_t cb))
{
   io_data *io = (io_data*)png_get_io_ptr(png_ptr);

   if (fread(buffer, cb, 1, io->input) != 1)
      png_error(png_ptr, strerror(errno));

   if (fwrite(buffer, cb, 1, io->output) != 1)
   {
      perror("temporary file");
      fprintf(stderr, "temporary file PNG write failed\n");
      exit(1);
   }
}

static void read_by_row(png_structp png_ptr, png_infop info_ptr,
      FILE *write_ptr, FILE *read_ptr)
{
   /* These don't get freed on error, this is fine; the program immediately
    * exits.
    */
   png_bytep row = NULL, display = NULL;
   io_data io_copy;

   if (write_ptr != NULL)
   {
      /* Set up for a copy to the temporary file: */
      io_copy.input = read_ptr;
      io_copy.output = write_ptr;
      png_set_read_fn(png_ptr, &io_copy, read_and_copy);
   }

   png_read_info(png_ptr, info_ptr);

   {
      size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);

      row = voidcast(png_bytep,malloc(rowbytes));
      display = voidcast(png_bytep,malloc(rowbytes));

      if (row == NULL || display == NULL)
         png_error(png_ptr, "OOM allocating row buffers");

      {
         png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
         int passes = png_set_interlace_handling(png_ptr);
         int pass;

         png_start_read_image(png_ptr);

         for (pass = 0; pass < passes; ++pass)
         {
            png_uint_32 y = height;

            /* NOTE: this trashes the row each time; interlace handling won't
             * work, but this avoids memory thrashing for speed testing and is
             * somewhat representative of an application that works row-by-row.
             */
            while (y-- > 0)
               png_read_row(png_ptr, row, display);
         }
      }
   }

   /* Make sure to read to the end of the file: */
   png_read_end(png_ptr, info_ptr);

   /* Free this up: */
   free(row);
   free(display);
}

static PNG_CALLBACK(void, no_warnings, (png_structp png_ptr,
         png_const_charp warning))
{
   (void)png_ptr;
   (void)warning;
}

static int read_png(FILE *fp, png_int_32 transforms, FILE *write_file)
{
   png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,0,0,
         no_warnings);
   png_infop info_ptr = NULL;

   if (png_ptr == NULL)
      return 0;

   if (setjmp(png_jmpbuf(png_ptr)))
   {
      png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
      return 0;
   }

#  ifdef PNG_BENIGN_ERRORS_SUPPORTED
      png_set_benign_errors(png_ptr, 1/*allowed*/);
#  endif
   png_init_io(png_ptr, fp);

   info_ptr = png_create_info_struct(png_ptr);

   if (info_ptr == NULL)
      png_error(png_ptr, "OOM allocating info structure");

   if (transforms < 0)
      read_by_row(png_ptr, info_ptr, write_file, fp);

   else
      png_read_png(png_ptr, info_ptr, transforms, NULL/*params*/);

   png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
   return 1;
}

static int mytime(struct timespec *t)
{
   /* Do the timing using clock_gettime and the per-process timer. */
   if (!clock_gettime(CLOCK_PROCESS_CPUTIME_ID, t))
      return 1;

   perror("CLOCK_PROCESS_CPUTIME_ID");
   fprintf(stderr, "timepng: could not get the time\n");
   return 0;
}

static int perform_one_test(FILE *fp, int nfiles, png_int_32 transforms)
{
   int i;
   struct timespec before, after;

   /* Clear out all errors: */
   rewind(fp);

   if (mytime(&before))
   {
      for (i=0; i<nfiles; ++i)
      {
         if (read_png(fp, transforms, NULL/*write*/))
         {
            if (ferror(fp))
            {
               perror("temporary file");
               fprintf(stderr, "file %d: error reading PNG data\n", i);
               return 0;
            }
         }

         else
         {
            perror("temporary file");
            fprintf(stderr, "file %d: error from libpng\n", i);
            return 0;
         }
      }
   }

   else
      return 0;

   if (mytime(&after))
   {
      /* Work out the time difference and print it - this is the only output,
       * so flush it immediately.
       */
      unsigned long s = after.tv_sec - before.tv_sec;
      long ns = after.tv_nsec - before.tv_nsec;

      if (ns < 0)
      {
         --s;
         ns += 1000000000;

         if (ns < 0)
         {
            fprintf(stderr, "timepng: bad clock from kernel\n");
            return 0;
         }
      }

      printf("%lu.%.9ld\n", s, ns);
      fflush(stdout);
      if (ferror(stdout))
      {
         fprintf(stderr, "timepng: error writing output\n");
         return 0;
      }

      /* Successful return */
      return 1;
   }

   else
      return 0;
}

static int add_one_file(FILE *fp, char *name)
{
   FILE *ip = fopen(name, "rb");

   if (ip != NULL)
   {
      /* Read the file using libpng; this detects errors and also deals with
       * files which contain data beyond the end of the file.
       */
      int ok = 0;
      fpos_t pos;

      if (fgetpos(fp, &pos))
      {
         /* Fatal error reading the start: */
         perror("temporary file");
         fprintf(stderr, "temporary file fgetpos error\n");
         exit(1);
      }

      if (read_png(ip, -1/*by row*/, fp/*output*/))
      {
         if (ferror(ip))
         {
            perror(name);
            fprintf(stderr, "%s: read error\n", name);
         }

         else
            ok = 1; /* read ok */
      }

      else
         fprintf(stderr, "%s: file not added\n", name);

      (void)fclose(ip);

      /* An error in the output is fatal; exit immediately: */
      if (ferror(fp))
      {
         perror("temporary file");
         fprintf(stderr, "temporary file write error\n");
         exit(1);
      }

      if (ok)
         return 1;

      /* Did not read the file successfully, simply rewind the temporary
       * file.  This must happen after the ferror check above to avoid clearing
       * the error.
       */
      if (fsetpos(fp, &pos))
      {
         perror("temporary file");
         fprintf(stderr, "temporary file fsetpos error\n");
         exit(1);
      }
   }

   else
   {
      /* file open error: */
      perror(name);
      fprintf(stderr, "%s: open failed\n", name);
   }

   return 0; /* file not added */
}

static void
usage(FILE *fp)
{
   if (fp != NULL) fclose(fp);

   fprintf(stderr,
"Usage:\n"
" timepng --assemble <assembly> {files}\n"
"  Read the files into <assembly>, output the count.  Options are ignored.\n"
" timepng --dissemble <assembly> <count> [options]\n"
"  Time <count> files from <assembly>, additional files may not be given.\n"
" Otherwise:\n"
"  Read the files into a temporary file and time the decode\n"
"Transforms:\n"
"  --by-image: read by image with png_read_png\n"
"  --<transform>: implies by-image, use PNG_TRANSFORM_<transform>\n"
"  Otherwise: read by row using png_read_row (to a single row buffer)\n"
   /* ISO C90 string length max 509 */);fprintf(stderr,
"{files}:\n"
"  PNG files to copy into the assembly and time.  Invalid files are skipped\n"
"  with appropriate error messages.  If no files are given the list of files\n"
"  is read from stdin with each file name terminated by a newline\n"
"Output:\n"
"  For --assemble the output is the name of the assembly file followed by the\n"
"  count of the files it contains; the arguments for --dissemble.  Otherwise\n"
"  the output is the total decode time in seconds.\n");

   exit(99);
}

int main(int argc, char **argv)
{
   int ok = 0;
   int err = 0;
   int nfiles = 0;
   int transforms = -1; /* by row */
   const char *assembly = NULL;
   FILE *fp;

   if (argc > 2 && strcmp(argv[1], "--assemble") == 0)
   {
      /* Just build the test file, argv[2] is the file name. */
      assembly = argv[2];
      fp = fopen(assembly, "wb");
      if (fp == NULL)
      {
         perror(assembly);
         fprintf(stderr, "timepng --assemble %s: could not open for write\n",
               assembly);
         usage(NULL);
      }

      argv += 2;
      argc -= 2;
   }

   else if (argc > 3 && strcmp(argv[1], "--dissemble") == 0)
   {
      fp = fopen(argv[2], "rb");

      if (fp == NULL)
      {
         perror(argv[2]);
         fprintf(stderr, "timepng --dissemble %s: could not open for read\n",
               argv[2]);
         usage(NULL);
      }

      nfiles = atoi(argv[3]);
      if (nfiles <= 0)
      {
         fprintf(stderr,
               "timepng --dissemble <file> <count>: %s is not a count\n",
               argv[3]);
         exit(99);
      }
#ifdef __COVERITY__
      else
      {
         nfiles &= PNG_UINT_31_MAX;
      }
#endif

      argv += 3;
      argc -= 3;
   }

   else /* Else use a temporary file */
   {
#ifndef __COVERITY__
      fp = tmpfile();
#else
      /* Experimental. Coverity says tmpfile() is insecure because it
       * generates predictable names.
       *
       * It is possible to satisfy Coverity by using mkstemp(); however,
       * any platform supporting mkstemp() undoubtedly has a secure tmpfile()
       * implementation as well, and doesn't need the fix.  Note that
       * the fix won't work on platforms that don't support mkstemp().
       *
       * https://www.securecoding.cert.org/confluence/display/c/
       * FIO21-C.+Do+not+create+temporary+files+in+shared+directories
       * says that most historic implementations of tmpfile() provide
       * only a limited number of possible temporary file names
       * (usually 26) before file names are recycled. That article also
       * provides a secure solution that unfortunately depends upon mkstemp().
       */
      char tmpfile[] = "timepng-XXXXXX";
      int filedes;
      umask(0177);
      filedes = mkstemp(tmpfile);
      if (filedes < 0)
        fp = NULL;
      else
      {
        fp = fdopen(filedes,"w+");
        /* Hide the filename immediately and ensure that the file does
         * not exist after the program ends
         */
        (void) unlink(tmpfile);
      }
#endif

      if (fp == NULL)
      {
         perror("tmpfile");
         fprintf(stderr, "timepng: could not open the temporary file\n");
         exit(1); /* not a user error */
      }
   }

   /* Handle the transforms: */
   while (argc > 1 && argv[1][0] == '-' && argv[1][1] == '-')
   {
      const char *opt = *++argv + 2;

      --argc;

      /* Transforms turn on the by-image processing and maybe set some
       * transforms:
       */
      if (transforms == -1)
         transforms = PNG_TRANSFORM_IDENTITY;

      if (strcmp(opt, "by-image") == 0)
      {
         /* handled above */
      }

#        define OPT(name) else if (strcmp(opt, #name) == 0)\
         transforms |= PNG_TRANSFORM_ ## name

      OPT(STRIP_16);
      OPT(STRIP_ALPHA);
      OPT(PACKING);
      OPT(PACKSWAP);
      OPT(EXPAND);
      OPT(INVERT_MONO);
      OPT(SHIFT);
      OPT(BGR);
      OPT(SWAP_ALPHA);
      OPT(SWAP_ENDIAN);
      OPT(INVERT_ALPHA);
      OPT(STRIP_FILLER);
      OPT(STRIP_FILLER_BEFORE);
      OPT(STRIP_FILLER_AFTER);
      OPT(GRAY_TO_RGB);
      OPT(EXPAND_16);
      OPT(SCALE_16);

      else
      {
         fprintf(stderr, "timepng %s: unrecognized transform\n", opt);
         usage(fp);
      }
   }

   /* Handle the files: */
   if (argc > 1 && nfiles > 0)
      usage(fp); /* Additional files not valid with --dissemble */

   else if (argc > 1)
   {
      int i;

      for (i=1; i<argc; ++i)
      {
         if (nfiles == INT_MAX)
         {
            fprintf(stderr, "%s: skipped, too many files\n", argv[i]);
            break;
         }

         else if (add_one_file(fp, argv[i]))
            ++nfiles;
      }
   }

   else if (nfiles == 0) /* Read from stdin without --dissemble */
   {
      char filename[FILENAME_MAX+1];

      while (fgets(filename, FILENAME_MAX+1, stdin))
      {
         size_t len = strlen(filename);

         if (filename[len-1] == '\n')
         {
            filename[len-1] = 0;
            if (nfiles == INT_MAX)
            {
               fprintf(stderr, "%s: skipped, too many files\n", filename);
               break;
            }

            else if (add_one_file(fp, filename))
               ++nfiles;
         }

         else
         {
            fprintf(stderr, "timepng: file name too long: ...%s\n",
               filename+len-32);
            err = 1;
            break;
         }
      }

      if (ferror(stdin))
      {
         fprintf(stderr, "timepng: stdin: read error\n");
         err = 1;
      }
   }

   /* Perform the test, or produce the --assemble output: */
   if (!err)
   {
      if (nfiles > 0)
      {
         if (assembly != NULL)
         {
            if (fflush(fp) && !ferror(fp) && fclose(fp))
            {
               perror(assembly);
               fprintf(stderr, "%s: close failed\n", assembly);
            }

            else
            {
               printf("%s %d\n", assembly, nfiles);
               fflush(stdout);
               ok = !ferror(stdout);
            }
         }

         else
         {
            ok = perform_one_test(fp, nfiles, transforms);
            (void)fclose(fp);
         }
      }

      else
         usage(fp);
   }

   else
      (void)fclose(fp);

   /* Exit code 0 on success. */
   return ok == 0;
}
#else /* !sufficient support */
int main(void) { return 77; }
#endif /* !sufficient support */
