/*
 * PPD test program for CUPS.
 *
 * THIS PROGRAM IS DEPRECATED AND WILL BE REMOVED IN A FUTURE VERSION OF CUPS.
 *
 * Copyright © 2020-2024 by OpenPrinting.
 * Copyright © 2007-2018 by Apple Inc.
 * Copyright © 1997-2007 by Easy Software Products, all rights reserved.
 *
 * Licensed under Apache License v2.0.  See the file "LICENSE" for more
 * information.
 *
 * PostScript is a trademark of Adobe Systems, Inc.
 */

/*
 * Include necessary headers...
 */

#include <cups/cups-private.h>
#include <cups/dir.h>
#include <cups/ppd-private.h>
#include <cups/raster.h>
#include <math.h>
#ifdef _WIN32
#  define X_OK 0
#endif /* _WIN32 */


/*
 * Error warning overrides...
 */

enum
{
  WARN_NONE = 0,
  WARN_CONSTRAINTS = 1,
  WARN_DEFAULTS = 2,
  WARN_FILTERS = 4,
  WARN_PROFILES = 8,
  WARN_TRANSLATIONS = 16,
  WARN_DUPLEX = 32,
  WARN_SIZES = 64,
  WARN_FILENAME = 128,
  WARN_ALL = 255
};


/*
 * Error codes...
 */

enum
{
  ERROR_NONE = 0,
  ERROR_USAGE,
  ERROR_FILE_OPEN,
  ERROR_PPD_FORMAT,
  ERROR_CONFORMANCE
};


/*
 * Line endings...
 */

enum
{
  EOL_NONE = 0,
  EOL_CR,
  EOL_LF,
  EOL_CRLF
};


/*
 * File permissions...
 */

#define MODE_WRITE	0022		/* Group/other write */
#define MODE_MASK	0555		/* Owner/group/other read+exec/search */
#define MODE_DATAFILE	0444		/* Owner/group/other read */
#define MODE_DIRECTORY	0555		/* Owner/group/other read+search */
#define MODE_PROGRAM	0555		/* Owner/group/other read+exec */


/*
 * Local functions...
 */

static void	check_basics(const char *filename);
static int	check_constraints(ppd_file_t *ppd, int errors, int verbose,
		                  int warn);
static int	check_case(ppd_file_t *ppd, int errors, int verbose);
static int	check_defaults(ppd_file_t *ppd, int errors, int verbose,
		               int warn);
static int	check_duplex(ppd_file_t *ppd, int errors, int verbose,
		             int warn);
static int	check_filters(ppd_file_t *ppd, const char *root, int errors,
		              int verbose, int warn);
static int	check_profiles(ppd_file_t *ppd, const char *root, int errors,
		               int verbose, int warn);
static int	check_sizes(ppd_file_t *ppd, int errors, int verbose, int warn);
static int	check_translations(ppd_file_t *ppd, int errors, int verbose,
		                   int warn);
static void	show_conflicts(ppd_file_t *ppd, const char *prefix);
static int	test_raster(ppd_file_t *ppd, int verbose);
static void	usage(void) _CUPS_NORETURN;
static int	valid_path(const char *keyword, const char *path, int errors,
		           int verbose, int warn);
static int	valid_utf8(const char *s);


/*
 * 'main()' - Main entry for test program.
 */

