1/* timepng.c
2 *
3 * Copyright (c) 2013,2016 John Cunningham Bowler
4 *
5 * Last changed in libpng 1.6.22 [May 26, 2016]
6 *
7 * This code is released under the libpng license.
8 * For conditions of distribution and use, see the disclaimer
9 * and license in png.h
10 *
11 * Load an arbitrary number of PNG files (from the command line, or, if there
12 * are no arguments on the command line, from stdin) then run a time test by
13 * reading each file by row or by image (possibly with transforms in the latter
14 * case).  The only output is a time as a floating point number of seconds with
15 * 9 decimal digits.
16 */
17#define _POSIX_C_SOURCE 199309L /* for clock_gettime */
18
19#include <stdlib.h>
20#include <stdio.h>
21#include <string.h>
22#include <errno.h>
23#include <limits.h>
24
25#include <time.h>
26
27#if defined(HAVE_CONFIG_H) && !defined(PNG_NO_CONFIG_H)
28#  include <config.h>
29#endif
30
31/* Define the following to use this test against your installed libpng, rather
32 * than the one being built here:
33 */
34#ifdef PNG_FREESTANDING_TESTS
35#  include <png.h>
36#else
37#  include "../../png.h"
38#endif
39
40/* The following is to support direct compilation of this file as C++ */
41#ifdef __cplusplus
42#  define voidcast(type, value) static_cast<type>(value)
43#else
44#  define voidcast(type, value) (value)
45#endif /* __cplusplus */
46
47/* 'CLOCK_PROCESS_CPUTIME_ID' is one of the clock timers for clock_gettime.  It
48 * need not be supported even when clock_gettime is available.  It returns the
49 * 'CPU' time the process has consumed.  'CPU' time is assumed to include time
50 * when the CPU is actually blocked by a pending cache fill but not time
51 * waiting for page faults.  The attempt is to get a measure of the actual time
52 * the implementation takes to read a PNG ignoring the potentially very large IO
53 * overhead.
54 */
55#if defined (CLOCK_PROCESS_CPUTIME_ID) && defined(PNG_STDIO_SUPPORTED) &&\
56    defined(PNG_EASY_ACCESS_SUPPORTED) &&\
57    (PNG_LIBPNG_VER >= 10700 ? defined(PNG_READ_PNG_SUPPORTED) :\
58     defined (PNG_SEQUENTIAL_READ_SUPPORTED) &&\
59     defined(PNG_INFO_IMAGE_SUPPORTED))
60
61typedef struct
62{
63   FILE *input;
64   FILE *output;
65}  io_data;
66
67static PNG_CALLBACK(void, read_and_copy,
68      (png_structp png_ptr, png_bytep buffer, png_size_t cb))
69{
70   io_data *io = (io_data*)png_get_io_ptr(png_ptr);
71
72   if (fread(buffer, cb, 1, io->input) != 1)
73      png_error(png_ptr, strerror(errno));
74
75   if (fwrite(buffer, cb, 1, io->output) != 1)
76   {
77      perror("temporary file");
78      fprintf(stderr, "temporary file PNG write failed\n");
79      exit(1);
80   }
81}
82
83static void read_by_row(png_structp png_ptr, png_infop info_ptr,
84      FILE *write_ptr, FILE *read_ptr)
85{
86   /* These don't get freed on error, this is fine; the program immediately
87    * exits.
88    */
89   png_bytep row = NULL, display = NULL;
90   io_data io_copy;
91
92   if (write_ptr != NULL)
93   {
94      /* Set up for a copy to the temporary file: */
95      io_copy.input = read_ptr;
96      io_copy.output = write_ptr;
97      png_set_read_fn(png_ptr, &io_copy, read_and_copy);
98   }
99
100   png_read_info(png_ptr, info_ptr);
101
102   {
103      png_size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);
104
105      row = voidcast(png_bytep,malloc(rowbytes));
106      display = voidcast(png_bytep,malloc(rowbytes));
107
108      if (row == NULL || display == NULL)
109         png_error(png_ptr, "OOM allocating row buffers");
110
111      {
112         png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
113         int passes = png_set_interlace_handling(png_ptr);
114         int pass;
115
116         png_start_read_image(png_ptr);
117
118         for (pass = 0; pass < passes; ++pass)
119         {
120            png_uint_32 y = height;
121
122            /* NOTE: this trashes the row each time; interlace handling won't
123             * work, but this avoids memory thrashing for speed testing and is
124             * somewhat representative of an application that works row-by-row.
125             */
126            while (y-- > 0)
127               png_read_row(png_ptr, row, display);
128         }
129      }
130   }
131
132   /* Make sure to read to the end of the file: */
133   png_read_end(png_ptr, info_ptr);
134
135   /* Free this up: */
136   free(row);
137   free(display);
138}
139
140static PNG_CALLBACK(void, no_warnings, (png_structp png_ptr,
141         png_const_charp warning))
142{
143   (void)png_ptr;
144   (void)warning;
145}
146
147static int read_png(FILE *fp, png_int_32 transforms, FILE *write_file)
148{
149   png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,0,0,
150         no_warnings);
151   png_infop info_ptr = NULL;
152
153   if (png_ptr == NULL)
154      return 0;
155
156   if (setjmp(png_jmpbuf(png_ptr)))
157   {
158      png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
159      return 0;
160   }
161
162#  ifdef PNG_BENIGN_ERRORS_SUPPORTED
163      png_set_benign_errors(png_ptr, 1/*allowed*/);
164#  endif
165   png_init_io(png_ptr, fp);
166
167   info_ptr = png_create_info_struct(png_ptr);
168
169   if (info_ptr == NULL)
170      png_error(png_ptr, "OOM allocating info structure");
171
172   if (transforms < 0)
173      read_by_row(png_ptr, info_ptr, write_file, fp);
174
175   else
176      png_read_png(png_ptr, info_ptr, transforms, NULL/*params*/);
177
178   png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
179   return 1;
180}
181
182static int mytime(struct timespec *t)
183{
184   /* Do the timing using clock_gettime and the per-process timer. */
185   if (!clock_gettime(CLOCK_PROCESS_CPUTIME_ID, t))
186      return 1;
187
188   perror("CLOCK_PROCESS_CPUTIME_ID");
189   fprintf(stderr, "timepng: could not get the time\n");
190   return 0;
191}
192
193static int perform_one_test(FILE *fp, int nfiles, png_int_32 transforms)
194{
195   int i;
196   struct timespec before, after;
197
198   /* Clear out all errors: */
199   rewind(fp);
200
201   if (mytime(&before))
202   {
203      for (i=0; i<nfiles; ++i)
204      {
205         if (read_png(fp, transforms, NULL/*write*/))
206         {
207            if (ferror(fp))
208            {
209               perror("temporary file");
210               fprintf(stderr, "file %d: error reading PNG data\n", i);
211               return 0;
212            }
213         }
214
215         else
216         {
217            perror("temporary file");
218            fprintf(stderr, "file %d: error from libpng\n", i);
219            return 0;
220         }
221      }
222   }
223
224   else
225      return 0;
226
227   if (mytime(&after))
228   {
229      /* Work out the time difference and print it - this is the only output,
230       * so flush it immediately.
231       */
232      unsigned long s = after.tv_sec - before.tv_sec;
233      long ns = after.tv_nsec - before.tv_nsec;
234
235      if (ns < 0)
236      {
237         --s;
238         ns += 1000000000;
239
240         if (ns < 0)
241         {
242            fprintf(stderr, "timepng: bad clock from kernel\n");
243            return 0;
244         }
245      }
246
247      printf("%lu.%.9ld\n", s, ns);
248      fflush(stdout);
249      if (ferror(stdout))
250      {
251         fprintf(stderr, "timepng: error writing output\n");
252         return 0;
253      }
254
255      /* Successful return */
256      return 1;
257   }
258
259   else
260      return 0;
261}
262
263static int add_one_file(FILE *fp, char *name)
264{
265   FILE *ip = fopen(name, "rb");
266
267   if (ip != NULL)
268   {
269      /* Read the file using libpng; this detects errors and also deals with
270       * files which contain data beyond the end of the file.
271       */
272      int ok = 0;
273      fpos_t pos;
274
275      if (fgetpos(fp, &pos))
276      {
277         /* Fatal error reading the start: */
278         perror("temporary file");
279         fprintf(stderr, "temporary file fgetpos error\n");
280         exit(1);
281      }
282
283      if (read_png(ip, -1/*by row*/, fp/*output*/))
284      {
285         if (ferror(ip))
286         {
287            perror(name);
288            fprintf(stderr, "%s: read error\n", name);
289         }
290
291         else
292            ok = 1; /* read ok */
293      }
294
295      else
296         fprintf(stderr, "%s: file not added\n", name);
297
298      (void)fclose(ip);
299
300      /* An error in the output is fatal; exit immediately: */
301      if (ferror(fp))
302      {
303         perror("temporary file");
304         fprintf(stderr, "temporary file write error\n");
305         exit(1);
306      }
307
308      if (ok)
309         return 1;
310
311      /* Did not read the file successfully, simply rewind the temporary
312       * file.  This must happen after the ferror check above to avoid clearing
313       * the error.
314       */
315      if (fsetpos(fp, &pos))
316      {
317         perror("temporary file");
318         fprintf(stderr, "temporary file fsetpos error\n");
319         exit(1);
320      }
321   }
322
323   else
324   {
325      /* file open error: */
326      perror(name);
327      fprintf(stderr, "%s: open failed\n", name);
328   }
329
330   return 0; /* file not added */
331}
332
333static void
334usage(FILE *fp)
335{
336   if (fp != NULL) fclose(fp);
337
338   fprintf(stderr,
339"Usage:\n"
340" timepng --assemble <assembly> {files}\n"
341"  Read the files into <assembly>, output the count.  Options are ignored.\n"
342" timepng --dissemble <assembly> <count> [options]\n"
343"  Time <count> files from <assembly>, additional files may not be given.\n"
344" Otherwise:\n"
345"  Read the files into a temporary file and time the decode\n"
346"Transforms:\n"
347"  --by-image: read by image with png_read_png\n"
348"  --<transform>: implies by-image, use PNG_TRANSFORM_<transform>\n"
349"  Otherwise: read by row using png_read_row (to a single row buffer)\n"
350   /* ISO C90 string length max 509 */);fprintf(stderr,
351"{files}:\n"
352"  PNG files to copy into the assembly and time.  Invalid files are skipped\n"
353"  with appropriate error messages.  If no files are given the list of files\n"
354"  is read from stdin with each file name terminated by a newline\n"
355"Output:\n"
356"  For --assemble the output is the name of the assembly file followed by the\n"
357"  count of the files it contains; the arguments for --dissemble.  Otherwise\n"
358"  the output is the total decode time in seconds.\n");
359
360   exit(99);
361}
362
363int main(int argc, char **argv)
364{
365   int ok = 0;
366   int err = 0;
367   int nfiles = 0;
368   int transforms = -1; /* by row */
369   const char *assembly = NULL;
370   FILE *fp;
371
372   if (argc > 2 && strcmp(argv[1], "--assemble") == 0)
373   {
374      /* Just build the test file, argv[2] is the file name. */
375      assembly = argv[2];
376      fp = fopen(assembly, "wb");
377      if (fp == NULL)
378      {
379         perror(assembly);
380         fprintf(stderr, "timepng --assemble %s: could not open for write\n",
381               assembly);
382         usage(NULL);
383      }
384
385      argv += 2;
386      argc -= 2;
387   }
388
389   else if (argc > 3 && strcmp(argv[1], "--dissemble") == 0)
390   {
391      fp = fopen(argv[2], "rb");
392
393      if (fp == NULL)
394      {
395         perror(argv[2]);
396         fprintf(stderr, "timepng --dissemble %s: could not open for read\n",
397               argv[2]);
398         usage(NULL);
399      }
400
401      nfiles = atoi(argv[3]);
402      if (nfiles <= 0)
403      {
404         fprintf(stderr,
405               "timepng --dissemble <file> <count>: %s is not a count\n",
406               argv[3]);
407         exit(99);
408      }
409#ifdef __COVERITY__
410      else
411      {
412         nfiles &= PNG_UINT_31_MAX;
413      }
414#endif
415
416      argv += 3;
417      argc -= 3;
418   }
419
420   else /* Else use a temporary file */
421   {
422#ifndef __COVERITY__
423      fp = tmpfile();
424#else
425      /* Experimental. Coverity says tmpfile() is insecure because it
426       * generates predictable names.
427       *
428       * It is possible to satisfy Coverity by using mkstemp(); however,
429       * any platform supporting mkstemp() undoubtedly has a secure tmpfile()
430       * implementation as well, and doesn't need the fix.  Note that
431       * the fix won't work on platforms that don't support mkstemp().
432       *
433       * https://www.securecoding.cert.org/confluence/display/c/
434       * FIO21-C.+Do+not+create+temporary+files+in+shared+directories
435       * says that most historic implementations of tmpfile() provide
436       * only a limited number of possible temporary file names
437       * (usually 26) before file names are recycled. That article also
438       * provides a secure solution that unfortunately depends upon mkstemp().
439       */
440      char tmpfile[] = "timepng-XXXXXX";
441      int filedes;
442      umask(0177);
443      filedes = mkstemp(tmpfile);
444      if (filedes < 0)
445        fp = NULL;
446      else
447      {
448        fp = fdopen(filedes,"w+");
449        /* Hide the filename immediately and ensure that the file does
450         * not exist after the program ends
451         */
452        (void) unlink(tmpfile);
453      }
454#endif
455
456      if (fp == NULL)
457      {
458         perror("tmpfile");
459         fprintf(stderr, "timepng: could not open the temporary file\n");
460         exit(1); /* not a user error */
461      }
462   }
463
464   /* Handle the transforms: */
465   while (argc > 1 && argv[1][0] == '-' && argv[1][1] == '-')
466   {
467      const char *opt = *++argv + 2;
468
469      --argc;
470
471      /* Transforms turn on the by-image processing and maybe set some
472       * transforms:
473       */
474      if (transforms == -1)
475         transforms = PNG_TRANSFORM_IDENTITY;
476
477      if (strcmp(opt, "by-image") == 0)
478      {
479         /* handled above */
480      }
481
482#        define OPT(name) else if (strcmp(opt, #name) == 0)\
483         transforms |= PNG_TRANSFORM_ ## name
484
485      OPT(STRIP_16);
486      OPT(STRIP_ALPHA);
487      OPT(PACKING);
488      OPT(PACKSWAP);
489      OPT(EXPAND);
490      OPT(INVERT_MONO);
491      OPT(SHIFT);
492      OPT(BGR);
493      OPT(SWAP_ALPHA);
494      OPT(SWAP_ENDIAN);
495      OPT(INVERT_ALPHA);
496      OPT(STRIP_FILLER);
497      OPT(STRIP_FILLER_BEFORE);
498      OPT(STRIP_FILLER_AFTER);
499      OPT(GRAY_TO_RGB);
500      OPT(EXPAND_16);
501      OPT(SCALE_16);
502
503      else
504      {
505         fprintf(stderr, "timepng %s: unrecognized transform\n", opt);
506         usage(fp);
507      }
508   }
509
510   /* Handle the files: */
511   if (argc > 1 && nfiles > 0)
512      usage(fp); /* Additional files not valid with --dissemble */
513
514   else if (argc > 1)
515   {
516      int i;
517
518      for (i=1; i<argc; ++i)
519      {
520         if (nfiles == INT_MAX)
521         {
522            fprintf(stderr, "%s: skipped, too many files\n", argv[i]);
523            break;
524         }
525
526         else if (add_one_file(fp, argv[i]))
527            ++nfiles;
528      }
529   }
530
531   else if (nfiles == 0) /* Read from stdin withoout --dissemble */
532   {
533      char filename[FILENAME_MAX+1];
534
535      while (fgets(filename, FILENAME_MAX+1, stdin))
536      {
537         size_t len = strlen(filename);
538
539         if (filename[len-1] == '\n')
540         {
541            filename[len-1] = 0;
542            if (nfiles == INT_MAX)
543            {
544               fprintf(stderr, "%s: skipped, too many files\n", filename);
545               break;
546            }
547
548            else if (add_one_file(fp, filename))
549               ++nfiles;
550         }
551
552         else
553         {
554            fprintf(stderr, "timepng: file name too long: ...%s\n",
555               filename+len-32);
556            err = 1;
557            break;
558         }
559      }
560
561      if (ferror(stdin))
562      {
563         fprintf(stderr, "timepng: stdin: read error\n");
564         err = 1;
565      }
566   }
567
568   /* Perform the test, or produce the --assemble output: */
569   if (!err)
570   {
571      if (nfiles > 0)
572      {
573         if (assembly != NULL)
574         {
575            if (fflush(fp) && !ferror(fp) && fclose(fp))
576            {
577               perror(assembly);
578               fprintf(stderr, "%s: close failed\n", assembly);
579            }
580
581            else
582            {
583               printf("%s %d\n", assembly, nfiles);
584               fflush(stdout);
585               ok = !ferror(stdout);
586            }
587         }
588
589         else
590         {
591            ok = perform_one_test(fp, nfiles, transforms);
592            (void)fclose(fp);
593         }
594      }
595
596      else
597         usage(fp);
598   }
599
600   else
601      (void)fclose(fp);
602
603   /* Exit code 0 on success. */
604   return ok == 0;
605}
606#else /* !sufficient support */
607int main(void) { return 77; }
608#endif /* !sufficient support */
609