int					/* O - Exit status */
main(int  argc,				/* I - Number of command-line args */
     char *argv[])			/* I - Command-line arguments */
{
  int		i, j, k, m, n;		/* Looping vars */
  size_t	len;			/* Length of option name */
  char		*opt;			/* Option character */
  const char	*ptr;			/* Pointer into string */
  cups_file_t	*fp;			/* PPD file */
  int		files;			/* Number of files */
  int		verbose;		/* Want verbose output? */
  int		warn;			/* Which errors to just warn about */
  int		ignore;			/* Which errors to ignore */
  int		status;			/* Exit status */
  int		errors;			/* Number of conformance errors */
  int		ppdversion;		/* PPD spec version in PPD file */
  ppd_status_t	error;			/* Status of ppdOpen*() */
  int		line;			/* Line number for error */
  char		*root;			/* Root directory */
  int		xdpi,			/* X resolution */
		ydpi;			/* Y resolution */
  ppd_file_t	*ppd;			/* PPD file record */
  ppd_attr_t	*attr;			/* PPD attribute */
  ppd_size_t	*size;			/* Size record */
  ppd_group_t	*group;			/* UI group */
  ppd_option_t	*option;		/* Standard UI option */
  ppd_group_t	*group2;		/* UI group */
  ppd_option_t	*option2;		/* Standard UI option */
  ppd_choice_t	*choice;		/* Standard UI option choice */
  struct lconv	*loc;			/* Locale data */
  static char	*uis[] = { "BOOLEAN", "PICKONE", "PICKMANY" };
  static char	*sections[] = { "ANY", "DOCUMENT", "EXIT",
                                "JCL", "PAGE", "PROLOG" };


  _cupsSetLocale(argv);
  loc = localeconv();

 /*
  * Display PPD files for each file listed on the command-line...
  */

  ppdSetConformance(PPD_CONFORM_STRICT);

  verbose = 0;
  ppd     = NULL;
  files   = 0;
  status  = ERROR_NONE;
  root    = "";
  warn    = WARN_NONE;
  ignore  = WARN_NONE;

  for (i = 1; i < argc; i ++)
    if (!strcmp(argv[i], "--help"))
      usage();
    else if (argv[i][0] == '-' && argv[i][1])
    {
      for (opt = argv[i] + 1; *opt; opt ++)
        switch (*opt)
	{
	  case 'I' :			/* Ignore errors */
	      i ++;

	      if (i >= argc)
	        usage();

              if (!strcmp(argv[i], "none"))
	        ignore = WARN_NONE;
	      else if (!strcmp(argv[i], "filename"))
	        ignore |= WARN_FILENAME;
	      else if (!strcmp(argv[i], "filters"))
	        ignore |= WARN_FILTERS;
	      else if (!strcmp(argv[i], "profiles"))
	        ignore |= WARN_PROFILES;
	      else if (!strcmp(argv[i], "all"))
	        ignore = WARN_FILTERS | WARN_PROFILES;
	      else
	        usage();
	      break;

	  case 'R' :			/* Alternate root directory */
	      i ++;

	      if (i >= argc)
	        usage();

              root = argv[i];
	      break;

	  case 'W' :			/* Turn errors into warnings */
	      i ++;

	      if (i >= argc)
	        usage();

              if (!strcmp(argv[i], "none"))
	        warn = WARN_NONE;
	      else if (!strcmp(argv[i], "constraints"))
	        warn |= WARN_CONSTRAINTS;
	      else if (!strcmp(argv[i], "defaults"))
	        warn |= WARN_DEFAULTS;
	      else if (!strcmp(argv[i], "duplex"))
	        warn |= WARN_DUPLEX;
	      else if (!strcmp(argv[i], "filters"))
	        warn |= WARN_FILTERS;
	      else if (!strcmp(argv[i], "profiles"))
	        warn |= WARN_PROFILES;
	      else if (!strcmp(argv[i], "sizes"))
	        warn |= WARN_SIZES;
	      else if (!strcmp(argv[i], "translations"))
	        warn |= WARN_TRANSLATIONS;
	      else if (!strcmp(argv[i], "all"))
	        warn = WARN_ALL;
	      else
	        usage();
	      break;

	  case 'q' :			/* Quiet mode */
	      if (verbose > 0)
	      {
        	_cupsLangPuts(stderr,
		              _("cupstestppd: The -q option is incompatible "
			        "with the -v option."));
		return (1);
	      }

	      verbose --;
	      break;

	  case 'r' :			/* Relaxed mode */
              ppdSetConformance(PPD_CONFORM_RELAXED);
	      break;

	  case 'v' :			/* Verbose mode */
	      if (verbose < 0)
	      {
        	_cupsLangPuts(stderr,
		              _("cupstestppd: The -v option is incompatible "
			        "with the -q option."));
		return (1);
	      }

	      verbose ++;
	      break;

	  default :
	      usage();
	}
    }
    else
    {
     /*
      * Open the PPD file...
      */

      if (files && verbose >= 0)
        puts("");

      files ++;

      if (argv[i][0] == '-')
      {
       /*
        * Read from stdin...
	*/

        ppd = _ppdOpen(cupsFileStdin(), _PPD_LOCALIZATION_ALL);

        if (verbose >= 0)
          printf("%s:", (ppd && ppd->pcfilename) ? ppd->pcfilename : "(stdin)");
      }
      else
      {
       /*
        * Read from a file...
	*/

        if (verbose >= 0)
          printf("%s:", argv[i]);

        if ((fp = cupsFileOpen(argv[i], "r")) != NULL)
        {
          ppd = _ppdOpen(fp, _PPD_LOCALIZATION_ALL);
          cupsFileClose(fp);
        }
        else
        {
	  status = ERROR_FILE_OPEN;

	  if (verbose >= 0)
          {
            _cupsLangPuts(stdout, _(" FAIL"));
            _cupsLangPrintf(stdout,
	                    _("      **FAIL**  Unable to open PPD file - %s on "
	                      "line %d."), strerror(errno), 0);
	    continue;
          }
        }
      }

      if (ppd == NULL)
      {
        error = ppdLastError(&line);

	if (error <= PPD_ALLOC_ERROR)
	{
	  status = ERROR_FILE_OPEN;

	  if (verbose >= 0)
          {
            _cupsLangPuts(stdout, _(" FAIL"));
            _cupsLangPrintf(stdout,
	                    _("      **FAIL**  Unable to open PPD file - %s on "
	                      "line %d."), strerror(errno), 0);
          }
	}
	else
	{
	  status = ERROR_PPD_FORMAT;

          if (verbose >= 0)
          {
            _cupsLangPuts(stdout, _(" FAIL"));
            _cupsLangPrintf(stdout,
	                    _("      **FAIL**  Unable to open PPD file - "
			      "%s on line %d."),
			    ppdErrorString(error), line);

            switch (error)
	    {
	      case PPD_MISSING_PPDADOBE4 :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 42, section "
				  "5.2."));
	          break;
	      case PPD_MISSING_VALUE :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 20, section "
				  "3.4."));
	          break;
	      case PPD_BAD_OPEN_GROUP :
	      case PPD_NESTED_OPEN_GROUP :
	          _cupsLangPuts(stdout,
		                _("                REF: Pages 45-46, section "
				  "5.2."));
	          break;
	      case PPD_BAD_OPEN_UI :
	      case PPD_NESTED_OPEN_UI :
	          _cupsLangPuts(stdout,
		                _("                REF: Pages 42-45, section "
				  "5.2."));
	          break;
	      case PPD_BAD_ORDER_DEPENDENCY :
	          _cupsLangPuts(stdout,
		                _("                REF: Pages 48-49, section "
				  "5.2."));
	          break;
	      case PPD_BAD_UI_CONSTRAINTS :
	          _cupsLangPuts(stdout,
		                _("                REF: Pages 52-54, section "
				  "5.2."));
	          break;
	      case PPD_MISSING_ASTERISK :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 15, section "
				  "3.2."));
	          break;
	      case PPD_LINE_TOO_LONG :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 15, section "
				  "3.1."));
	          break;
	      case PPD_ILLEGAL_CHARACTER :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 15, section "
				  "3.1."));
	          break;
	      case PPD_ILLEGAL_MAIN_KEYWORD :
	          _cupsLangPuts(stdout,
		                _("                REF: Pages 16-17, section "
				  "3.2."));
	          break;
	      case PPD_ILLEGAL_OPTION_KEYWORD :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 19, section "
				  "3.3."));
	          break;
	      case PPD_ILLEGAL_TRANSLATION :
	          _cupsLangPuts(stdout,
		                _("                REF: Page 27, section "
				  "3.5."));
	          break;
              default :
	          break;
	    }

	    check_basics(argv[i]);
	  }
        }

	continue;
      }

     /*
      * Show the header and then perform basic conformance tests (limited
      * only by what the CUPS PPD functions actually load...)
      */

      errors     = 0;
      ppdversion = 43;

      if (verbose > 0)
        _cupsLangPuts(stdout,
	              _("\n    DETAILED CONFORMANCE TEST RESULTS"));

      if ((attr = ppdFindAttr(ppd, "FormatVersion", NULL)) != NULL &&
          attr->value)
        ppdversion = (int)(10 * _cupsStrScand(attr->value, NULL, loc) + 0.5);

      if ((attr = ppdFindAttr(ppd, "cupsFilter2", NULL)) != NULL)
      {
        do
        {
	  if (strstr(attr->value, "application/vnd.cups-raster"))
	  {
	    if (!test_raster(ppd, verbose))
	      errors ++;
	    break;
	  }
	}
	while ((attr = ppdFindNextAttr(ppd, "cupsFilter2", NULL)) != NULL);
      }
      else
      {
	for (j = 0; j < ppd->num_filters; j ++)
	  if (strstr(ppd->filters[j], "application/vnd.cups-raster"))
	  {
	    if (!test_raster(ppd, verbose))
	      errors ++;
	    break;
	  }
      }

     /*
      * Look for default keywords with no matching option...
      */

      if (!(warn & WARN_DEFAULTS))
        errors = check_defaults(ppd, errors, verbose, 0);

      if ((attr = ppdFindAttr(ppd, "DefaultImageableArea", NULL)) == NULL)
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
			_("      **FAIL**  REQUIRED DefaultImageableArea\n"
			  "                REF: Page 102, section 5.15."));
	}

	errors ++;
      }
      else if (ppdPageSize(ppd, attr->value) == NULL &&
	       strcmp(attr->value, "Unknown"))
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPrintf(stdout,
			  _("      **FAIL**  Bad DefaultImageableArea %s\n"
			    "                REF: Page 102, section 5.15."),
			  attr->value);
	}

	errors ++;
      }
      else
      {
	if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    DefaultImageableArea"));
      }

      if ((attr = ppdFindAttr(ppd, "DefaultPaperDimension", NULL)) == NULL)
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
			_("      **FAIL**  REQUIRED DefaultPaperDimension\n"
			  "                REF: Page 103, section 5.15."));
	}

	errors ++;
      }
      else if (ppdPageSize(ppd, attr->value) == NULL &&
	       strcmp(attr->value, "Unknown"))
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPrintf(stdout,
			  _("      **FAIL**  Bad DefaultPaperDimension %s\n"
			    "                REF: Page 103, section 5.15."),
			  attr->value);
	}

	errors ++;
      }
      else if (verbose > 0)
	_cupsLangPuts(stdout, _("        PASS    DefaultPaperDimension"));

      for (j = 0, group = ppd->groups; j < ppd->num_groups; j ++, group ++)
	for (k = 0, option = group->options;
	     k < group->num_options;
	     k ++, option ++)
	{
	 /*
	  * Verify that we have a default choice...
	  */

	  if (option->defchoice[0])
	  {
	    if (ppdFindChoice(option, option->defchoice) == NULL &&
		strcmp(option->defchoice, "Unknown"))
	    {
	      if (verbose >= 0)
	      {
		if (!errors && !verbose)
		  _cupsLangPuts(stdout, _(" FAIL"));

		_cupsLangPrintf(stdout,
				_("      **FAIL**  Bad Default%s %s\n"
				  "                REF: Page 40, section 4.5."),
				option->keyword, option->defchoice);
	      }

	      errors ++;
	    }
	    else if (verbose > 0)
	      _cupsLangPrintf(stdout,
			      _("        PASS    Default%s"),
			      option->keyword);
	  }
	  else
	  {
	    if (verbose >= 0)
	    {
	      if (!errors && !verbose)
		_cupsLangPuts(stdout, _(" FAIL"));

	      _cupsLangPrintf(stdout,
			      _("      **FAIL**  REQUIRED Default%s\n"
				"                REF: Page 40, section 4.5."),
			      option->keyword);
	    }

	    errors ++;
	  }
	}

      if ((attr = ppdFindAttr(ppd, "FileVersion", NULL)) != NULL)
      {
        for (ptr = attr->value; *ptr; ptr ++)
	  if (!isdigit(*ptr & 255) && *ptr != '.')
	    break;

	if (*ptr)
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPrintf(stdout,
			    _("      **FAIL**  Bad FileVersion \"%s\"\n"
			      "                REF: Page 56, section 5.3."),
			    attr->value);
	  }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    FileVersion"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED FileVersion\n"
			  "                REF: Page 56, section 5.3."));
        }

	errors ++;
      }

      if ((attr = ppdFindAttr(ppd, "FormatVersion", NULL)) != NULL)
      {
        ptr = attr->value;
	if (*ptr == '4' && ptr[1] == '.')
	{

	  for (ptr += 2; *ptr; ptr ++)
	    if (!isdigit(*ptr & 255))
	      break;
        }

	if (*ptr)
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPrintf(stdout,
			    _("      **FAIL**  Bad FormatVersion \"%s\"\n"
			      "                REF: Page 56, section 5.3."),
			    attr->value);
	  }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    FormatVersion"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED FormatVersion\n"
			  "                REF: Page 56, section 5.3."));
        }

	errors ++;
      }

      if (ppd->lang_encoding != NULL)
      {
	if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    LanguageEncoding"));
      }
      else if (ppdversion > 40)
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED LanguageEncoding\n"
			  "                REF: Pages 56-57, section 5.3."));
        }

	errors ++;
      }

      if (ppd->lang_version != NULL)
      {
	if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    LanguageVersion"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED LanguageVersion\n"
			  "                REF: Pages 57-58, section 5.3."));
        }

	errors ++;
      }

      if (ppd->manufacturer != NULL)
      {
        if (!_cups_strncasecmp(ppd->manufacturer, "Hewlett-Packard", 15) ||
	    !_cups_strncasecmp(ppd->manufacturer, "Hewlett Packard", 15))
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPrintf(stdout,
			    _("      **FAIL**  Bad Manufacturer (should be "
			      "\"%s\")\n"
			      "                REF: Page 211, table D.1."),
			    "HP");
          }

	  errors ++;
	}
        else if (!_cups_strncasecmp(ppd->manufacturer, "OkiData", 7) ||
	         !_cups_strncasecmp(ppd->manufacturer, "Oki Data", 8))
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPrintf(stdout,
	                    _("      **FAIL**  Bad Manufacturer (should be "
			      "\"%s\")\n"
			      "                REF: Page 211, table D.1."),
			    "Oki");
          }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    Manufacturer"));
      }
      else if (ppdversion >= 43)
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED Manufacturer\n"
			  "                REF: Pages 58-59, section 5.3."));
        }

	errors ++;
      }

      if (ppd->modelname != NULL)
      {
        for (ptr = ppd->modelname; *ptr; ptr ++)
	  if (!isalnum(*ptr & 255) && !strchr(" ./-+", *ptr))
	    break;

	if (*ptr)
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPrintf(stdout,
	                    _("      **FAIL**  Bad ModelName - \"%c\" not "
			      "allowed in string.\n"
			      "                REF: Pages 59-60, section 5.3."),
	                    *ptr);
          }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    ModelName"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED ModelName\n"
			  "                REF: Pages 59-60, section 5.3."));
        }

	errors ++;
      }

      if (ppd->nickname != NULL)
      {
	if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    NickName"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED NickName\n"
	                  "                REF: Page 60, section 5.3."));
        }

	errors ++;
      }

      if (ppdFindOption(ppd, "PageSize") != NULL)
      {
	if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    PageSize"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED PageSize\n"
			  "                REF: Pages 99-100, section 5.14."));
        }

	errors ++;
      }

      if (ppdFindOption(ppd, "PageRegion") != NULL)
      {
	if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    PageRegion"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED PageRegion\n"
			  "                REF: Page 100, section 5.14."));
        }

	errors ++;
      }

      if (ppd->pcfilename != NULL)
      {
	if (verbose > 0)
          _cupsLangPuts(stdout, _("        PASS    PCFileName"));
      }
      else if (!(ignore & WARN_FILENAME))
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED PCFileName\n"
			  "                REF: Pages 61-62, section 5.3."));
        }

	errors ++;
      }

      if (ppd->product != NULL)
      {
        if (ppd->product[0] != '(' ||
	    ppd->product[strlen(ppd->product) - 1] != ')')
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPuts(stdout,
	                  _("      **FAIL**  Bad Product - not \"(string)\".\n"
			    "                REF: Page 62, section 5.3."));
          }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    Product"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED Product\n"
			  "                REF: Page 62, section 5.3."));
        }

	errors ++;
      }

      if ((attr = ppdFindAttr(ppd, "PSVersion", NULL)) != NULL &&
          attr->value != NULL)
      {
        char	junkstr[255];			/* Temp string */
	int	junkint;			/* Temp integer */


        if (sscanf(attr->value, "(%254[^)\n])%d", junkstr, &junkint) != 2)
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPuts(stdout,
	                  _("      **FAIL**  Bad PSVersion - not \"(string) "
			    "int\".\n"
			    "                REF: Pages 62-64, section 5.3."));
          }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    PSVersion"));
      }
      else
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED PSVersion\n"
			  "                REF: Pages 62-64, section 5.3."));
        }

	errors ++;
      }

      if (ppd->shortnickname != NULL)
      {
        if (strlen(ppd->shortnickname) > 31)
	{
	  if (verbose >= 0)
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    _cupsLangPuts(stdout,
	                  _("      **FAIL**  Bad ShortNickName - longer "
			    "than 31 chars.\n"
			    "                REF: Pages 64-65, section 5.3."));
          }

	  errors ++;
	}
	else if (verbose > 0)
	  _cupsLangPuts(stdout, _("        PASS    ShortNickName"));
      }
      else if (ppdversion >= 43)
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED ShortNickName\n"
			  "                REF: Page 64-65, section 5.3."));
        }

	errors ++;
      }

      if (ppd->patches != NULL && strchr(ppd->patches, '\"') &&
          strstr(ppd->patches, "*End"))
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  Bad JobPatchFile attribute in file\n"
	                  "                REF: Page 24, section 3.4."));
        }

	errors ++;
      }

     /*
      * Check for page sizes without the corresponding ImageableArea or
      * PaperDimension values...
      */

      if (ppd->num_sizes == 0)
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPuts(stdout,
	                _("      **FAIL**  REQUIRED PageSize\n"
			  "                REF: Page 41, section 5.\n"
			  "                REF: Page 99, section 5.14."));
        }

	errors ++;
      }
      else
      {
	for (j = 0, size = ppd->sizes; j < ppd->num_sizes; j ++, size ++)
	{
	 /*
	  * Don't check custom size...
	  */

	  if (!strcmp(size->name, "Custom"))
	    continue;

	 /*
	  * Check for ImageableArea...
	  */

          if (size->left == 0.0 && size->bottom == 0.0 &&
	      size->right == 0.0 && size->top == 0.0)
	  {
	    if (verbose >= 0)
	    {
	      if (!errors && !verbose)
		_cupsLangPuts(stdout, _(" FAIL"));

	      _cupsLangPrintf(stdout,
	                      _("      **FAIL**  REQUIRED ImageableArea for "
			        "PageSize %s\n"
				"                REF: Page 41, section 5.\n"
				"                REF: Page 102, section 5.15."),
	        	      size->name);
            }

	    errors ++;
	  }

	 /*
	  * Check for PaperDimension...
	  */

          if (size->width <= 0.0 && size->length <= 0.0)
	  {
	    if (verbose >= 0)
	    {
	      if (!errors && !verbose)
		_cupsLangPuts(stdout, _(" FAIL"));

	      _cupsLangPrintf(stdout,
	                      _("      **FAIL**  REQUIRED PaperDimension "
			        "for PageSize %s\n"
				"                REF: Page 41, section 5.\n"
				"                REF: Page 103, section 5.15."),
	                      size->name);
            }

	    errors ++;
	  }
	}
      }

     /*
      * Check for valid Resolution, JCLResolution, or SetResolution values...
      */

      if ((option = ppdFindOption(ppd, "Resolution")) == NULL)
	if ((option = ppdFindOption(ppd, "JCLResolution")) == NULL)
          option = ppdFindOption(ppd, "SetResolution");

      if (option != NULL)
      {
        for (j = option->num_choices, choice = option->choices;
	     j > 0;
	     j --, choice ++)
        {
	 /*
	  * Verify that all resolution options are of the form NNNdpi
	  * or NNNxNNNdpi...
	  */

          xdpi = strtol(choice->choice, (char **)&ptr, 10);
	  if (ptr > choice->choice && xdpi > 0)
	  {
	    if (*ptr == 'x')
	      ydpi = strtol(ptr + 1, (char **)&ptr, 10);
	    else
	      ydpi = xdpi;
	  }
	  else
	    ydpi = xdpi;

	  if (xdpi <= 0 || xdpi > 99999 || ydpi <= 0 || ydpi > 99999 ||
	      strcmp(ptr, "dpi"))
	  {
	    if (verbose >= 0)
	    {
	      if (!errors && !verbose)
		_cupsLangPuts(stdout, _(" FAIL"));

	      _cupsLangPrintf(stdout,
	                      _("      **FAIL**  Bad option %s choice %s\n"
			        "                REF: Page 84, section 5.9"),
	                      option->keyword, choice->choice);
            }

	    errors ++;
	  }
	}
      }

      if ((attr = ppdFindAttr(ppd, "1284DeviceID", NULL)) &&
          strcmp(attr->name, "1284DeviceID"))
      {
	if (verbose >= 0)
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPrintf(stdout,
	                  _("      **FAIL**  %s must be 1284DeviceID\n"
			    "                REF: Page 72, section 5.5"),
			  attr->name);
        }

	errors ++;
      }

      errors = check_case(ppd, errors, verbose);

      if (!(warn & WARN_CONSTRAINTS))
        errors = check_constraints(ppd, errors, verbose, 0);

      if (!(warn & WARN_FILTERS) && !(ignore & WARN_FILTERS))
        errors = check_filters(ppd, root, errors, verbose, 0);

      if (!(warn & WARN_PROFILES) && !(ignore & WARN_PROFILES))
        errors = check_profiles(ppd, root, errors, verbose, 0);

      if (!(warn & WARN_SIZES))
	errors = check_sizes(ppd, errors, verbose, 0);

      if (!(warn & WARN_TRANSLATIONS))
        errors = check_translations(ppd, errors, verbose, 0);

      if (!(warn & WARN_DUPLEX))
        errors = check_duplex(ppd, errors, verbose, 0);

      if ((attr = ppdFindAttr(ppd, "cupsLanguages", NULL)) != NULL &&
	  attr->value)
      {
       /*
	* This file contains localizations, check for conformance of the
	* base translation...
	*/

        if ((attr = ppdFindAttr(ppd, "LanguageEncoding", NULL)) != NULL)
	{
	  if (!attr->value || strcmp(attr->value, "ISOLatin1"))
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

            if (verbose >= 0)
	      _cupsLangPrintf(stdout,
	                      _("      **FAIL**  Bad LanguageEncoding %s - "
			        "must be ISOLatin1."),
	                      attr->value ? attr->value : "(null)");

            errors ++;
	  }

          if (!ppd->lang_version || strcmp(ppd->lang_version, "English"))
	  {
	    if (!errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

            if (verbose >= 0)
	      _cupsLangPrintf(stdout,
	                      _("      **FAIL**  Bad LanguageVersion %s - "
			        "must be English."),
	                      ppd->lang_version ? ppd->lang_version : "(null)");

            errors ++;
	  }

	 /*
	  * Loop through all options and choices...
	  */

	  for (option = ppdFirstOption(ppd);
	       option;
	       option = ppdNextOption(ppd))
	  {
	   /*
	    * Check for special characters outside A0 to BF, F7, or F8
	    * that are used for languages other than English.
	    */

	    for (ptr = option->text; *ptr; ptr ++)
	      if ((*ptr & 0x80) && (*ptr & 0xe0) != 0xa0 &&
		  (*ptr & 0xff) != 0xf7 && (*ptr & 0xff) != 0xf8)
		break;

	    if (*ptr)
	    {
	      if (!errors && !verbose)
		_cupsLangPuts(stdout, _(" FAIL"));

	      if (verbose >= 0)
		_cupsLangPrintf(stdout,
				_("      **FAIL**  Default translation "
				  "string for option %s contains 8-bit "
				  "characters."),
				option->keyword);

	      errors ++;
	    }

	    for (j = 0; j < option->num_choices; j ++)
	    {
	     /*
	      * Check for special characters outside A0 to BF, F7, or F8
	      * that are used for languages other than English.
	      */

	      for (ptr = option->choices[j].text; *ptr; ptr ++)
		if ((*ptr & 0x80) && (*ptr & 0xe0) != 0xa0 &&
		    (*ptr & 0xff) != 0xf7 && (*ptr & 0xff) != 0xf8)
		  break;

	      if (*ptr)
	      {
		if (!errors && !verbose)
		  _cupsLangPuts(stdout, _(" FAIL"));

		if (verbose >= 0)
		  _cupsLangPrintf(stdout,
				  _("      **FAIL**  Default translation "
				    "string for option %s choice %s contains "
				    "8-bit characters."),
				  option->keyword,
				  option->choices[j].choice);

		errors ++;
	      }
	    }
	  }
	}
      }

     /*
      * Final pass/fail notification...
      */

      if (errors)
	status = ERROR_CONFORMANCE;
      else if (!verbose)
	_cupsLangPuts(stdout, _(" PASS"));

      if (verbose >= 0)
      {
        check_basics(argv[i]);

	if (warn & WARN_DEFAULTS)
	  errors = check_defaults(ppd, errors, verbose, 1);

	if (warn & WARN_CONSTRAINTS)
	  errors = check_constraints(ppd, errors, verbose, 1);

	if ((warn & WARN_FILTERS) && !(ignore & WARN_FILTERS))
	  errors = check_filters(ppd, root, errors, verbose, 1);

	if ((warn & WARN_PROFILES) && !(ignore & WARN_PROFILES))
	  errors = check_profiles(ppd, root, errors, verbose, 1);

        if (warn & WARN_SIZES)
	  errors = check_sizes(ppd, errors, verbose, 1);
        else
	  errors = check_sizes(ppd, errors, verbose, 2);

	if (warn & WARN_TRANSLATIONS)
	  errors = check_translations(ppd, errors, verbose, 1);

	if (warn & WARN_DUPLEX)
	  errors = check_duplex(ppd, errors, verbose, 1);

       /*
        * Look for legacy duplex keywords...
	*/

	if ((option = ppdFindOption(ppd, "JCLDuplex")) == NULL)
	  if ((option = ppdFindOption(ppd, "EFDuplex")) == NULL)
	    option = ppdFindOption(ppd, "KD03Duplex");

	if (option)
	  _cupsLangPrintf(stdout,
			  _("        WARN    Duplex option keyword %s may not "
			    "work as expected and should be named Duplex.\n"
			    "                REF: Page 122, section 5.17"),
			  option->keyword);

       /*
	* Look for default keywords with no corresponding option...
	*/

	for (j = 0; j < ppd->num_attrs; j ++)
	{
	  attr = ppd->attrs[j];

          if (!strcmp(attr->name, "DefaultColorSpace") ||
	      !strcmp(attr->name, "DefaultColorSep") ||
	      !strcmp(attr->name, "DefaultFont") ||
	      !strcmp(attr->name, "DefaultHalftoneType") ||
	      !strcmp(attr->name, "DefaultImageableArea") ||
	      !strcmp(attr->name, "DefaultLeadingEdge") ||
	      !strcmp(attr->name, "DefaultOutputOrder") ||
	      !strcmp(attr->name, "DefaultPaperDimension") ||
	      !strcmp(attr->name, "DefaultResolution") ||
	      !strcmp(attr->name, "DefaultScreenProc") ||
	      !strcmp(attr->name, "DefaultTransfer"))
	    continue;

	  if (!strncmp(attr->name, "Default", 7) &&
	      !ppdFindOption(ppd, attr->name + 7))
            _cupsLangPrintf(stdout,
	                    _("        WARN    %s has no corresponding "
			      "options."),
	                    attr->name);
	}

        if (ppdversion < 43)
	{
          _cupsLangPrintf(stdout,
	                  _("        WARN    Obsolete PPD version %.1f.\n"
			    "                REF: Page 42, section 5.2."),
	        	  0.1f * ppdversion);
	}

        if (!ppd->lang_encoding && ppdversion < 41)
	{
	  _cupsLangPuts(stdout,
	                _("        WARN    LanguageEncoding required by PPD "
			  "4.3 spec.\n"
			  "                REF: Pages 56-57, section 5.3."));
	}

        if (!ppd->manufacturer && ppdversion < 43)
	{
	  _cupsLangPuts(stdout,
	                _("        WARN    Manufacturer required by PPD "
			  "4.3 spec.\n"
			  "                REF: Pages 58-59, section 5.3."));
	}

       /*
	* Treat a PCFileName attribute longer than 12 characters as
	* a warning and not a hard error...
	*/

        if (!(ignore & WARN_FILENAME) && ppd->pcfilename)
        {
	  if (strlen(ppd->pcfilename) > 12)
	  {
	    _cupsLangPuts(stdout,
			  _("        WARN    PCFileName longer than 8.3 in "
			    "violation of PPD spec.\n"
			    "                REF: Pages 61-62, section "
			    "5.3."));
	  }

	  if (!_cups_strcasecmp(ppd->pcfilename, "unused.ppd"))
	    _cupsLangPuts(stdout,
	                  _("        WARN    PCFileName should contain a "
	                    "unique filename.\n"
			    "                REF: Pages 61-62, section "
			    "5.3."));
        }

        if (!ppd->shortnickname && ppdversion < 43)
	{
	  _cupsLangPuts(stdout,
	                _("        WARN    ShortNickName required by PPD "
			  "4.3 spec.\n"
			  "                REF: Pages 64-65, section 5.3."));
	}

       /*
        * Check the Protocols line and flag PJL + BCP since TBCP is
	* usually used with PJL...
	*/

        if (ppd->protocols)
	{
	  if (strstr(ppd->protocols, "PJL") &&
	      strstr(ppd->protocols, "BCP") &&
	      !strstr(ppd->protocols, "TBCP"))
	  {
	    _cupsLangPuts(stdout,
	                  _("        WARN    Protocols contains both PJL "
			    "and BCP; expected TBCP.\n"
			    "                REF: Pages 78-79, section 5.7."));
	  }

	  if (strstr(ppd->protocols, "PJL") &&
	      (!ppd->jcl_begin || !ppd->jcl_end || !ppd->jcl_ps))
	  {
	    _cupsLangPuts(stdout,
	                  _("        WARN    Protocols contains PJL but JCL "
			    "attributes are not set.\n"
			    "                REF: Pages 78-79, section 5.7."));
	  }
	}

       /*
        * Check for options with a common prefix, e.g. Duplex and Duplexer,
	* which are errors according to the spec but won't cause problems
	* with CUPS specifically...
	*/

	for (j = 0, group = ppd->groups; j < ppd->num_groups; j ++, group ++)
	  for (k = 0, option = group->options;
	       k < group->num_options;
	       k ++, option ++)
	  {
	    len = strlen(option->keyword);

	    for (m = 0, group2 = ppd->groups;
		 m < ppd->num_groups;
		 m ++, group2 ++)
	      for (n = 0, option2 = group2->options;
	           n < group2->num_options;
		   n ++, option2 ++)
		if (option != option2 &&
	            len < strlen(option2->keyword) &&
	            !strncmp(option->keyword, option2->keyword, len))
		{
		  _cupsLangPrintf(stdout,
		                  _("        WARN    %s shares a common "
				    "prefix with %s\n"
				    "                REF: Page 15, section "
				    "3.2."),
		                  option->keyword, option2->keyword);
        	}
	  }
      }

      if (verbose > 0)
      {
        if (errors)
          _cupsLangPrintf(stdout, _("    %d ERRORS FOUND"), errors);
	else
	  _cupsLangPuts(stdout, _("    NO ERRORS FOUND"));
      }

     /*
      * Then list the options, if "-v" was provided...
      */

      if (verbose > 1)
      {
	_cupsLangPrintf(stdout,
                        "\n"
		        "    language_level = %d\n"
			"    color_device = %s\n"
			"    variable_sizes = %s\n"
			"    landscape = %d",
			ppd->language_level,
			ppd->color_device ? "TRUE" : "FALSE",
			ppd->variable_sizes ? "TRUE" : "FALSE",
			ppd->landscape);

	switch (ppd->colorspace)
	{
	  case PPD_CS_CMYK :
              _cupsLangPuts(stdout, "    colorspace = PPD_CS_CMYK");
	      break;
	  case PPD_CS_CMY :
              _cupsLangPuts(stdout, "    colorspace = PPD_CS_CMY");
	      break;
	  case PPD_CS_GRAY :
              _cupsLangPuts(stdout, "    colorspace = PPD_CS_GRAY");
	      break;
	  case PPD_CS_RGB :
              _cupsLangPuts(stdout, "    colorspace = PPD_CS_RGB");
	      break;
	  default :
              _cupsLangPuts(stdout, "    colorspace = <unknown>");
	      break;
	}

	_cupsLangPrintf(stdout, "    num_emulations = %d",
			ppd->num_emulations);
	for (j = 0; j < ppd->num_emulations; j ++)
	  _cupsLangPrintf(stdout, "        emulations[%d] = %s",
	                  j, ppd->emulations[j].name);

	_cupsLangPrintf(stdout, "    lang_encoding = %s",
	                ppd->lang_encoding);
	_cupsLangPrintf(stdout, "    lang_version = %s",
	                ppd->lang_version);
	_cupsLangPrintf(stdout, "    modelname = %s", ppd->modelname);
	_cupsLangPrintf(stdout, "    ttrasterizer = %s",
        		ppd->ttrasterizer == NULL ? "None" : ppd->ttrasterizer);
	_cupsLangPrintf(stdout, "    manufacturer = %s",
	                ppd->manufacturer);
	_cupsLangPrintf(stdout, "    product = %s", ppd->product);
	_cupsLangPrintf(stdout, "    nickname = %s", ppd->nickname);
	_cupsLangPrintf(stdout, "    shortnickname = %s",
	                ppd->shortnickname);
	_cupsLangPrintf(stdout, "    patches = %d bytes",
        		ppd->patches == NULL ? 0 : (int)strlen(ppd->patches));

	_cupsLangPrintf(stdout, "    num_groups = %d", ppd->num_groups);
	for (j = 0, group = ppd->groups; j < ppd->num_groups; j ++, group ++)
	{
	  _cupsLangPrintf(stdout, "        group[%d] = %s",
	                  j, group->text);

	  for (k = 0, option = group->options; k < group->num_options; k ++, option ++)
	  {
	    _cupsLangPrintf(stdout,
	                    "            options[%d] = %s (%s) %s %s %.0f "
			    "(%d choices)",
	        	    k, option->keyword, option->text, uis[option->ui],
			    sections[option->section], option->order,
			    option->num_choices);

            if (!strcmp(option->keyword, "PageSize") ||
        	!strcmp(option->keyword, "PageRegion"))
            {
              for (m = option->num_choices, choice = option->choices;
		   m > 0;
		   m --, choice ++)
	      {
		size = ppdPageSize(ppd, choice->choice);

		if (size == NULL)
		  _cupsLangPrintf(stdout,
                                  "                %s (%s) = ERROR%s",
				  choice->choice, choice->text,
				  !strcmp(option->defchoice, choice->choice)
				      ? " *" : "");
        	else
		  _cupsLangPrintf(stdout,
                                  "                %s (%s) = %.2fx%.2fin "
				  "(%.1f,%.1f,%.1f,%.1f)%s",
		        	  choice->choice, choice->text,
				  size->width / 72.0, size->length / 72.0,
				  size->left / 72.0, size->bottom / 72.0,
				  size->right / 72.0, size->top / 72.0,
				  !strcmp(option->defchoice, choice->choice)
				      ? " *" : "");
              }
	    }
	    else
	    {
	      for (m = option->num_choices, choice = option->choices;
		   m > 0;
		   m --, choice ++)
	      {
		_cupsLangPrintf(stdout, "                %s (%s)%s",
		                choice->choice, choice->text,
				!strcmp(option->defchoice, choice->choice)
				    ? " *" : "");
	      }
            }
	  }
	}

	_cupsLangPrintf(stdout, "    num_consts = %d",
	                ppd->num_consts);
	for (j = 0; j < ppd->num_consts; j ++)
	  _cupsLangPrintf(stdout,
                	  "        consts[%d] = *%s %s *%s %s",
        		  j, ppd->consts[j].option1, ppd->consts[j].choice1,
			  ppd->consts[j].option2, ppd->consts[j].choice2);

	_cupsLangPrintf(stdout, "    num_profiles = %d",
	                ppd->num_profiles);
	for (j = 0; j < ppd->num_profiles; j ++)
	  _cupsLangPrintf(stdout,
                	  "        profiles[%d] = %s/%s %.3f %.3f "
			  "[ %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f %.3f ]",
        		  j, ppd->profiles[j].resolution,
			  ppd->profiles[j].media_type,
			  ppd->profiles[j].gamma, ppd->profiles[j].density,
			  ppd->profiles[j].matrix[0][0],
			  ppd->profiles[j].matrix[0][1],
			  ppd->profiles[j].matrix[0][2],
			  ppd->profiles[j].matrix[1][0],
			  ppd->profiles[j].matrix[1][1],
			  ppd->profiles[j].matrix[1][2],
			  ppd->profiles[j].matrix[2][0],
			  ppd->profiles[j].matrix[2][1],
			  ppd->profiles[j].matrix[2][2]);

	_cupsLangPrintf(stdout, "    num_fonts = %d", ppd->num_fonts);
	for (j = 0; j < ppd->num_fonts; j ++)
	  _cupsLangPrintf(stdout, "        fonts[%d] = %s",
	                  j, ppd->fonts[j]);

	_cupsLangPrintf(stdout, "    num_attrs = %d", ppd->num_attrs);
	for (j = 0; j < ppd->num_attrs; j ++)
	  _cupsLangPrintf(stdout,
	                  "        attrs[%d] = %s %s%s%s: \"%s\"", j,
	        	  ppd->attrs[j]->name, ppd->attrs[j]->spec,
			  ppd->attrs[j]->text[0] ? "/" : "",
			  ppd->attrs[j]->text,
			  ppd->attrs[j]->value ?
			      ppd->attrs[j]->value : "(null)");
      }

      ppdClose(ppd);
    }

  if (!files)
    usage();

  return (status);
}


/*
 * 'check_basics()' - Check for CR LF, mixed line endings, and blank lines.
 */

static void
check_basics(const char *filename)	/* I - PPD file to check */
{
  cups_file_t	*fp;			/* File pointer */
  int		ch;			/* Current character */
  int		col,			/* Current column */
		whitespace;		/* Only seen whitespace? */
  int		eol;			/* Line endings */
  int		linenum;		/* Line number */
  int		mixed;			/* Mixed line endings? */


  if ((fp = cupsFileOpen(filename, "r")) == NULL)
    return;

  linenum    = 1;
  col        = 0;
  eol        = EOL_NONE;
  mixed      = 0;
  whitespace = 1;

  while ((ch = cupsFileGetChar(fp)) != EOF)
  {
    if (ch == '\r' || ch == '\n')
    {
      if (ch == '\n')
      {
	if (eol == EOL_NONE)
	  eol = EOL_LF;
	else if (eol != EOL_LF)
	  mixed = 1;
      }
      else if (ch == '\r')
      {
	if (cupsFilePeekChar(fp) == '\n')
	{
	  cupsFileGetChar(fp);

          if (eol == EOL_NONE)
	    eol = EOL_CRLF;
	  else if (eol != EOL_CRLF)
	    mixed = 1;
	}
	else if (eol == EOL_NONE)
	  eol = EOL_CR;
        else if (eol != EOL_CR)
	  mixed = 1;
      }

      if (col > 0 && whitespace)
	_cupsLangPrintf(stdout,
		        _("        WARN    Line %d only contains whitespace."),
			linenum);

      linenum ++;
      col        = 0;
      whitespace = 1;
    }
    else
    {
      if (ch != ' ' && ch != '\t')
        whitespace = 0;

      col ++;
    }
  }

  if (mixed)
    _cupsLangPuts(stdout,
		  _("        WARN    File contains a mix of CR, LF, and "
		    "CR LF line endings."));

  if (eol == EOL_CRLF)
    _cupsLangPuts(stdout,
		  _("        WARN    Non-Windows PPD files should use lines "
		    "ending with only LF, not CR LF."));

  cupsFileClose(fp);
}


/*
 * 'check_constraints()' - Check UIConstraints in the PPD file.
 */

static int				/* O - Errors found */
check_constraints(ppd_file_t *ppd,	/* I - PPD file */
                  int        errors,	/* I - Errors found */
                  int        verbose,	/* I - Verbosity level */
                  int        warn)	/* I - Warnings only? */
{
  int			i;		/* Looping var */
  const char		*prefix;	/* WARN/FAIL prefix */
  ppd_const_t		*c;		/* Current UIConstraints data */
  ppd_attr_t		*constattr;	/* Current cupsUIConstraints attribute */
  const char		*vptr;		/* Pointer into constraint value */
  char			option[PPD_MAX_NAME],
  					/* Option name/MainKeyword */
			choice[PPD_MAX_NAME],
					/* Choice/OptionKeyword */
			*ptr;		/* Pointer into option or choice */
  int			num_options;	/* Number of options */
  cups_option_t		*options;	/* Options */
  ppd_option_t		*o;		/* PPD option */


  prefix = warn ? "  WARN  " : "**FAIL**";


 /*
  * See what kind of constraint data we have in the PPD...
  */

  if ((constattr = ppdFindAttr(ppd, "cupsUIConstraints", NULL)) != NULL)
  {
   /*
    * Check new-style cupsUIConstraints data...
    */

    for (; constattr;
         constattr = ppdFindNextAttr(ppd, "cupsUIConstraints", NULL))
    {
      if (!constattr->value)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Empty cupsUIConstraints %s"),
			prefix, constattr->spec);

	if (!warn)
	  errors ++;

        continue;
      }

      for (i = 0, vptr = strchr(constattr->value, '*');
           vptr;
	   i ++, vptr = strchr(vptr + 1, '*'));

      if (i == 0)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Bad cupsUIConstraints %s: \"%s\""),
			prefix, constattr->spec, constattr->value);

	if (!warn)
	  errors ++;

        continue;
      }

      cupsArraySave(ppd->sorted_attrs);

      if (constattr->spec[0] &&
          !ppdFindAttr(ppd, "cupsUIResolver", constattr->spec))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Missing cupsUIResolver %s"),
			prefix, constattr->spec);

	if (!warn)
	  errors ++;
      }

      cupsArrayRestore(ppd->sorted_attrs);

      num_options = 0;
      options     = NULL;

      for (vptr = strchr(constattr->value, '*');
           vptr;
	   vptr = strchr(vptr, '*'))
      {
       /*
        * Extract "*Option Choice" or just "*Option"...
	*/

        for (vptr ++, ptr = option; *vptr && !isspace(*vptr & 255); vptr ++)
	  if (ptr < (option + sizeof(option) - 1))
	    *ptr++ = *vptr;

        *ptr = '\0';

        while (isspace(*vptr & 255))
	  vptr ++;

        if (*vptr == '*')
	  choice[0] = '\0';
	else
	{
	  for (ptr = choice; *vptr && !isspace(*vptr & 255); vptr ++)
	    if (ptr < (choice + sizeof(choice) - 1))
	      *ptr++ = *vptr;

	  *ptr = '\0';
	}

        if (!_cups_strncasecmp(option, "Custom", 6) && !_cups_strcasecmp(choice, "True"))
	{
	  _cups_strcpy(option, option + 6);
	  strlcpy(choice, "Custom", sizeof(choice));
	}

        if ((o = ppdFindOption(ppd, option)) == NULL)
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPrintf(stdout,
			  _("      %s  Missing option %s in "
			    "cupsUIConstraints %s: \"%s\""),
			  prefix, option, constattr->spec, constattr->value);

	  if (!warn)
	    errors ++;

	  continue;
	}

        if (choice[0] && !ppdFindChoice(o, choice))
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPrintf(stdout,
			  _("      %s  Missing choice *%s %s in "
			    "cupsUIConstraints %s: \"%s\""),
			  prefix, option, choice, constattr->spec,
			  constattr->value);

	  if (!warn)
	    errors ++;

	  continue;
	}

        if (choice[0])
	  num_options = cupsAddOption(option, choice, num_options, &options);
	else
	{
	  for (i = 0; i < o->num_choices; i ++)
	    if (_cups_strcasecmp(o->choices[i].choice, "None") &&
	        _cups_strcasecmp(o->choices[i].choice, "Off") &&
	        _cups_strcasecmp(o->choices[i].choice, "False"))
            {
	      num_options = cupsAddOption(option, o->choices[i].choice,
	                                  num_options, &options);
              break;
	    }
	}
      }

     /*
      * Resolvers must list at least two options...
      */

      if (num_options < 2)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  cupsUIResolver %s does not list at least "
			  "two different options."),
			prefix, constattr->spec);

	if (!warn)
	  errors ++;
      }

     /*
      * Test the resolver...
      */

      if (!cupsResolveConflicts(ppd, NULL, NULL, &num_options, &options))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  cupsUIResolver %s causes a loop."),
			prefix, constattr->spec);

	if (!warn)
	  errors ++;
      }

      cupsFreeOptions(num_options, options);
    }
  }
  else
  {
   /*
    * Check old-style [Non]UIConstraints data...
    */

    for (i = ppd->num_consts, c = ppd->consts; i > 0; i --, c ++)
    {
      if (!_cups_strncasecmp(c->option1, "Custom", 6) &&
          !_cups_strcasecmp(c->choice1, "True"))
      {
	strlcpy(option, c->option1 + 6, sizeof(option));
	strlcpy(choice, "Custom", sizeof(choice));
      }
      else
      {
	strlcpy(option, c->option1, sizeof(option));
	strlcpy(choice, c->choice1, sizeof(choice));
      }

      if ((o = ppdFindOption(ppd, option)) == NULL)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Missing option %s in "
			  "UIConstraints \"*%s %s *%s %s\"."),
			prefix, c->option1,
			c->option1, c->choice1, c->option2, c->choice2);

	if (!warn)
	  errors ++;
      }
      else if (choice[0] && !ppdFindChoice(o, choice))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Missing choice *%s %s in "
			  "UIConstraints \"*%s %s *%s %s\"."),
			prefix, c->option1, c->choice1,
			c->option1, c->choice1, c->option2, c->choice2);

	if (!warn)
	  errors ++;
      }

      if (!_cups_strncasecmp(c->option2, "Custom", 6) &&
          !_cups_strcasecmp(c->choice2, "True"))
      {
	strlcpy(option, c->option2 + 6, sizeof(option));
	strlcpy(choice, "Custom", sizeof(choice));
      }
      else
      {
	strlcpy(option, c->option2, sizeof(option));
	strlcpy(choice, c->choice2, sizeof(choice));
      }

      if ((o = ppdFindOption(ppd, option)) == NULL)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Missing option %s in "
			  "UIConstraints \"*%s %s *%s %s\"."),
			prefix, c->option2,
			c->option1, c->choice1, c->option2, c->choice2);

	if (!warn)
	  errors ++;
      }
      else if (choice[0] && !ppdFindChoice(o, choice))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  Missing choice *%s %s in "
			  "UIConstraints \"*%s %s *%s %s\"."),
			prefix, c->option2, c->choice2,
			c->option1, c->choice1, c->option2, c->choice2);

	if (!warn)
	  errors ++;
      }
    }
  }

  return (errors);
}


/*
 * 'check_case()' - Check that there are no duplicate groups, options,
 *                  or choices that differ only by case.
 */

static int				/* O - Errors found */
check_case(ppd_file_t *ppd,		/* I - PPD file */
           int        errors,		/* I - Errors found */
	   int        verbose)		/* I - Verbosity level */
{
  int		i, j;			/* Looping vars */
  ppd_group_t	*groupa,		/* First group */
		*groupb;		/* Second group */
  ppd_option_t	*optiona,		/* First option */
		*optionb;		/* Second option */
  ppd_choice_t	*choicea,		/* First choice */
		*choiceb;		/* Second choice */


 /*
  * Check that the groups do not have any duplicate names...
  */

  for (i = ppd->num_groups, groupa = ppd->groups; i > 1; i --, groupa ++)
    for (j = i - 1, groupb = groupa + 1; j > 0; j --, groupb ++)
      if (!_cups_strcasecmp(groupa->name, groupb->name))
      {
	if (!errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      **FAIL**  Group names %s and %s differ only "
			    "by case."),
			  groupa->name, groupb->name);

	errors ++;
      }

 /*
  * Check that the options do not have any duplicate names...
  */

  for (optiona = ppdFirstOption(ppd); optiona; optiona = ppdNextOption(ppd))
  {
    cupsArraySave(ppd->options);
    for (optionb = ppdNextOption(ppd); optionb; optionb = ppdNextOption(ppd))
      if (!_cups_strcasecmp(optiona->keyword, optionb->keyword))
      {
	if (!errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      **FAIL**  Option names %s and %s differ only "
			    "by case."),
			  optiona->keyword, optionb->keyword);

	errors ++;
      }
    cupsArrayRestore(ppd->options);

   /*
    * Then the choices...
    */

    for (i = optiona->num_choices, choicea = optiona->choices;
         i > 1;
	 i --, choicea ++)
      for (j = i - 1, choiceb = choicea + 1; j > 0; j --, choiceb ++)
        if (!strcmp(choicea->choice, choiceb->choice))
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  if (verbose >= 0)
	    _cupsLangPrintf(stdout,
			    _("      **FAIL**  Multiple occurrences of "
			      "option %s choice name %s."),
			    optiona->keyword, choicea->choice);

	  errors ++;

	  choicea ++;
	  i --;
	  break;
	}
        else if (!_cups_strcasecmp(choicea->choice, choiceb->choice))
	{
	  if (!errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  if (verbose >= 0)
	    _cupsLangPrintf(stdout,
			    _("      **FAIL**  Option %s choice names %s and "
			      "%s differ only by case."),
			    optiona->keyword, choicea->choice, choiceb->choice);

	  errors ++;
	}
  }

 /*
  * Return the number of errors found...
  */

  return (errors);
}


/*
 * 'check_defaults()' - Check default option keywords in the PPD file.
 */

static int				/* O - Errors found */
check_defaults(ppd_file_t *ppd,		/* I - PPD file */
	       int        errors,	/* I - Errors found */
	       int        verbose,	/* I - Verbosity level */
	       int        warn)		/* I - Warnings only? */
{
  int		j, k;			/* Looping vars */
  ppd_attr_t	*attr;			/* PPD attribute */
  ppd_option_t	*option;		/* Standard UI option */
  const char	*prefix;		/* WARN/FAIL prefix */


  prefix = warn ? "  WARN  " : "**FAIL**";

  ppdMarkDefaults(ppd);
  if (ppdConflicts(ppd))
  {
    if (!warn && !errors && !verbose)
      _cupsLangPuts(stdout, _(" FAIL"));

    if (verbose >= 0)
      _cupsLangPrintf(stdout,
		      _("      %s  Default choices conflicting."), prefix);

    show_conflicts(ppd, prefix);

    if (!warn)
      errors ++;
  }

  for (j = 0; j < ppd->num_attrs; j ++)
  {
    attr = ppd->attrs[j];

    if (!strcmp(attr->name, "DefaultColorSpace") ||
	!strcmp(attr->name, "DefaultFont") ||
	!strcmp(attr->name, "DefaultHalftoneType") ||
	!strcmp(attr->name, "DefaultImageableArea") ||
	!strcmp(attr->name, "DefaultLeadingEdge") ||
	!strcmp(attr->name, "DefaultOutputOrder") ||
	!strcmp(attr->name, "DefaultPaperDimension") ||
	!strcmp(attr->name, "DefaultResolution") ||
	!strcmp(attr->name, "DefaultTransfer"))
      continue;

    if (!strncmp(attr->name, "Default", 7))
    {
      if ((option = ppdFindOption(ppd, attr->name + 7)) != NULL &&
	  strcmp(attr->value, "Unknown"))
      {
       /*
	* Check that the default option value matches a choice...
	*/

	for (k = 0; k < option->num_choices; k ++)
	  if (!strcmp(option->choices[k].choice, attr->value))
	    break;

	if (k >= option->num_choices)
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  if (verbose >= 0)
	    _cupsLangPrintf(stdout,
			    _("      %s  %s %s does not exist."),
			    prefix, attr->name, attr->value);

          if (!warn)
	    errors ++;
	}
      }
    }
  }

  return (errors);
}


/*
 * 'check_duplex()' - Check duplex keywords in the PPD file.
 */

static int				/* O - Errors found */
check_duplex(ppd_file_t *ppd,		/* I - PPD file */
             int        errors,		/* I - Error found */
	     int        verbose,	/* I - Verbosity level */
             int        warn)		/* I - Warnings only? */
{
  int		i;			/* Looping var */
  ppd_option_t	*option;		/* PPD option */
  ppd_choice_t	*choice;		/* Current choice */
  const char	*prefix;		/* Message prefix */


  prefix = warn ? "  WARN  " : "**FAIL**";

 /*
  * Check for a duplex option, and for standard values...
  */

  if ((option = ppdFindOption(ppd, "Duplex")) != NULL)
  {
    if (!ppdFindChoice(option, "None"))
    {
      if (verbose >= 0)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	_cupsLangPrintf(stdout,
			_("      %s  REQUIRED %s does not define "
			  "choice None.\n"
			  "                REF: Page 122, section 5.17"),
			prefix, option->keyword);
      }

      if (!warn)
	errors ++;
    }

    for (i = option->num_choices, choice = option->choices;
	 i > 0;
	 i --, choice ++)
      if (strcmp(choice->choice, "None") &&
	  strcmp(choice->choice, "DuplexNoTumble") &&
	  strcmp(choice->choice, "DuplexTumble") &&
	  strcmp(choice->choice, "SimplexTumble"))
      {
	if (verbose >= 0)
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  _cupsLangPrintf(stdout,
			  _("      %s  Bad %s choice %s.\n"
			    "                REF: Page 122, section 5.17"),
			  prefix, option->keyword, choice->choice);
	}

	if (!warn)
	  errors ++;
      }
  }

  return (errors);
}


/*
 * 'check_filters()' - Check filters in the PPD file.
 */

static int				/* O - Errors found */
check_filters(ppd_file_t *ppd,		/* I - PPD file */
              const char *root,		/* I - Root directory */
	      int        errors,	/* I - Errors found */
	      int        verbose,	/* I - Verbosity level */
	      int        warn)		/* I - Warnings only? */
{
  ppd_attr_t	*attr;			/* PPD attribute */
  const char	*ptr;			/* Pointer into string */
  char		super[16],		/* Super-type for filter */
		type[256],		/* Type for filter */
		dstsuper[16],		/* Destination super-type for filter */
		dsttype[256],		/* Destination type for filter */
		program[1024],		/* Program/filter name */
		pathprog[1024];		/* Complete path to program/filter */
  int		cost;			/* Cost of filter */
  const char	*prefix;		/* WARN/FAIL prefix */
  struct stat	fileinfo;		/* File information */


  prefix = warn ? "  WARN  " : "**FAIL**";

 /*
  * cupsFilter
  */

  for (attr = ppdFindAttr(ppd, "cupsFilter", NULL);
       attr;
       attr = ppdFindNextAttr(ppd, "cupsFilter", NULL))
  {
    if (strcmp(attr->name, "cupsFilter"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "cupsFilter");

      if (!warn)
        errors ++;
    }

    if (!attr->value ||
        sscanf(attr->value, "%15[^/]/%255s%d%*[ \t]%1023[^\n]", super, type,
               &cost, program) != 4)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad cupsFilter value \"%s\"."),
			prefix, attr->value);

      if (!warn)
        errors ++;

      continue;
    }

    if (!strncmp(program, "maxsize(", 8))
    {
      char	*mptr;			/* Pointer into maxsize(nnnn) program */

      strtoll(program + 8, &mptr, 10);

      if (*mptr != ')')
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      %s  Bad cupsFilter value \"%s\"."),
			  prefix, attr->value);

	if (!warn)
	  errors ++;

	continue;
      }

      mptr ++;
      while (_cups_isspace(*mptr))
	mptr ++;

      _cups_strcpy(program, mptr);
    }

    if (strcmp(program, "-"))
    {
      if (program[0] == '/')
	snprintf(pathprog, sizeof(pathprog), "%s%s", root, program);
      else
      {
	if ((ptr = getenv("CUPS_SERVERBIN")) == NULL)
	  ptr = CUPS_SERVERBIN;

	if (*ptr == '/' || !*root)
	  snprintf(pathprog, sizeof(pathprog), "%s%s/filter/%s", root, ptr,
		   program);
	else
	  snprintf(pathprog, sizeof(pathprog), "%s/%s/filter/%s", root, ptr,
		   program);
      }

      if (stat(pathprog, &fileinfo))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
	                  prefix, "cupsFilter", pathprog);

	if (!warn)
	  errors ++;
      }
      else if (fileinfo.st_uid != 0 ||
               (fileinfo.st_mode & MODE_WRITE) ||
	       (fileinfo.st_mode & MODE_MASK) != MODE_PROGRAM)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
	                  _("      %s  Bad permissions on %s file \"%s\"."),
			  prefix, "cupsFilter", pathprog);

	if (!warn)
	  errors ++;
      }
      else
        errors = valid_path("cupsFilter", pathprog, errors, verbose, warn);
    }
  }

 /*
  * cupsFilter2
  */

  for (attr = ppdFindAttr(ppd, "cupsFilter2", NULL);
       attr;
       attr = ppdFindNextAttr(ppd, "cupsFilter2", NULL))
  {
    if (strcmp(attr->name, "cupsFilter2"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "cupsFilter2");

      if (!warn)
        errors ++;
    }

    if (!attr->value ||
	sscanf(attr->value, "%15[^/]/%255s%*[ \t]%15[^/]/%255s%d%*[ \t]%1023[^\n]",
	       super, type, dstsuper, dsttype, &cost, program) != 6)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad cupsFilter2 value \"%s\"."),
			prefix, attr->value);

      if (!warn)
        errors ++;

      continue;
    }

    if (!strncmp(program, "maxsize(", 8))
    {
      char	*mptr;			/* Pointer into maxsize(nnnn) program */

      strtoll(program + 8, &mptr, 10);

      if (*mptr != ')')
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      %s  Bad cupsFilter2 value \"%s\"."),
			  prefix, attr->value);

	if (!warn)
	  errors ++;

	continue;
      }

      mptr ++;
      while (_cups_isspace(*mptr))
	mptr ++;

      _cups_strcpy(program, mptr);
    }

    if (strcmp(program, "-"))
    {
      if (strncmp(program, "maxsize(", 8) &&
          (ptr = strchr(program + 8, ')')) != NULL)
      {
	ptr ++;
	while (_cups_isspace(*ptr))
	  ptr ++;

	_cups_strcpy(program, ptr);
      }

      if (program[0] == '/')
	snprintf(pathprog, sizeof(pathprog), "%s%s", root, program);
      else
      {
	if ((ptr = getenv("CUPS_SERVERBIN")) == NULL)
	  ptr = CUPS_SERVERBIN;

	if (*ptr == '/' || !*root)
	  snprintf(pathprog, sizeof(pathprog), "%s%s/filter/%s", root, ptr,
		   program);
	else
	  snprintf(pathprog, sizeof(pathprog), "%s/%s/filter/%s", root, ptr,
		   program);
      }

      if (stat(pathprog, &fileinfo))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
	                  prefix, "cupsFilter2", pathprog);

	if (!warn)
	  errors ++;
      }
      else if (fileinfo.st_uid != 0 ||
               (fileinfo.st_mode & MODE_WRITE) ||
	       (fileinfo.st_mode & MODE_MASK) != MODE_PROGRAM)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
	                  _("      %s  Bad permissions on %s file \"%s\"."),
			  prefix, "cupsFilter2", pathprog);

	if (!warn)
	  errors ++;
      }
      else
        errors = valid_path("cupsFilter2", pathprog, errors, verbose, warn);
    }
  }

 /*
  * cupsPreFilter
  */

  for (attr = ppdFindAttr(ppd, "cupsPreFilter", NULL);
       attr;
       attr = ppdFindNextAttr(ppd, "cupsPreFilter", NULL))
  {
    if (strcmp(attr->name, "cupsPreFilter"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "cupsPreFilter");

      if (!warn)
        errors ++;
    }

    if (!attr->value ||
	sscanf(attr->value, "%15[^/]/%255s%d%*[ \t]%1023[^\n]", super, type,
	       &cost, program) != 4)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad cupsPreFilter value \"%s\"."),
			prefix, attr->value ? attr->value : "");

      if (!warn)
        errors ++;
    }
    else if (strcmp(program, "-"))
    {
      if (program[0] == '/')
	snprintf(pathprog, sizeof(pathprog), "%s%s", root, program);
      else
      {
	if ((ptr = getenv("CUPS_SERVERBIN")) == NULL)
	  ptr = CUPS_SERVERBIN;

	if (*ptr == '/' || !*root)
	  snprintf(pathprog, sizeof(pathprog), "%s%s/filter/%s", root, ptr,
		   program);
	else
	  snprintf(pathprog, sizeof(pathprog), "%s/%s/filter/%s", root, ptr,
		   program);
      }

      if (stat(pathprog, &fileinfo))
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
	                  prefix, "cupsPreFilter", pathprog);

        if (!warn)
	  errors ++;
      }
      else if (fileinfo.st_uid != 0 ||
               (fileinfo.st_mode & MODE_WRITE) ||
	       (fileinfo.st_mode & MODE_MASK) != MODE_PROGRAM)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
	                  _("      %s  Bad permissions on %s file \"%s\"."),
			  prefix, "cupsPreFilter", pathprog);

	if (!warn)
	  errors ++;
      }
      else
        errors = valid_path("cupsPreFilter", pathprog, errors, verbose, warn);
    }
  }

#ifdef __APPLE__
 /*
  * APDialogExtension
  */

  for (attr = ppdFindAttr(ppd, "APDialogExtension", NULL);
       attr != NULL;
       attr = ppdFindNextAttr(ppd, "APDialogExtension", NULL))
  {
    if (strcmp(attr->name, "APDialogExtension"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "APDialogExtension");

      if (!warn)
        errors ++;
    }

    snprintf(pathprog, sizeof(pathprog), "%s%s", root,
             attr->value ? attr->value : "(null)");

    if (!attr->value || stat(pathprog, &fileinfo))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
			prefix, "APDialogExtension", pathprog);

      if (!warn)
	errors ++;
    }
    else if (fileinfo.st_uid != 0 ||
	     (fileinfo.st_mode & MODE_WRITE) ||
	     (fileinfo.st_mode & MODE_MASK) != MODE_DIRECTORY)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
	                _("      %s  Bad permissions on %s file \"%s\"."),
			prefix, "APDialogExtension", pathprog);

      if (!warn)
	errors ++;
    }
    else
      errors = valid_path("APDialogExtension", pathprog, errors, verbose,
                          warn);
  }

 /*
  * APPrinterIconPath
  */

  if ((attr = ppdFindAttr(ppd, "APPrinterIconPath", NULL)) != NULL)
  {
    if (strcmp(attr->name, "APPrinterIconPath"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "APPrinterIconPath");

      if (!warn)
        errors ++;
    }

    snprintf(pathprog, sizeof(pathprog), "%s%s", root,
             attr->value ? attr->value : "(null)");

    if (!attr->value || stat(pathprog, &fileinfo))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
			prefix, "APPrinterIconPath", pathprog);

      if (!warn)
	errors ++;
    }
    else if (fileinfo.st_uid != 0 ||
	     (fileinfo.st_mode & MODE_WRITE) ||
	     (fileinfo.st_mode & MODE_MASK) != MODE_DATAFILE)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
	                _("      %s  Bad permissions on %s file \"%s\"."),
			prefix, "APPrinterIconPath", pathprog);

      if (!warn)
	errors ++;
    }
    else
      errors = valid_path("APPrinterIconPath", pathprog, errors, verbose,
                          warn);
  }

 /*
  * APPrinterLowInkTool
  */

  if ((attr = ppdFindAttr(ppd, "APPrinterLowInkTool", NULL)) != NULL)
  {
    if (strcmp(attr->name, "APPrinterLowInkTool"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "APPrinterLowInkTool");

      if (!warn)
        errors ++;
    }

    snprintf(pathprog, sizeof(pathprog), "%s%s", root,
             attr->value ? attr->value : "(null)");

    if (!attr->value || stat(pathprog, &fileinfo))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
			prefix, "APPrinterLowInkTool", pathprog);

      if (!warn)
	errors ++;
    }
    else if (fileinfo.st_uid != 0 ||
	     (fileinfo.st_mode & MODE_WRITE) ||
	     (fileinfo.st_mode & MODE_MASK) != MODE_DIRECTORY)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
	                _("      %s  Bad permissions on %s file \"%s\"."),
			prefix, "APPrinterLowInkTool", pathprog);

      if (!warn)
	errors ++;
    }
    else
      errors = valid_path("APPrinterLowInkTool", pathprog, errors, verbose,
                          warn);
  }

 /*
  * APPrinterUtilityPath
  */

  if ((attr = ppdFindAttr(ppd, "APPrinterUtilityPath", NULL)) != NULL)
  {
    if (strcmp(attr->name, "APPrinterUtilityPath"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "APPrinterUtilityPath");

      if (!warn)
        errors ++;
    }

    snprintf(pathprog, sizeof(pathprog), "%s%s", root,
             attr->value ? attr->value : "(null)");

    if (!attr->value || stat(pathprog, &fileinfo))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
			prefix, "APPrinterUtilityPath", pathprog);

      if (!warn)
	errors ++;
    }
    else if (fileinfo.st_uid != 0 ||
	     (fileinfo.st_mode & MODE_WRITE) ||
	     (fileinfo.st_mode & MODE_MASK) != MODE_DIRECTORY)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
	                _("      %s  Bad permissions on %s file \"%s\"."),
			prefix, "APPrinterUtilityPath", pathprog);

      if (!warn)
	errors ++;
    }
    else
      errors = valid_path("APPrinterUtilityPath", pathprog, errors, verbose,
                          warn);
  }

 /*
  * APScanAppBundleID and APScanAppPath
  */

  if ((attr = ppdFindAttr(ppd, "APScanAppPath", NULL)) != NULL)
  {
    if (strcmp(attr->name, "APScanAppPath"))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad spelling of %s - should be %s."),
			prefix, attr->name, "APScanAppPath");

      if (!warn)
        errors ++;
    }

    if (!attr->value || stat(attr->value, &fileinfo))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
			prefix, "APScanAppPath",
			attr->value ? attr->value : "<NULL>");

      if (!warn)
	errors ++;
    }
    else if (fileinfo.st_uid != 0 ||
	     (fileinfo.st_mode & MODE_WRITE) ||
	     (fileinfo.st_mode & MODE_MASK) != MODE_DIRECTORY)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
	                _("      %s  Bad permissions on %s file \"%s\"."),
			prefix, "APScanAppPath", attr->value);

      if (!warn)
	errors ++;
    }
    else
      errors = valid_path("APScanAppPath", attr->value, errors, verbose,
                          warn);

    if (ppdFindAttr(ppd, "APScanAppBundleID", NULL))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Cannot provide both "
				  "APScanAppPath and APScanAppBundleID."),
			prefix);

      if (!warn)
	errors ++;
    }
  }
#endif	/* __APPLE__ */

  return (errors);
}


/*
 * 'check_profiles()' - Check ICC color profiles in the PPD file.
 */

static int				/* O - Errors found */
check_profiles(ppd_file_t *ppd,		/* I - PPD file */
               const char *root,	/* I - Root directory */
	       int        errors,	/* I - Errors found */
	       int        verbose,	/* I - Verbosity level */
	       int        warn)		/* I - Warnings only? */
{
  int		i;			/* Looping var */
  ppd_attr_t	*attr;			/* PPD attribute */
  const char	*ptr;			/* Pointer into string */
  const char	*prefix;		/* WARN/FAIL prefix */
  char		filename[1024];		/* Profile filename */
  struct stat	fileinfo;		/* File information */
  int		num_profiles = 0;	/* Number of profiles */
  unsigned	hash,			/* Current hash value */
		hashes[1000];		/* Hash values of profile names */
  const char	*specs[1000];		/* Specifiers for profiles */


  prefix = warn ? "  WARN  " : "**FAIL**";

  for (attr = ppdFindAttr(ppd, "cupsICCProfile", NULL);
       attr;
       attr = ppdFindNextAttr(ppd, "cupsICCProfile", NULL))
  {
   /*
    * Check for valid selector...
    */

    for (i = 0, ptr = strchr(attr->spec, '.'); ptr; ptr = strchr(ptr + 1, '.'))
      i ++;

    if (!attr->value || i < 2)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Bad cupsICCProfile %s."),
			prefix, attr->spec);

      if (!warn)
        errors ++;

      continue;
    }

   /*
    * Check for valid profile filename...
    */

    if (attr->value[0] == '/')
      snprintf(filename, sizeof(filename), "%s%s", root, attr->value);
    else
    {
      if ((ptr = getenv("CUPS_DATADIR")) == NULL)
	ptr = CUPS_DATADIR;

      if (*ptr == '/' || !*root)
	snprintf(filename, sizeof(filename), "%s%s/profiles/%s", root, ptr,
		 attr->value);
      else
	snprintf(filename, sizeof(filename), "%s/%s/profiles/%s", root, ptr,
		 attr->value);
    }

    if (stat(filename, &fileinfo))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout, _("      %s  Missing %s file \"%s\"."),
	                prefix, "cupsICCProfile", filename);

      if (!warn)
	errors ++;
    }
    else if (fileinfo.st_uid != 0 ||
	     (fileinfo.st_mode & MODE_WRITE) ||
	     (fileinfo.st_mode & MODE_MASK) != MODE_DATAFILE)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
	                _("      %s  Bad permissions on %s file \"%s\"."),
			prefix, "cupsICCProfile", filename);

      if (!warn)
	errors ++;
    }
    else
      errors = valid_path("cupsICCProfile", filename, errors, verbose, warn);

   /*
    * Check for hash collisions...
    */

    hash = _ppdHashName(attr->spec);

    if (num_profiles > 0)
    {
      for (i = 0; i < num_profiles; i ++)
	if (hashes[i] == hash)
	  break;

      if (i < num_profiles)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      %s  cupsICCProfile %s hash value "
			    "collides with %s."), prefix, attr->spec,
			  specs[i]);

	if (!warn)
	  errors ++;
      }
    }

   /*
    * Remember up to 1000 profiles...
    */

    if (num_profiles < 1000)
    {
      hashes[num_profiles] = hash;
      specs[num_profiles]  = attr->spec;
      num_profiles ++;
    }
  }

  return (errors);
}


/*
 * 'check_sizes()' - Check media sizes in the PPD file.
 */

static int				/* O - Errors found */
check_sizes(ppd_file_t *ppd,		/* I - PPD file */
	    int        errors,		/* I - Errors found */
	    int        verbose,		/* I - Verbosity level */
	    int        warn)		/* I - Warnings only? */
{
  int		i;			/* Looping var */
  ppd_size_t	*size;			/* Current size */
  int		width,			/* Custom width */
		length;			/* Custom length */
  const char	*prefix;		/* WARN/FAIL prefix */
  ppd_option_t	*page_size,		/* PageSize option */
		*page_region;		/* PageRegion option */
  pwg_media_t	*pwg_media;		/* PWG media */
  char		buf[PPD_MAX_NAME];	/* PapeSize name that is supposed to be */
  const char	*ptr;			/* Pointer into string */
  int		width_2540ths,		/* PageSize width in 2540ths */
		length_2540ths;		/* PageSize length in 2540ths */
  int		is_ok;			/* Flag for PageSize name verification */
  double	width_tmp,		/* Width after rounded up */
		length_tmp,		/* Length after rounded up */
		width_inch,		/* Width in inches */
		length_inch,		/* Length in inches */
		width_mm,		/* Width in millimeters */
		length_mm;		/* Length in millimeters */


  prefix = warn ? "  WARN  " : "**FAIL**";

  if ((page_size = ppdFindOption(ppd, "PageSize")) == NULL && warn != 2)
  {
    if (!warn && !errors && !verbose)
      _cupsLangPuts(stdout, _(" FAIL"));

    if (verbose >= 0)
      _cupsLangPrintf(stdout,
		      _("      %s  Missing REQUIRED PageSize option.\n"
		        "                REF: Page 99, section 5.14."),
		      prefix);

    if (!warn)
      errors ++;
  }

  if ((page_region = ppdFindOption(ppd, "PageRegion")) == NULL && warn != 2)
  {
    if (!warn && !errors && !verbose)
      _cupsLangPuts(stdout, _(" FAIL"));

    if (verbose >= 0)
      _cupsLangPrintf(stdout,
		      _("      %s  Missing REQUIRED PageRegion option.\n"
		        "                REF: Page 100, section 5.14."),
		      prefix);

    if (!warn)
      errors ++;
  }

  for (i = ppd->num_sizes, size = ppd->sizes; i > 0; i --, size ++)
  {
   /*
    * Check that the size name is standard...
    */

    if (!strcmp(size->name, "Custom"))
    {
     /*
      * Skip custom page size...
      */

      continue;
    }

    if (warn != 2 && size->name[0] == 'w' &&
        sscanf(size->name, "w%dh%d", &width, &length) == 2)
    {
     /*
      * Validate device-specific size wNNNhNNN should have proper width and
      * length...
      */

      if (fabs(width - size->width) >= 1.0 ||
          fabs(length - size->length) >= 1.0)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      %s  Size \"%s\" has unexpected dimensions "
			    "(%gx%g)."),
			  prefix, size->name, size->width, size->length);

	if (!warn)
	  errors ++;
      }
    }

   /*
    * Verify that the size is defined for both PageSize and PageRegion...
    */

    if (warn != 2 && !ppdFindChoice(page_size, size->name))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Size \"%s\" defined for %s but not for "
			  "%s."),
			prefix, size->name, "PageRegion", "PageSize");

      if (!warn)
	errors ++;
    }
    else if (warn != 2 && !ppdFindChoice(page_region, size->name))
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  Size \"%s\" defined for %s but not for "
			  "%s."),
			prefix, size->name, "PageSize", "PageRegion");

      if (!warn)
	errors ++;
    }

   /*
    * Verify that the size name is Adobe standard name if it's a standard size
    * and the dimensional name if it's not a standard size.  Suffix should be
    * .Fullbleed, etc., or numeric, e.g., Letter, Letter.Fullbleed,
    * Letter.Transverse, Letter1, Letter2, 4x8, 55x91mm, 55x91mm.Fullbleed, etc.
    */

    if (warn != 0)
    {
      is_ok          = 1;
      width_2540ths  = (size->length > size->width) ?
                           PWG_FROM_POINTS(size->width) :
			   PWG_FROM_POINTS(size->length);
      length_2540ths = (size->length > size->width) ?
                           PWG_FROM_POINTS(size->length) :
			   PWG_FROM_POINTS(size->width);
      pwg_media      = pwgMediaForSize(width_2540ths, length_2540ths);

      if (pwg_media &&
          (abs(pwg_media->width - width_2540ths) > 34 ||
           abs(pwg_media->length - length_2540ths) > 34))
        pwg_media = NULL;		/* Only flag matches within a point */

      if (pwg_media && pwg_media->ppd &&
          (pwg_media->ppd[0] < 'a' || pwg_media->ppd[0] > 'z'))
      {
        size_t ppdlen = strlen(pwg_media->ppd);
					/* Length of standard PPD name */

        strlcpy(buf, pwg_media->ppd, sizeof(buf));

        if (strcmp(size->name, buf) && size->width > size->length)
        {
          if (!strcmp(pwg_media->ppd, "DoublePostcardRotated"))
            strlcpy(buf, "DoublePostcard", sizeof(buf));
          else if (strstr(size->name, ".Transverse"))
            snprintf(buf, sizeof(buf), "%s.Transverse", pwg_media->ppd);
          else
            snprintf(buf, sizeof(buf), "%sRotated", pwg_media->ppd);

	  ppdlen = strlen(buf);
        }

        if (size->left == 0 && size->bottom == 0 &&
	    size->right == size->width && size->top == size->length)
        {
          strlcat(buf, ".Fullbleed", sizeof(buf) - strlen(buf));
	  if (_cups_strcasecmp(size->name, buf))
	  {
	   /*
	    * Allow an additional qualifier such as ".WithTab"...
	    */

	    size_t buflen = strlen(buf);/* Length of full bleed name */

            if (_cups_strncasecmp(size->name, buf, buflen) ||
                size->name[buflen] != '.')
	      is_ok = 0;
	  }
        }
	else if (!strncmp(size->name, pwg_media->ppd, ppdlen))
	{
	 /*
	  * Check for a proper qualifier (number, "Small", or .something)...
	  */

	  ptr = size->name + ppdlen;

	  if (isdigit(*ptr & 255))
          {
            for (ptr ++; *ptr; ptr ++)
            {
              if (!isdigit(*ptr & 255))
	      {
                is_ok = 0;
		break;
	      }
            }
          }
          else if (*ptr != '.' && *ptr && strcmp(ptr, "Small"))
	    is_ok = 0;
        }
	else
	{
	 /*
	  * Check for EnvSizeName as well...
	  */

	  if (strncmp(pwg_media->ppd, "Env", 3) &&
	      !strncmp(size->name, "Env", 3))
            snprintf(buf, sizeof(buf), "Env%s", pwg_media->ppd);

	  if (strcmp(size->name, buf))
	    is_ok = 0;
	}

        if (!is_ok)
          _cupsLangPrintf(stdout,
                          _("      %s  Size \"%s\" should be the Adobe "
			    "standard name \"%s\"."),
                          prefix, size->name, buf);
      }
      else
      {
        width_tmp  = (fabs(size->width - ceil(size->width)) < 0.1) ?
	                 ceil(size->width) : size->width;
        length_tmp = (fabs(size->length - ceil(size->length)) < 0.1) ?
	                 ceil(size->length) : size->length;

        if (fmod(width_tmp, 9.0) == 0.0 && fmod(length_tmp, 9.0) == 0.0)
        {
          width_inch  = width_tmp / 72.0;
          length_inch = length_tmp / 72.0;

          snprintf(buf, sizeof(buf), "%gx%g", width_inch, length_inch);
        }
        else
        {
          width_mm  = size->width / 72.0 * 25.4;
          length_mm = size->length / 72.0 * 25.4;

          snprintf(buf, sizeof(buf), "%.0fx%.0fmm", width_mm, length_mm);
        }

        if (size->left == 0 && size->bottom == 0 &&
	    size->right == size->width && size->top == size->length)
          strlcat(buf, ".Fullbleed", sizeof(buf));
        else if (size->width > size->length)
          strlcat(buf, ".Transverse", sizeof(buf));

        if (_cups_strcasecmp(size->name, buf))
        {
          size_t	buflen = strlen(buf);
          				/* Length of proposed name */

          if (_cups_strncasecmp(size->name, buf, buflen) ||
              (strcmp(size->name + buflen, "in") &&
               size->name[buflen] != '.'))
          {
	    char	altbuf[PPD_MAX_NAME];
					/* Alternate "wNNNhNNN" name */
	    size_t	altlen;		/* Length of alternate name */

	    snprintf(altbuf, sizeof(altbuf), "w%.0fh%.0f", size->width,
	             size->length);
	    altlen = strlen(altbuf);
	    if (_cups_strncasecmp(size->name, altbuf, altlen) ||
	        (size->name[altlen] && size->name[altlen] != '.'))
	      _cupsLangPrintf(stdout,
			      _("      %s  Size \"%s\" should be \"%s\"."),
			      prefix, size->name, buf);
	  }
        }
      }
    }
  }

  return (errors);
}


/*
 * 'check_translations()' - Check translations in the PPD file.
 */

static int				/* O - Errors found */
check_translations(ppd_file_t *ppd,	/* I - PPD file */
                   int        errors,	/* I - Errors found */
                   int        verbose,	/* I - Verbosity level */
                   int        warn)	/* I - Warnings only? */
{
  int		j;			/* Looping var */
  ppd_attr_t	*attr;			/* PPD attribute */
  cups_array_t	*languages;		/* Array of languages */
  int		langlen;		/* Length of language */
  char		*language,		/* Current language */
		keyword[PPD_MAX_NAME],	/* Localization keyword (full) */
		llkeyword[PPD_MAX_NAME],/* Localization keyword (base) */
		ckeyword[PPD_MAX_NAME],	/* Custom option keyword (full) */
		cllkeyword[PPD_MAX_NAME];
					/* Custom option keyword (base) */
  ppd_option_t	*option;		/* Standard UI option */
  ppd_coption_t	*coption;		/* Custom option */
  ppd_cparam_t	*cparam;		/* Custom parameter */
  char		ll[3];			/* Base language */
  const char	*prefix;		/* WARN/FAIL prefix */
  const char	*text;			/* Pointer into UI text */


  prefix = warn ? "  WARN  " : "**FAIL**";

  if ((languages = _ppdGetLanguages(ppd)) != NULL)
  {
   /*
    * This file contains localizations, check them...
    */

    for (language = (char *)cupsArrayFirst(languages);
         language;
	 language = (char *)cupsArrayNext(languages))
    {
      langlen = (int)strlen(language);
      if (langlen != 2 && langlen != 5)
      {
	if (!warn && !errors && !verbose)
	  _cupsLangPuts(stdout, _(" FAIL"));

	if (verbose >= 0)
	  _cupsLangPrintf(stdout,
			  _("      %s  Bad language \"%s\"."),
			  prefix, language);

	if (!warn)
	  errors ++;

	continue;
      }

      if (!strcmp(language, "en"))
        continue;

      strlcpy(ll, language, sizeof(ll));

     /*
      * Loop through all options and choices...
      */

      for (option = ppdFirstOption(ppd);
	   option;
	   option = ppdNextOption(ppd))
      {
        if (!strcmp(option->keyword, "PageRegion"))
	  continue;

	snprintf(keyword, sizeof(keyword), "%s.Translation", language);
	snprintf(llkeyword, sizeof(llkeyword), "%s.Translation", ll);

	if ((attr = ppdFindAttr(ppd, keyword, option->keyword)) == NULL &&
	    (attr = ppdFindAttr(ppd, llkeyword, option->keyword)) == NULL)
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  if (verbose >= 0)
	    _cupsLangPrintf(stdout,
			    _("      %s  Missing \"%s\" translation "
			      "string for option %s."),
			    prefix, language, option->keyword);

          if (!warn)
	    errors ++;
	}
	else if (!valid_utf8(attr->text))
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  if (verbose >= 0)
	    _cupsLangPrintf(stdout,
			    _("      %s  Bad UTF-8 \"%s\" translation "
			      "string for option %s."),
			    prefix, language, option->keyword);

	  if (!warn)
	    errors ++;
	}

	snprintf(keyword, sizeof(keyword), "%s.%s", language,
		 option->keyword);
	snprintf(llkeyword, sizeof(llkeyword), "%s.%s", ll,
		 option->keyword);

	for (j = 0; j < option->num_choices; j ++)
	{
         /*
	  * First see if this choice is a number; if so, don't require
	  * translation...
	  */

          for (text = option->choices[j].text; *text; text ++)
	    if (!strchr("0123456789-+.", *text))
	      break;

          if (!*text)
	    continue;

	 /*
	  * Check custom choices differently...
	  */

	  if (!_cups_strcasecmp(option->choices[j].choice, "Custom") &&
	      (coption = ppdFindCustomOption(ppd,
					     option->keyword)) != NULL)
	  {
	    snprintf(ckeyword, sizeof(ckeyword), "%s.Custom%s",
		     language, option->keyword);

	    if ((attr = ppdFindAttr(ppd, ckeyword, "True")) != NULL &&
		!valid_utf8(attr->text))
	    {
	      if (!warn && !errors && !verbose)
		_cupsLangPuts(stdout, _(" FAIL"));

	      if (verbose >= 0)
		_cupsLangPrintf(stdout,
				_("      %s  Bad UTF-8 \"%s\" "
				  "translation string for option %s, "
				  "choice %s."),
				prefix, language,
				ckeyword + 1 + strlen(language),
				"True");

              if (!warn)
		errors ++;
	    }

	    if (_cups_strcasecmp(option->keyword, "PageSize"))
	    {
	      for (cparam = (ppd_cparam_t *)cupsArrayFirst(coption->params);
		   cparam;
		   cparam = (ppd_cparam_t *)cupsArrayNext(coption->params))
	      {
		snprintf(ckeyword, sizeof(ckeyword), "%s.ParamCustom%s",
			 language, option->keyword);
		snprintf(cllkeyword, sizeof(cllkeyword), "%s.ParamCustom%s",
			 ll, option->keyword);

		if ((attr = ppdFindAttr(ppd, ckeyword,
					cparam->name)) == NULL &&
		    (attr = ppdFindAttr(ppd, cllkeyword,
					cparam->name)) == NULL)
		{
		  if (!warn && !errors && !verbose)
		    _cupsLangPuts(stdout, _(" FAIL"));

		  if (verbose >= 0)
		    _cupsLangPrintf(stdout,
				    _("      %s  Missing \"%s\" "
				      "translation string for option %s, "
				      "choice %s."),
				    prefix, language,
				    ckeyword + 1 + strlen(language),
				    cparam->name);

                  if (!warn)
		    errors ++;
		}
		else if (!valid_utf8(attr->text))
		{
		  if (!warn && !errors && !verbose)
		    _cupsLangPuts(stdout, _(" FAIL"));

		  if (verbose >= 0)
		    _cupsLangPrintf(stdout,
				    _("      %s  Bad UTF-8 \"%s\" "
				      "translation string for option %s, "
				      "choice %s."),
				    prefix, language,
				    ckeyword + 1 + strlen(language),
				    cparam->name);

		  if (!warn)
		    errors ++;
		}
	      }
	    }
	  }
	  else if ((attr = ppdFindAttr(ppd, keyword,
				       option->choices[j].choice)) == NULL &&
		   (attr = ppdFindAttr(ppd, llkeyword,
				       option->choices[j].choice)) == NULL)
	  {
	    if (!warn && !errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    if (verbose >= 0)
	      _cupsLangPrintf(stdout,
			      _("      %s  Missing \"%s\" "
				"translation string for option %s, "
				"choice %s."),
			      prefix, language, option->keyword,
			      option->choices[j].choice);

	    if (!warn)
	      errors ++;
	  }
	  else if (!valid_utf8(attr->text))
	  {
	    if (!warn && !errors && !verbose)
	      _cupsLangPuts(stdout, _(" FAIL"));

	    if (verbose >= 0)
	      _cupsLangPrintf(stdout,
			      _("      %s  Bad UTF-8 \"%s\" "
				"translation string for option %s, "
				"choice %s."),
			      prefix, language, option->keyword,
			      option->choices[j].choice);

	    if (!warn)
	      errors ++;
	  }
	}
      }
    }

   /*
    * Verify that we have the base language for each localized one...
    */

    for (language = (char *)cupsArrayFirst(languages);
	 language;
	 language = (char *)cupsArrayNext(languages))
      if (language[2])
      {
       /*
	* Lookup the base language...
	*/

	cupsArraySave(languages);

	strlcpy(ll, language, sizeof(ll));

	if (!cupsArrayFind(languages, ll) &&
	    strcmp(ll, "zh") && strcmp(ll, "en"))
	{
	  if (!warn && !errors && !verbose)
	    _cupsLangPuts(stdout, _(" FAIL"));

	  if (verbose >= 0)
	    _cupsLangPrintf(stdout,
			    _("      %s  No base translation \"%s\" "
			      "is included in file."), prefix, ll);

	  if (!warn)
	    errors ++;
	}

	cupsArrayRestore(languages);
      }

   /*
    * Free memory used for the languages...
    */

    _ppdFreeLanguages(languages);
  }

  return (errors);
}


/*
 * 'show_conflicts()' - Show option conflicts in a PPD file.
 */

static void
show_conflicts(ppd_file_t *ppd,		/* I - PPD to check */
               const char *prefix)	/* I - Prefix string */
{
  int		i, j;			/* Looping variables */
  ppd_const_t	*c;			/* Current constraint */
  ppd_option_t	*o1, *o2;		/* Options */
  ppd_choice_t	*c1, *c2;		/* Choices */


 /*
  * Loop through all of the UI constraints and report any options
  * that conflict...
  */

  for (i = ppd->num_consts, c = ppd->consts; i > 0; i --, c ++)
  {
   /*
    * Grab pointers to the first option...
    */

    o1 = ppdFindOption(ppd, c->option1);

    if (o1 == NULL)
      continue;
    else if (c->choice1[0] != '\0')
    {
     /*
      * This constraint maps to a specific choice.
      */

      c1 = ppdFindChoice(o1, c->choice1);
    }
    else
    {
     /*
      * This constraint applies to any choice for this option.
      */

      for (j = o1->num_choices, c1 = o1->choices; j > 0; j --, c1 ++)
        if (c1->marked)
	  break;

      if (j == 0 ||
          !_cups_strcasecmp(c1->choice, "None") ||
          !_cups_strcasecmp(c1->choice, "Off") ||
          !_cups_strcasecmp(c1->choice, "False"))
        c1 = NULL;
    }

   /*
    * Grab pointers to the second option...
    */

    o2 = ppdFindOption(ppd, c->option2);

    if (o2 == NULL)
      continue;
    else if (c->choice2[0] != '\0')
    {
     /*
      * This constraint maps to a specific choice.
      */

      c2 = ppdFindChoice(o2, c->choice2);
    }
    else
    {
     /*
      * This constraint applies to any choice for this option.
      */

      for (j = o2->num_choices, c2 = o2->choices; j > 0; j --, c2 ++)
        if (c2->marked)
	  break;

      if (j == 0 ||
          !_cups_strcasecmp(c2->choice, "None") ||
          !_cups_strcasecmp(c2->choice, "Off") ||
          !_cups_strcasecmp(c2->choice, "False"))
        c2 = NULL;
    }

   /*
    * If both options are marked then there is a conflict...
    */

    if (c1 != NULL && c1->marked && c2 != NULL && c2->marked)
      _cupsLangPrintf(stdout,
                      _("      %s  \"%s %s\" conflicts with \"%s %s\"\n"
                        "                (constraint=\"%s %s %s %s\")."),
        	      prefix, o1->keyword, c1->choice, o2->keyword, c2->choice,
		      c->option1, c->choice1, c->option2, c->choice2);
  }
}


/*
 * 'test_raster()' - Test PostScript commands for raster printers.
 */

static int				/* O - 1 on success, 0 on failure */
test_raster(ppd_file_t *ppd,		/* I - PPD file */
            int        verbose)		/* I - Verbosity */
{
  cups_page_header2_t	header;		/* Page header */


  ppdMarkDefaults(ppd);
  if (cupsRasterInterpretPPD(&header, ppd, 0, NULL, 0))
  {
    if (!verbose)
      _cupsLangPuts(stdout, _(" FAIL"));

    if (verbose >= 0)
      _cupsLangPrintf(stdout,
		      _("      **FAIL**  Default option code cannot be "
			"interpreted: %s"), cupsRasterErrorString());

    return (0);
  }

 /*
  * Try a test of custom page size code, if available...
  */

  if (!ppdPageSize(ppd, "Custom.612x792"))
    return (1);

  ppdMarkOption(ppd, "PageSize", "Custom.612x792");

  if (cupsRasterInterpretPPD(&header, ppd, 0, NULL, 0))
  {
    if (!verbose)
      _cupsLangPuts(stdout, _(" FAIL"));

    if (verbose >= 0)
      _cupsLangPrintf(stdout,
		      _("      **FAIL**  Default option code cannot be "
			"interpreted: %s"), cupsRasterErrorString());

    return (0);
  }

  return (1);
}


/*
 * 'usage()' - Show program usage.
 */

static void
usage(void)
{
  _cupsLangPuts(stdout, _("Warning: This program will be removed in a future version of CUPS."));
  _cupsLangPuts(stdout, _("Usage: cupstestppd [options] filename1.ppd[.gz] [... filenameN.ppd[.gz]]\n"
		          "       program | cupstestppd [options] -"));
  _cupsLangPuts(stdout, _("Options:"));
  _cupsLangPuts(stdout, _("-I {filename,filters,none,profiles}\n"
                          "                          Ignore specific warnings"));
  _cupsLangPuts(stdout, _("-R root-directory       Set alternate root"));
  _cupsLangPuts(stdout, _("-W {all,none,constraints,defaults,duplex,filters,profiles,sizes,translations}\n"
                          "                          Issue warnings instead of errors"));
  _cupsLangPuts(stdout, _("-q                      Run silently"));
  _cupsLangPuts(stdout, _("-r                      Use 'relaxed' open mode"));
  _cupsLangPuts(stdout, _("-v                      Be verbose"));
  _cupsLangPuts(stdout, _("-vv                     Be very verbose"));

  exit(ERROR_USAGE);
}


/*
 * 'valid_path()' - Check whether a path has the correct capitalization.
 */

static int				/* O - Errors found */
valid_path(const char *keyword,		/* I - Keyword using path */
           const char *path,		/* I - Path to check */
	   int        errors,		/* I - Errors found */
	   int        verbose,		/* I - Verbosity level */
	   int        warn)		/* I - Warnings only? */
{
  cups_dir_t	*dir;			/* Current directory */
  cups_dentry_t	*dentry;		/* Current directory entry */
  char		temp[1024],		/* Temporary path */
		*ptr;			/* Pointer into temporary path */
  const char	*prefix;		/* WARN/FAIL prefix */


  prefix = warn ? "  WARN  " : "**FAIL**";

 /*
  * Loop over the components of the path, checking that the entry exists with
  * the same capitalization...
  */

  strlcpy(temp, path, sizeof(temp));

  while ((ptr = strrchr(temp, '/')) != NULL)
  {
   /*
    * Chop off the trailing component so temp == dirname and ptr == basename.
    */

    *ptr++ = '\0';

   /*
    * Try opening the directory containing the base name...
    */

    if (temp[0])
      dir = cupsDirOpen(temp);
    else
      dir = cupsDirOpen("/");

    if (!dir)
      dentry = NULL;
    else
    {
      while ((dentry = cupsDirRead(dir)) != NULL)
      {
        if (!strcmp(dentry->filename, ptr))
	  break;
      }

      cupsDirClose(dir);
    }

   /*
    * Display an error if the filename doesn't exist with the same
    * capitalization...
    */

    if (!dentry)
    {
      if (!warn && !errors && !verbose)
	_cupsLangPuts(stdout, _(" FAIL"));

      if (verbose >= 0)
	_cupsLangPrintf(stdout,
			_("      %s  %s file \"%s\" has the wrong "
			  "capitalization."), prefix, keyword, path);

      if (!warn)
	errors ++;

      break;
    }
  }

  return (errors);
}


/*
 * 'valid_utf8()' - Check whether a string contains valid UTF-8 text.
 */

static int				/* O - 1 if valid, 0 if not */
valid_utf8(const char *s)		/* I - String to check */
{
  while (*s)
  {
    if (*s & 0x80)
    {
     /*
      * Check for valid UTF-8 sequence...
      */

      if ((*s & 0xc0) == 0x80)
        return (0);			/* Illegal suffix byte */
      else if ((*s & 0xe0) == 0xc0)
      {
       /*
        * 2-byte sequence...
        */

        s ++;

        if ((*s & 0xc0) != 0x80)
          return (0);			/* Missing suffix byte */
      }
      else if ((*s & 0xf0) == 0xe0)
      {
       /*
        * 3-byte sequence...
        */

        s ++;

        if ((*s & 0xc0) != 0x80)
          return (0);			/* Missing suffix byte */

        s ++;

        if ((*s & 0xc0) != 0x80)
          return (0);			/* Missing suffix byte */
      }
      else if ((*s & 0xf8) == 0xf0)
      {
       /*
        * 4-byte sequence...
        */

        s ++;

        if ((*s & 0xc0) != 0x80)
          return (0);			/* Missing suffix byte */

        s ++;

        if ((*s & 0xc0) != 0x80)
          return (0);			/* Missing suffix byte */

        s ++;

        if ((*s & 0xc0) != 0x80)
          return (0);			/* Missing suffix byte */
      }
      else
        return (0);			/* Bad sequence */
    }

    s ++;
  }

  return (1);
}
