/*
 * PPD file routines for CUPS.
 *
 * Copyright © 2021-2022 by OpenPrinting.
 * Copyright © 2007-2019 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-private.h"
#include "ppd-private.h"
#include "debug-internal.h"


/*
 * Definitions...
 */

#define PPD_KEYWORD	1		/* Line contained a keyword */
#define PPD_OPTION	2		/* Line contained an option name */
#define PPD_TEXT	4		/* Line contained human-readable text */
#define PPD_STRING	8		/* Line contained a string or code */

#define PPD_HASHSIZE	512		/* Size of hash */


/*
 * Line buffer structure...
 */

typedef struct _ppd_line_s
{
  char		*buffer;		/* Pointer to buffer */
  size_t	bufsize;		/* Size of the buffer */
} _ppd_line_t;


/*
 * Local globals...
 */

static _cups_threadkey_t ppd_globals_key = _CUPS_THREADKEY_INITIALIZER;
					/* Thread local storage key */
#ifdef HAVE_PTHREAD_H
static pthread_once_t	ppd_globals_key_once = PTHREAD_ONCE_INIT;
					/* One-time initialization object */
#endif /* HAVE_PTHREAD_H */


/*
 * Local functions...
 */

static ppd_attr_t	*ppd_add_attr(ppd_file_t *ppd, const char *name,
			              const char *spec, const char *text,
				      const char *value);
static ppd_choice_t	*ppd_add_choice(ppd_option_t *option, const char *name);
static ppd_size_t	*ppd_add_size(ppd_file_t *ppd, const char *name);
static int		ppd_compare_attrs(ppd_attr_t *a, ppd_attr_t *b);
static int		ppd_compare_choices(ppd_choice_t *a, ppd_choice_t *b);
static int		ppd_compare_coptions(ppd_coption_t *a,
			                     ppd_coption_t *b);
static int		ppd_compare_options(ppd_option_t *a, ppd_option_t *b);
static int		ppd_decode(char *string);
static void		ppd_free_filters(ppd_file_t *ppd);
static void		ppd_free_group(ppd_group_t *group);
static void		ppd_free_option(ppd_option_t *option);
static ppd_coption_t	*ppd_get_coption(ppd_file_t *ppd, const char *name);
static ppd_cparam_t	*ppd_get_cparam(ppd_coption_t *opt,
			                const char *param,
					const char *text);
static ppd_group_t	*ppd_get_group(ppd_file_t *ppd, const char *name,
			               const char *text, _ppd_globals_t *pg,
				       cups_encoding_t encoding);
static ppd_option_t	*ppd_get_option(ppd_group_t *group, const char *name);
static _ppd_globals_t	*ppd_globals_alloc(void);
#if defined(HAVE_PTHREAD_H) || defined(_WIN32)
static void		ppd_globals_free(_ppd_globals_t *g);
#endif /* HAVE_PTHREAD_H || _WIN32 */
#ifdef HAVE_PTHREAD_H
static void		ppd_globals_init(void);
#endif /* HAVE_PTHREAD_H */
static int		ppd_hash_option(ppd_option_t *option);
static int		ppd_read(cups_file_t *fp, _ppd_line_t *line,
			         char *keyword, char *option, char *text,
				 char **string, int ignoreblank,
				 _ppd_globals_t *pg);
static int		ppd_update_filters(ppd_file_t *ppd,
			                   _ppd_globals_t *pg);


/*
 * 'ppdClose()' - Free all memory used by the PPD file.
 */

void
ppdClose(ppd_file_t *ppd)		/* I - PPD file record */
{
  int			i;		/* Looping var */
  ppd_group_t		*group;		/* Current group */
  char			**font;		/* Current font */
  ppd_attr_t		**attr;		/* Current attribute */
  ppd_coption_t		*coption;	/* Current custom option */
  ppd_cparam_t		*cparam;	/* Current custom parameter */


 /*
  * Range check arguments...
  */

  if (!ppd)
    return;

 /*
  * Free all strings at the top level...
  */

  free(ppd->lang_encoding);
  free(ppd->nickname);
  free(ppd->patches);
  free(ppd->emulations);
  free(ppd->jcl_begin);
  free(ppd->jcl_end);
  free(ppd->jcl_ps);

 /*
  * Free any UI groups, subgroups, and options...
  */

  if (ppd->num_groups > 0)
  {
    for (i = ppd->num_groups, group = ppd->groups; i > 0; i --, group ++)
      ppd_free_group(group);

    free(ppd->groups);
  }

  cupsArrayDelete(ppd->options);
  cupsArrayDelete(ppd->marked);

 /*
  * Free any page sizes...
  */

  if (ppd->num_sizes > 0)
    free(ppd->sizes);

 /*
  * Free any constraints...
  */

  if (ppd->num_consts > 0)
    free(ppd->consts);

 /*
  * Free any filters...
  */

  ppd_free_filters(ppd);

 /*
  * Free any fonts...
  */

  if (ppd->num_fonts > 0)
  {
    for (i = ppd->num_fonts, font = ppd->fonts; i > 0; i --, font ++)
      free(*font);

    free(ppd->fonts);
  }

 /*
  * Free any profiles...
  */

  if (ppd->num_profiles > 0)
    free(ppd->profiles);

 /*
  * Free any attributes...
  */

  if (ppd->num_attrs > 0)
  {
    for (i = ppd->num_attrs, attr = ppd->attrs; i > 0; i --, attr ++)
    {
      free((*attr)->value);
      free(*attr);
    }

    free(ppd->attrs);
  }

  cupsArrayDelete(ppd->sorted_attrs);

 /*
  * Free custom options...
  */

  for (coption = (ppd_coption_t *)cupsArrayFirst(ppd->coptions);
       coption;
       coption = (ppd_coption_t *)cupsArrayNext(ppd->coptions))
  {
    for (cparam = (ppd_cparam_t *)cupsArrayFirst(coption->params);
         cparam;
	 cparam = (ppd_cparam_t *)cupsArrayNext(coption->params))
    {
      switch (cparam->type)
      {
        case PPD_CUSTOM_PASSCODE :
        case PPD_CUSTOM_PASSWORD :
        case PPD_CUSTOM_STRING :
            free(cparam->current.custom_string);
	    break;

	default :
	    break;
      }

      free(cparam);
    }

    cupsArrayDelete(coption->params);

    free(coption);
  }

  cupsArrayDelete(ppd->coptions);

 /*
  * Free constraints...
  */

  if (ppd->cups_uiconstraints)
  {
    _ppd_cups_uiconsts_t *consts;	/* Current constraints */


    for (consts = (_ppd_cups_uiconsts_t *)cupsArrayFirst(ppd->cups_uiconstraints);
         consts;
	 consts = (_ppd_cups_uiconsts_t *)cupsArrayNext(ppd->cups_uiconstraints))
    {
      free(consts->constraints);
      free(consts);
    }

    cupsArrayDelete(ppd->cups_uiconstraints);
  }

 /*
  * Free any PPD cache/mapping data...
  */

  if (ppd->cache)
    _ppdCacheDestroy(ppd->cache);

 /*
  * Free the whole record...
  */

  free(ppd);
}


/*
 * 'ppdErrorString()' - Returns the text associated with a status.
 *
 * @since CUPS 1.1.19/macOS 10.3@
 */

const char *				/* O - Status string */
ppdErrorString(ppd_status_t status)	/* I - PPD status */
{
  static const char * const messages[] =/* Status messages */
		{
		  _("OK"),
		  _("Unable to open PPD file"),
		  _("NULL PPD file pointer"),
		  _("Memory allocation error"),
		  _("Missing PPD-Adobe-4.x header"),
		  _("Missing value string"),
		  _("Internal error"),
		  _("Bad OpenGroup"),
		  _("OpenGroup without a CloseGroup first"),
		  _("Bad OpenUI/JCLOpenUI"),
		  _("OpenUI/JCLOpenUI without a CloseUI/JCLCloseUI first"),
		  _("Bad OrderDependency"),
		  _("Bad UIConstraints"),
		  _("Missing asterisk in column 1"),
		  _("Line longer than the maximum allowed (255 characters)"),
		  _("Illegal control character"),
		  _("Illegal main keyword string"),
		  _("Illegal option keyword string"),
		  _("Illegal translation string"),
		  _("Illegal whitespace character"),
		  _("Bad custom parameter"),
		  _("Missing option keyword"),
		  _("Bad value string"),
		  _("Missing CloseGroup"),
		  _("Bad CloseUI/JCLCloseUI"),
		  _("Missing CloseUI/JCLCloseUI")
		};


  if (status < PPD_OK || status >= PPD_MAX_STATUS)
    return (_cupsLangString(cupsLangDefault(), _("Unknown")));
  else
    return (_cupsLangString(cupsLangDefault(), messages[status]));
}


/*
 * '_ppdGetEncoding()' - Get the CUPS encoding value for the given
 *                       LanguageEncoding.
 */

cups_encoding_t				/* O - CUPS encoding value */
_ppdGetEncoding(const char *name)	/* I - LanguageEncoding string */
{
  if (!_cups_strcasecmp(name, "ISOLatin1"))
    return (CUPS_ISO8859_1);
  else if (!_cups_strcasecmp(name, "ISOLatin2"))
    return (CUPS_ISO8859_2);
  else if (!_cups_strcasecmp(name, "ISOLatin5"))
    return (CUPS_ISO8859_5);
  else if (!_cups_strcasecmp(name, "JIS83-RKSJ"))
    return (CUPS_JIS_X0213);
  else if (!_cups_strcasecmp(name, "MacStandard"))
    return (CUPS_MAC_ROMAN);
  else if (!_cups_strcasecmp(name, "WindowsANSI"))
    return (CUPS_WINDOWS_1252);
  else
    return (CUPS_UTF8);
}


/*
 * '_ppdGlobals()' - Return a pointer to thread local storage
 */

_ppd_globals_t *			/* O - Pointer to global data */
_ppdGlobals(void)
{
  _ppd_globals_t *pg;			/* Pointer to global data */


#ifdef HAVE_PTHREAD_H
 /*
  * Initialize the global data exactly once...
  */

  pthread_once(&ppd_globals_key_once, ppd_globals_init);
#endif /* HAVE_PTHREAD_H */

 /*
  * See if we have allocated the data yet...
  */

  if ((pg = (_ppd_globals_t *)_cupsThreadGetData(ppd_globals_key)) == NULL)
  {
   /*
    * No, allocate memory as set the pointer for the key...
    */

    if ((pg = ppd_globals_alloc()) != NULL)
      _cupsThreadSetData(ppd_globals_key, pg);
  }

 /*
  * Return the pointer to the data...
  */

  return (pg);
}


/*
 * 'ppdLastError()' - Return the status from the last ppdOpen*().
 *
 * @since CUPS 1.1.19/macOS 10.3@
 */

ppd_status_t				/* O - Status code */
ppdLastError(int *line)			/* O - Line number */
{
  _ppd_globals_t	*pg = _ppdGlobals();
					/* Global data */


  if (line)
    *line = pg->ppd_line;

  return (pg->ppd_status);
}


/*
 * '_ppdOpen()' - Read a PPD file into memory.
 *
 * @since CUPS 1.2/macOS 10.5@
 */

ppd_file_t *				/* O - PPD file record or @code NULL@ if the PPD file could not be opened. */
_ppdOpen(
    cups_file_t		*fp,		/* I - File to read from */
    _ppd_localization_t	localization)	/* I - Localization to load */
{
  int			i, j, k;	/* Looping vars */
  _ppd_line_t		line;		/* Line buffer */
  ppd_file_t		*ppd;		/* PPD file record */
  ppd_group_t		*group,		/* Current group */
			*subgroup;	/* Current sub-group */
  ppd_option_t		*option;	/* Current option */
  ppd_choice_t		*choice;	/* Current choice */
  ppd_const_t		*constraint;	/* Current constraint */
  ppd_size_t		*size;		/* Current page size */
  int			mask;		/* Line data mask */
  char			keyword[PPD_MAX_NAME],
  					/* Keyword from file */
			name[PPD_MAX_NAME],
					/* Option from file */
			text[PPD_MAX_LINE],
					/* Human-readable text from file */
			*string,	/* Code/text from file */
			*sptr,		/* Pointer into string */
			*temp,		/* Temporary string pointer */
			**tempfonts;	/* Temporary fonts pointer */
  float			order;		/* Order dependency number */
  ppd_section_t		section;	/* Order dependency section */
  ppd_profile_t		*profile;	/* Pointer to color profile */
  char			**filter;	/* Pointer to filter */
  struct lconv		*loc;		/* Locale data */
  int			ui_keyword;	/* Is this line a UI keyword? */
  cups_lang_t		*lang;		/* Language data */
  cups_encoding_t	encoding;	/* Encoding of PPD file */
  _ppd_globals_t	*pg = _ppdGlobals();
					/* Global data */
  char			custom_name[PPD_MAX_NAME];
					/* CustomFoo attribute name */
  ppd_attr_t		*custom_attr;	/* CustomFoo attribute */
  char			ll[7],		/* Base language + '.' */
			ll_CC[7];	/* Language w/country + '.' */
  size_t		ll_len = 0,	/* Base language length */
			ll_CC_len = 0;	/* Language w/country length */
  static const char * const ui_keywords[] =
			{
#ifdef CUPS_USE_FULL_UI_KEYWORDS_LIST
 /*
  * Adobe defines some 41 keywords as "UI", meaning that they are
  * user interface elements and that they should be treated as such
  * even if the PPD creator doesn't use Open/CloseUI around them.
  *
  * Since this can cause previously invisible options to appear and
  * confuse users, the default is to only treat the PageSize and
  * PageRegion keywords this way.
  */
			  /* Boolean keywords */
			  "BlackSubstitution",
			  "Booklet",
			  "Collate",
			  "ManualFeed",
			  "MirrorPrint",
			  "NegativePrint",
			  "Sorter",
			  "TraySwitch",

			  /* PickOne keywords */
			  "AdvanceMedia",
			  "BindColor",
			  "BindEdge",
			  "BindType",
			  "BindWhen",
			  "BitsPerPixel",
			  "ColorModel",
			  "CutMedia",
			  "Duplex",
			  "FoldType",
			  "FoldWhen",
			  "InputSlot",
			  "JCLFrameBufferSize",
			  "JCLResolution",
			  "Jog",
			  "MediaColor",
			  "MediaType",
			  "MediaWeight",
			  "OutputBin",
			  "OutputMode",
			  "OutputOrder",
			  "PageRegion",
			  "PageSize",
			  "Resolution",
			  "Separations",
			  "Signature",
			  "Slipsheet",
			  "Smoothing",
			  "StapleLocation",
			  "StapleOrientation",
			  "StapleWhen",
			  "StapleX",
			  "StapleY"
#else /* !CUPS_USE_FULL_UI_KEYWORDS_LIST */
			  "PageRegion",
			  "PageSize"
#endif /* CUPS_USE_FULL_UI_KEYWORDS_LIST */
			};
  static const char * const color_keywords[] =	/* Keywords associated with color profiles */
			{
			  ".cupsICCProfile",
			  ".ColorModel",
			};


  DEBUG_printf(("_ppdOpen(fp=%p)", fp));

 /*
  * Default to "OK" status...
  */

  pg->ppd_status = PPD_OK;
  pg->ppd_line   = 0;

 /*
  * Range check input...
  */

  if (fp == NULL)
  {
    pg->ppd_status = PPD_NULL_FILE;
    return (NULL);
  }

 /*
  * If only loading a single localization set up the strings to match...
  */

  if (localization == _PPD_LOCALIZATION_DEFAULT)
  {
    if ((lang = cupsLangDefault()) == NULL)
      return (NULL);

    snprintf(ll_CC, sizeof(ll_CC), "%s.", lang->language);

   /*
    * <rdar://problem/22130168>
    * <rdar://problem/27245567>
    *
    * Need to use a different base language for some locales...
    */

    if (!strcmp(lang->language, "zh_HK"))
    {					/* Traditional Chinese + variants */
      strlcpy(ll_CC, "zh_TW.", sizeof(ll_CC));
      strlcpy(ll, "zh_", sizeof(ll));
    }
    else if (!strncmp(lang->language, "zh", 2))
      strlcpy(ll, "zh_", sizeof(ll));	/* Any Chinese variant */
    else if (!strncmp(lang->language, "jp", 2))
    {					/* Any Japanese variant */
      strlcpy(ll_CC, "ja", sizeof(ll_CC));
      strlcpy(ll, "jp", sizeof(ll));
    }
    else if (!strncmp(lang->language, "nb", 2) || !strncmp(lang->language, "no", 2))
    {					/* Any Norwegian variant */
      strlcpy(ll_CC, "nb", sizeof(ll_CC));
      strlcpy(ll, "no", sizeof(ll));
    }
    else
      snprintf(ll, sizeof(ll), "%2.2s.", lang->language);

    ll_CC_len = strlen(ll_CC);
    ll_len    = strlen(ll);

    DEBUG_printf(("2_ppdOpen: Loading localizations matching \"%s\" and \"%s\"",
                  ll_CC, ll));
  }

 /*
  * Grab the first line and make sure it reads '*PPD-Adobe: "major.minor"'...
  */

  line.buffer  = NULL;
  line.bufsize = 0;

  mask = ppd_read(fp, &line, keyword, name, text, &string, 0, pg);

  DEBUG_printf(("2_ppdOpen: mask=%x, keyword=\"%s\"...", mask, keyword));

  if (mask == 0 ||
      strcmp(keyword, "PPD-Adobe") ||
      string == NULL || string[0] != '4')
  {
   /*
    * Either this is not a PPD file, or it is not a 4.x PPD file.
    */

    if (pg->ppd_status == PPD_OK)
      pg->ppd_status = PPD_MISSING_PPDADOBE4;

    free(string);
    free(line.buffer);

    return (NULL);
  }

  DEBUG_printf(("2_ppdOpen: keyword=%s, string=%p", keyword, string));

 /*
  * Allocate memory for the PPD file record...
  */

  if ((ppd = calloc(1, sizeof(ppd_file_t))) == NULL)
  {
    pg->ppd_status = PPD_ALLOC_ERROR;

    free(string);
    free(line.buffer);

    return (NULL);
  }

  free(string);
  string = NULL;

  ppd->language_level = 2;
  ppd->color_device   = 0;
  ppd->colorspace     = PPD_CS_N;
  ppd->landscape      = -90;
  ppd->coptions       = cupsArrayNew((cups_array_func_t)ppd_compare_coptions, NULL);

 /*
  * Read lines from the PPD file and add them to the file record...
  */

  group      = NULL;
  subgroup   = NULL;
  option     = NULL;
  choice     = NULL;
  ui_keyword = 0;
  encoding   = CUPS_ISO8859_1;
  loc        = localeconv();

  while ((mask = ppd_read(fp, &line, keyword, name, text, &string, 1, pg)) != 0)
  {
    DEBUG_printf(("2_ppdOpen: mask=%x, keyword=\"%s\", name=\"%s\", "
                  "text=\"%s\", string=%d chars...", mask, keyword, name, text,
		  string ? (int)strlen(string) : 0));

    if (strncmp(keyword, "Default", 7) && !string &&
        pg->ppd_conform != PPD_CONFORM_RELAXED)
    {
     /*
      * Need a string value!
      */

      pg->ppd_status = PPD_MISSING_VALUE;

      goto error;
    }
    else if (!string)
      continue;

   /*
    * Certain main keywords (as defined by the PPD spec) may be used
    * without the usual OpenUI/CloseUI stuff.  Presumably this is just
    * so that Adobe wouldn't completely break compatibility with PPD
    * files prior to v4.0 of the spec, but it is hopelessly
    * inconsistent...  Catch these main keywords and automatically
    * create the corresponding option, as needed...
    */

    if (ui_keyword)
    {
     /*
      * Previous line was a UI keyword...
      */

      option     = NULL;
      ui_keyword = 0;
    }

   /*
    * If we are filtering out keyword localizations, see if this line needs to
    * be used...
    */

    if (localization != _PPD_LOCALIZATION_ALL &&
        (temp = strchr(keyword, '.')) != NULL &&
        ((temp - keyword) == 2 || (temp - keyword) == 5) &&
        _cups_isalpha(keyword[0]) &&
        _cups_isalpha(keyword[1]) &&
        (keyword[2] == '.' ||
         (keyword[2] == '_' && _cups_isalpha(keyword[3]) &&
          _cups_isalpha(keyword[4]) && keyword[5] == '.')))
    {
      if (localization == _PPD_LOCALIZATION_NONE ||
	  (localization == _PPD_LOCALIZATION_DEFAULT &&
	   strncmp(ll_CC, keyword, ll_CC_len) &&
	   strncmp(ll, keyword, ll_len)))
      {
	DEBUG_printf(("2_ppdOpen: Ignoring localization: \"%s\"\n", keyword));
	free(string);
	string = NULL;
	continue;
      }
      else if (localization == _PPD_LOCALIZATION_ICC_PROFILES)
      {
       /*
        * Only load localizations for the color profile related keywords...
        */

	for (i = 0;
	     i < (int)(sizeof(color_keywords) / sizeof(color_keywords[0]));
	     i ++)
	{
	  if (!_cups_strcasecmp(temp, color_keywords[i]))
	    break;
	}

	if (i >= (int)(sizeof(color_keywords) / sizeof(color_keywords[0])))
	{
	  DEBUG_printf(("2_ppdOpen: Ignoring localization: \"%s\"\n", keyword));
	  free(string);
	  string = NULL;
	  continue;
	}
      }
    }

    if (option == NULL &&
        (mask & (PPD_KEYWORD | PPD_OPTION | PPD_STRING)) ==
	    (PPD_KEYWORD | PPD_OPTION | PPD_STRING))
    {
      for (i = 0; i < (int)(sizeof(ui_keywords) / sizeof(ui_keywords[0])); i ++)
        if (!strcmp(keyword, ui_keywords[i]))
	  break;

      if (i < (int)(sizeof(ui_keywords) / sizeof(ui_keywords[0])))
      {
       /*
        * Create the option in the appropriate group...
	*/

        ui_keyword = 1;

        DEBUG_printf(("2_ppdOpen: FOUND ADOBE UI KEYWORD %s WITHOUT OPENUI!",
	              keyword));

        if (!group)
	{
          if ((group = ppd_get_group(ppd, "General", _("General"), pg,
	                             encoding)) == NULL)
	    goto error;

          DEBUG_printf(("2_ppdOpen: Adding to group %s...", group->text));
          option = ppd_get_option(group, keyword);
	  group  = NULL;
	}
	else
          option = ppd_get_option(group, keyword);

	if (option == NULL)
	{
          pg->ppd_status = PPD_ALLOC_ERROR;

          goto error;
	}

       /*
	* Now fill in the initial information for the option...
	*/

	if (!strncmp(keyword, "JCL", 3))
          option->section = PPD_ORDER_JCL;
	else
          option->section = PPD_ORDER_ANY;

	option->order = 10.0f;

	if (i < 8)
          option->ui = PPD_UI_BOOLEAN;
	else
          option->ui = PPD_UI_PICKONE;

        for (j = 0; j < ppd->num_attrs; j ++)
	  if (!strncmp(ppd->attrs[j]->name, "Default", 7) &&
	      !strcmp(ppd->attrs[j]->name + 7, keyword) &&
	      ppd->attrs[j]->value)
	  {
	    DEBUG_printf(("2_ppdOpen: Setting Default%s to %s via attribute...",
	                  option->keyword, ppd->attrs[j]->value));
	    strlcpy(option->defchoice, ppd->attrs[j]->value,
	            sizeof(option->defchoice));
	    break;
	  }

        if (!strcmp(keyword, "PageSize"))
	  strlcpy(option->text, _("Media Size"), sizeof(option->text));
	else if (!strcmp(keyword, "MediaType"))
	  strlcpy(option->text, _("Media Type"), sizeof(option->text));
	else if (!strcmp(keyword, "InputSlot"))
	  strlcpy(option->text, _("Media Source"), sizeof(option->text));
	else if (!strcmp(keyword, "ColorModel"))
	  strlcpy(option->text, _("Output Mode"), sizeof(option->text));
	else if (!strcmp(keyword, "Resolution"))
	  strlcpy(option->text, _("Resolution"), sizeof(option->text));
        else
	  strlcpy(option->text, keyword, sizeof(option->text));
      }
    }

    if (!strcmp(keyword, "LanguageLevel"))
      ppd->language_level = atoi(string);
    else if (!strcmp(keyword, "LanguageEncoding"))
    {
     /*
      * Say all PPD files are UTF-8, since we convert to UTF-8...
      */

      ppd->lang_encoding = strdup("UTF-8");
      encoding           = _ppdGetEncoding(string);
    }
    else if (!strcmp(keyword, "LanguageVersion"))
      ppd->lang_version = string;
    else if (!strcmp(keyword, "Manufacturer"))
      ppd->manufacturer = string;
    else if (!strcmp(keyword, "ModelName"))
      ppd->modelname = string;
    else if (!strcmp(keyword, "Protocols"))
      ppd->protocols = string;
    else if (!strcmp(keyword, "PCFileName"))
      ppd->pcfilename = string;
    else if (!strcmp(keyword, "NickName"))
    {
      if (encoding != CUPS_UTF8)
      {
        cups_utf8_t	utf8[256];	/* UTF-8 version of NickName */


        cupsCharsetToUTF8(utf8, string, sizeof(utf8), encoding);
	ppd->nickname = strdup((char *)utf8);
      }
      else
        ppd->nickname = strdup(string);
    }
    else if (!strcmp(keyword, "Product"))
      ppd->product = string;
    else if (!strcmp(keyword, "ShortNickName"))
      ppd->shortnickname = string;
    else if (!strcmp(keyword, "TTRasterizer"))
      ppd->ttrasterizer = string;
    else if (!strcmp(keyword, "JCLBegin"))
    {
      ppd->jcl_begin = strdup(string);
      ppd_decode(ppd->jcl_begin);	/* Decode quoted string */
    }
    else if (!strcmp(keyword, "JCLEnd"))
    {
      ppd->jcl_end = strdup(string);
      ppd_decode(ppd->jcl_end);		/* Decode quoted string */
    }
    else if (!strcmp(keyword, "JCLToPSInterpreter"))
    {
      ppd->jcl_ps = strdup(string);
      ppd_decode(ppd->jcl_ps);		/* Decode quoted string */
    }
    else if (!strcmp(keyword, "AccurateScreensSupport"))
      ppd->accurate_screens = !strcasecmp(string, "True");
    else if (!strcmp(keyword, "ColorDevice"))
      ppd->color_device = !strcasecmp(string, "True");
    else if (!strcmp(keyword, "ContoneOnly"))
      ppd->contone_only = !strcasecmp(string, "True");
    else if (!strcmp(keyword, "cupsFlipDuplex"))
      ppd->flip_duplex = !strcasecmp(string, "True");
    else if (!strcmp(keyword, "cupsManualCopies"))
      ppd->manual_copies = !strcasecmp(string, "True");
    else if (!strcmp(keyword, "cupsModelNumber"))
      ppd->model_number = atoi(string);
    else if (!strcmp(keyword, "cupsColorProfile"))
    {
      if (ppd->num_profiles == 0)
        profile = malloc(sizeof(ppd_profile_t));
      else
        profile = realloc(ppd->profiles, sizeof(ppd_profile_t) * (size_t)(ppd->num_profiles + 1));

      if (!profile)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      ppd->profiles     = profile;
      profile           += ppd->num_profiles;
      ppd->num_profiles ++;

      memset(profile, 0, sizeof(ppd_profile_t));
      strlcpy(profile->resolution, name, sizeof(profile->resolution));
      strlcpy(profile->media_type, text, sizeof(profile->media_type));

      profile->density      = (float)_cupsStrScand(string, &sptr, loc);
      profile->gamma        = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[0][0] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[0][1] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[0][2] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[1][0] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[1][1] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[1][2] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[2][0] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[2][1] = (float)_cupsStrScand(sptr, &sptr, loc);
      profile->matrix[2][2] = (float)_cupsStrScand(sptr, &sptr, loc);
    }
    else if (!strcmp(keyword, "cupsFilter"))
    {
      if (ppd->num_filters == 0)
        filter = malloc(sizeof(char *));
      else
        filter = realloc(ppd->filters, sizeof(char *) * (size_t)(ppd->num_filters + 1));

      if (filter == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      ppd->filters     = filter;
      filter           += ppd->num_filters;
      ppd->num_filters ++;

     /*
      * Make a copy of the filter string...
      */

      *filter = strdup(string);
    }
    else if (!strcmp(keyword, "Throughput"))
      ppd->throughput = atoi(string);
    else if (!strcmp(keyword, "Font"))
    {
     /*
      * Add this font to the list of available fonts...
      */

      if (ppd->num_fonts == 0)
        tempfonts = (char **)malloc(sizeof(char *));
      else
        tempfonts = (char **)realloc(ppd->fonts, sizeof(char *) * (size_t)(ppd->num_fonts + 1));

      if (tempfonts == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      ppd->fonts                 = tempfonts;
      ppd->fonts[ppd->num_fonts] = strdup(name);
      ppd->num_fonts ++;
    }
    else if (!strncmp(keyword, "ParamCustom", 11))
    {
      ppd_coption_t	*coption;	/* Custom option */
      ppd_cparam_t	*cparam;	/* Custom parameter */
      int		corder;		/* Order number */
      char		ctype[33],	/* Data type */
			cminimum[65],	/* Minimum value */
			cmaximum[65];	/* Maximum value */


     /*
      * Get the custom option and parameter...
      */

      if ((coption = ppd_get_coption(ppd, keyword + 11)) == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      if ((cparam = ppd_get_cparam(coption, name, text)) == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      if (cparam->type != PPD_CUSTOM_UNKNOWN)
      {
        pg->ppd_status = PPD_BAD_CUSTOM_PARAM;

        goto error;
      }

     /*
      * Get the parameter data...
      */

      if (!string ||
          sscanf(string, "%d%32s%64s%64s", &corder, ctype, cminimum,
                 cmaximum) != 4)
      {
        pg->ppd_status = PPD_BAD_CUSTOM_PARAM;

	goto error;
      }

      cparam->order = corder;

      if (!strcmp(ctype, "curve"))
      {
        cparam->type = PPD_CUSTOM_CURVE;
	cparam->minimum.custom_curve = (float)_cupsStrScand(cminimum, NULL, loc);
	cparam->maximum.custom_curve = (float)_cupsStrScand(cmaximum, NULL, loc);
      }
      else if (!strcmp(ctype, "int"))
      {
        cparam->type = PPD_CUSTOM_INT;
	cparam->minimum.custom_int = atoi(cminimum);
	cparam->maximum.custom_int = atoi(cmaximum);
      }
      else if (!strcmp(ctype, "invcurve"))
      {
        cparam->type = PPD_CUSTOM_INVCURVE;
	cparam->minimum.custom_invcurve = (float)_cupsStrScand(cminimum, NULL, loc);
	cparam->maximum.custom_invcurve = (float)_cupsStrScand(cmaximum, NULL, loc);
      }
      else if (!strcmp(ctype, "passcode"))
      {
        cparam->type = PPD_CUSTOM_PASSCODE;
	cparam->minimum.custom_passcode = atoi(cminimum);
	cparam->maximum.custom_passcode = atoi(cmaximum);
      }
      else if (!strcmp(ctype, "password"))
      {
        cparam->type = PPD_CUSTOM_PASSWORD;
	cparam->minimum.custom_password = atoi(cminimum);
	cparam->maximum.custom_password = atoi(cmaximum);
      }
      else if (!strcmp(ctype, "points"))
      {
        cparam->type = PPD_CUSTOM_POINTS;
	cparam->minimum.custom_points = (float)_cupsStrScand(cminimum, NULL, loc);
	cparam->maximum.custom_points = (float)_cupsStrScand(cmaximum, NULL, loc);
      }
      else if (!strcmp(ctype, "real"))
      {
        cparam->type = PPD_CUSTOM_REAL;
	cparam->minimum.custom_real = (float)_cupsStrScand(cminimum, NULL, loc);
	cparam->maximum.custom_real = (float)_cupsStrScand(cmaximum, NULL, loc);
      }
      else if (!strcmp(ctype, "string"))
      {
        cparam->type = PPD_CUSTOM_STRING;
	cparam->minimum.custom_string = atoi(cminimum);
	cparam->maximum.custom_string = atoi(cmaximum);
      }
      else
      {
        pg->ppd_status = PPD_BAD_CUSTOM_PARAM;

	goto error;
      }

     /*
      * Now special-case for CustomPageSize...
      */

      if (!strcmp(coption->keyword, "PageSize"))
      {
	if (!strcmp(name, "Width"))
	{
	  ppd->custom_min[0] = cparam->minimum.custom_points;
	  ppd->custom_max[0] = cparam->maximum.custom_points;
	}
	else if (!strcmp(name, "Height"))
	{
	  ppd->custom_min[1] = cparam->minimum.custom_points;
	  ppd->custom_max[1] = cparam->maximum.custom_points;
	}
      }
    }
    else if (!strcmp(keyword, "HWMargins"))
    {
      for (i = 0, sptr = string; i < 4; i ++)
        ppd->custom_margins[i] = (float)_cupsStrScand(sptr, &sptr, loc);
    }
    else if (!strncmp(keyword, "Custom", 6) && !strcmp(name, "True") && !option)
    {
      ppd_option_t	*custom_option;	/* Custom option */

      DEBUG_puts("2_ppdOpen: Processing Custom option...");

     /*
      * Get the option and custom option...
      */

      if (!ppd_get_coption(ppd, keyword + 6))
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      if (option && !_cups_strcasecmp(option->keyword, keyword + 6))
        custom_option = option;
      else
        custom_option = ppdFindOption(ppd, keyword + 6);

      if (custom_option)
      {
       /*
	* Add the "custom" option...
	*/

        if ((choice = ppdFindChoice(custom_option, "Custom")) == NULL)
	  if ((choice = ppd_add_choice(custom_option, "Custom")) == NULL)
	  {
	    DEBUG_puts("1_ppdOpen: Unable to add Custom choice!");

	    pg->ppd_status = PPD_ALLOC_ERROR;

	    goto error;
	  }

	strlcpy(choice->text, text[0] ? text : _("Custom"),
		sizeof(choice->text));

	choice->code = strdup(string);

	if (custom_option->section == PPD_ORDER_JCL)
	  ppd_decode(choice->code);
      }

     /*
      * Now process custom page sizes specially...
      */

      if (!strcmp(keyword, "CustomPageSize"))
      {
       /*
	* Add a "Custom" page size entry...
	*/

	ppd->variable_sizes = 1;

	ppd_add_size(ppd, "Custom");

	if (option && !_cups_strcasecmp(option->keyword, "PageRegion"))
	  custom_option = option;
	else
	  custom_option = ppdFindOption(ppd, "PageRegion");

        if (custom_option)
	{
	  if ((choice = ppdFindChoice(custom_option, "Custom")) == NULL)
	    if ((choice = ppd_add_choice(custom_option, "Custom")) == NULL)
	    {
	      DEBUG_puts("1_ppdOpen: Unable to add Custom choice!");

	      pg->ppd_status = PPD_ALLOC_ERROR;

	      goto error;
	    }

	  strlcpy(choice->text, text[0] ? text : _("Custom"),
		  sizeof(choice->text));
        }
      }
    }
    else if (!strcmp(keyword, "LandscapeOrientation"))
    {
      if (!strcmp(string, "Minus90"))
        ppd->landscape = -90;
      else if (!strcmp(string, "Plus90"))
        ppd->landscape = 90;
    }
    else if (!strcmp(keyword, "Emulators") && string && ppd->num_emulations == 0)
    {
     /*
      * Issue #5562: Samsung printer drivers incorrectly use Emulators keyword
      *              to configure themselves
      *
      * The Emulators keyword was loaded but never used by anything in CUPS,
      * and has no valid purpose in CUPS.  The old code was removed due to a
      * memory leak (Issue #5475), so the following (new) code supports a single
      * name for the Emulators keyword, allowing these drivers to work until we
      * remove PPD and driver support entirely in a future version of CUPS.
      */

      ppd->num_emulations = 1;
      ppd->emulations     = calloc(1, sizeof(ppd_emul_t));

      strlcpy(ppd->emulations[0].name, string, sizeof(ppd->emulations[0].name));
    }
    else if (!strcmp(keyword, "JobPatchFile"))
    {
     /*
      * CUPS STR #3421: Check for "*JobPatchFile: int: string"
      */

      if (isdigit(*string & 255))
      {
        for (sptr = string + 1; isdigit(*sptr & 255); sptr ++);

        if (*sptr == ':')
        {
         /*
          * Found "*JobPatchFile: int: string"...
          */

          pg->ppd_status = PPD_BAD_VALUE;

	  goto error;
        }
      }

      if (!name[0] && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
       /*
        * Found "*JobPatchFile: string"...
        */

        pg->ppd_status = PPD_MISSING_OPTION_KEYWORD;

	goto error;
      }

      if (ppd->patches == NULL)
        ppd->patches = strdup(string);
      else
      {
        temp = realloc(ppd->patches, strlen(ppd->patches) +
	                             strlen(string) + 1);
        if (temp == NULL)
	{
          pg->ppd_status = PPD_ALLOC_ERROR;

	  goto error;
	}

        ppd->patches = temp;

        memcpy(ppd->patches + strlen(ppd->patches), string, strlen(string) + 1);
      }
    }
    else if (!strcmp(keyword, "OpenUI"))
    {
     /*
      * Don't allow nesting of options...
      */

      if (option && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
        pg->ppd_status = PPD_NESTED_OPEN_UI;

	goto error;
      }

     /*
      * Add an option record to the current sub-group, group, or file...
      */

      DEBUG_printf(("2_ppdOpen: name=\"%s\" (%d)", name, (int)strlen(name)));

      if (name[0] == '*')
        _cups_strcpy(name, name + 1); /* Eliminate leading asterisk */

      for (i = (int)strlen(name) - 1; i > 0 && _cups_isspace(name[i]); i --)
        name[i] = '\0'; /* Eliminate trailing spaces */

      DEBUG_printf(("2_ppdOpen: OpenUI of %s in group %s...", name,
                    group ? group->text : "(null)"));

      if (subgroup != NULL)
        option = ppd_get_option(subgroup, name);
      else if (group == NULL)
      {
	if ((group = ppd_get_group(ppd, "General", _("General"), pg,
	                           encoding)) == NULL)
	  goto error;

        DEBUG_printf(("2_ppdOpen: Adding to group %s...", group->text));
        option = ppd_get_option(group, name);
	group  = NULL;
      }
      else
        option = ppd_get_option(group, name);

      if (option == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

     /*
      * Now fill in the initial information for the option...
      */

      if (string && !strcmp(string, "PickMany"))
        option->ui = PPD_UI_PICKMANY;
      else if (string && !strcmp(string, "Boolean"))
        option->ui = PPD_UI_BOOLEAN;
      else if (string && !strcmp(string, "PickOne"))
        option->ui = PPD_UI_PICKONE;
      else if (pg->ppd_conform == PPD_CONFORM_STRICT)
      {
        pg->ppd_status = PPD_BAD_OPEN_UI;

	goto error;
      }
      else
        option->ui = PPD_UI_PICKONE;

      for (j = 0; j < ppd->num_attrs; j ++)
	if (!strncmp(ppd->attrs[j]->name, "Default", 7) &&
	    !strcmp(ppd->attrs[j]->name + 7, name) &&
	    ppd->attrs[j]->value)
	{
	  DEBUG_printf(("2_ppdOpen: Setting Default%s to %s via attribute...",
	                option->keyword, ppd->attrs[j]->value));
	  strlcpy(option->defchoice, ppd->attrs[j]->value,
	          sizeof(option->defchoice));
	  break;
	}

      if (text[0])
        cupsCharsetToUTF8((cups_utf8_t *)option->text, text,
	                   sizeof(option->text), encoding);
      else
      {
        if (!strcmp(name, "PageSize"))
	  strlcpy(option->text, _("Media Size"), sizeof(option->text));
	else if (!strcmp(name, "MediaType"))
	  strlcpy(option->text, _("Media Type"), sizeof(option->text));
	else if (!strcmp(name, "InputSlot"))
	  strlcpy(option->text, _("Media Source"), sizeof(option->text));
	else if (!strcmp(name, "ColorModel"))
	  strlcpy(option->text, _("Output Mode"), sizeof(option->text));
	else if (!strcmp(name, "Resolution"))
	  strlcpy(option->text, _("Resolution"), sizeof(option->text));
        else
	  strlcpy(option->text, name, sizeof(option->text));
      }

      option->section = PPD_ORDER_ANY;

      free(string);
      string = NULL;

     /*
      * Add a custom option choice if we have already seen a CustomFoo
      * attribute...
      */

      if (!_cups_strcasecmp(name, "PageRegion"))
        strlcpy(custom_name, "CustomPageSize", sizeof(custom_name));
      else
        snprintf(custom_name, sizeof(custom_name), "Custom%s", name);

      if ((custom_attr = ppdFindAttr(ppd, custom_name, "True")) != NULL)
      {
        if ((choice = ppdFindChoice(option, "Custom")) == NULL)
	  if ((choice = ppd_add_choice(option, "Custom")) == NULL)
	  {
	    DEBUG_puts("1_ppdOpen: Unable to add Custom choice!");

	    pg->ppd_status = PPD_ALLOC_ERROR;

	    goto error;
	  }

	strlcpy(choice->text,
	        custom_attr->text[0] ? custom_attr->text : _("Custom"),
		sizeof(choice->text));
        choice->code = strdup(custom_attr->value);
      }
    }
    else if (!strcmp(keyword, "JCLOpenUI"))
    {
     /*
      * Don't allow nesting of options...
      */

      if (option && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
        pg->ppd_status = PPD_NESTED_OPEN_UI;

	goto error;
      }

     /*
      * Find the JCL group, and add if needed...
      */

      group = ppd_get_group(ppd, "JCL", _("JCL"), pg, encoding);

      if (group == NULL)
	goto error;

     /*
      * Add an option record to the current JCLs...
      */

      if (name[0] == '*')
        _cups_strcpy(name, name + 1);

      option = ppd_get_option(group, name);

      if (option == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

     /*
      * Now fill in the initial information for the option...
      */

      if (string && !strcmp(string, "PickMany"))
        option->ui = PPD_UI_PICKMANY;
      else if (string && !strcmp(string, "Boolean"))
        option->ui = PPD_UI_BOOLEAN;
      else if (string && !strcmp(string, "PickOne"))
        option->ui = PPD_UI_PICKONE;
      else
      {
        pg->ppd_status = PPD_BAD_OPEN_UI;

	goto error;
      }

      for (j = 0; j < ppd->num_attrs; j ++)
	if (!strncmp(ppd->attrs[j]->name, "Default", 7) &&
	    !strcmp(ppd->attrs[j]->name + 7, name) &&
	    ppd->attrs[j]->value)
	{
	  DEBUG_printf(("2_ppdOpen: Setting Default%s to %s via attribute...",
	                option->keyword, ppd->attrs[j]->value));
	  strlcpy(option->defchoice, ppd->attrs[j]->value,
	          sizeof(option->defchoice));
	  break;
	}

      if (text[0])
        cupsCharsetToUTF8((cups_utf8_t *)option->text, text,
	                   sizeof(option->text), encoding);
      else
        strlcpy(option->text, name, sizeof(option->text));

      option->section = PPD_ORDER_JCL;
      group = NULL;

      free(string);
      string = NULL;

     /*
      * Add a custom option choice if we have already seen a CustomFoo
      * attribute...
      */

      snprintf(custom_name, sizeof(custom_name), "Custom%s", name);

      if ((custom_attr = ppdFindAttr(ppd, custom_name, "True")) != NULL)
      {
	if ((choice = ppd_add_choice(option, "Custom")) == NULL)
	{
	  DEBUG_puts("1_ppdOpen: Unable to add Custom choice!");

	  pg->ppd_status = PPD_ALLOC_ERROR;

	  goto error;
	}

	strlcpy(choice->text,
	        custom_attr->text[0] ? custom_attr->text : _("Custom"),
		sizeof(choice->text));
        choice->code = strdup(custom_attr->value);
      }
    }
    else if (!strcmp(keyword, "CloseUI"))
    {
      if ((!option || option->section == PPD_ORDER_JCL) && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
        pg->ppd_status = PPD_BAD_CLOSE_UI;

	goto error;
      }

      if (option && (!_cups_strcasecmp(option->defchoice, "custom") || !_cups_strncasecmp(option->defchoice, "custom.", 7)))
      {
       /*
	* "*DefaultOption: Custom..." may set the default to a custom value
	* or (for a very small number of incompatible PPD files) select a
	* standard choice for the option, which CUPS renames to "_Custom..."
	* to avoid compatibility issues.  See which this is...
	*/

        char tchoice[PPD_MAX_NAME];	/* Temporary choice name */

	snprintf(tchoice, sizeof(tchoice), "_%s", option->defchoice);

	if (ppdFindChoice(option, tchoice))
	{
	  strlcpy(option->defchoice, tchoice, sizeof(option->defchoice));

	  DEBUG_printf(("2_ppdOpen: Reset Default%s to %s...", option->keyword, tchoice));
	}
      }

      option = NULL;

      free(string);
      string = NULL;
    }
    else if (!strcmp(keyword, "JCLCloseUI"))
    {
      if ((!option || option->section != PPD_ORDER_JCL) && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
        pg->ppd_status = PPD_BAD_CLOSE_UI;

	goto error;
      }

      if (option && (!_cups_strcasecmp(option->defchoice, "custom") || !_cups_strncasecmp(option->defchoice, "custom.", 7)))
      {
       /*
	* "*DefaultOption: Custom..." may set the default to a custom value
	* or (for a very small number of incompatible PPD files) select a
	* standard choice for the option, which CUPS renames to "_Custom..."
	* to avoid compatibility issues.  See which this is...
	*/

        char tchoice[PPD_MAX_NAME];	/* Temporary choice name */

	snprintf(tchoice, sizeof(tchoice), "_%s", option->defchoice);

	if (ppdFindChoice(option, tchoice))
	{
	  strlcpy(option->defchoice, tchoice, sizeof(option->defchoice));

	  DEBUG_printf(("2_ppdOpen: Reset Default%s to %s...", option->keyword, tchoice));
	}
      }

      option = NULL;

      free(string);
      string = NULL;
    }
    else if (!strcmp(keyword, "OpenGroup"))
    {
     /*
      * Open a new group...
      */

      if (group != NULL)
      {
        pg->ppd_status = PPD_NESTED_OPEN_GROUP;

	goto error;
      }

      if (!string)
      {
        pg->ppd_status = PPD_BAD_OPEN_GROUP;

	goto error;
      }

     /*
      * Separate the group name from the text (name/text)...
      */

      if ((sptr = strchr(string, '/')) != NULL)
        *sptr++ = '\0';
      else
        sptr = string;

     /*
      * Fix up the text...
      */

      ppd_decode(sptr);

     /*
      * Find/add the group...
      */

      group = ppd_get_group(ppd, string, sptr, pg, encoding);

      if (group == NULL)
	goto error;

      free(string);
      string = NULL;
    }
    else if (!strcmp(keyword, "CloseGroup"))
    {
      group = NULL;

      free(string);
      string = NULL;
    }
    else if (!strcmp(keyword, "OrderDependency"))
    {
      order = (float)_cupsStrScand(string, &sptr, loc);

      if (!sptr || sscanf(sptr, "%40s%40s", name, keyword) != 2)
      {
        pg->ppd_status = PPD_BAD_ORDER_DEPENDENCY;

	goto error;
      }

      if (keyword[0] == '*')
        _cups_strcpy(keyword, keyword + 1);

      if (!strcmp(name, "ExitServer"))
        section = PPD_ORDER_EXIT;
      else if (!strcmp(name, "Prolog"))
        section = PPD_ORDER_PROLOG;
      else if (!strcmp(name, "DocumentSetup"))
        section = PPD_ORDER_DOCUMENT;
      else if (!strcmp(name, "PageSetup"))
        section = PPD_ORDER_PAGE;
      else if (!strcmp(name, "JCLSetup"))
        section = PPD_ORDER_JCL;
      else
        section = PPD_ORDER_ANY;

      if (option == NULL)
      {
        ppd_group_t	*gtemp;


       /*
        * Only valid for Non-UI options...
	*/

        for (i = ppd->num_groups, gtemp = ppd->groups; i > 0; i --, gtemp ++)
          if (gtemp->text[0] == '\0')
	    break;

        if (i > 0)
          for (i = 0; i < gtemp->num_options; i ++)
	    if (!strcmp(keyword, gtemp->options[i].keyword))
	    {
	      gtemp->options[i].section = section;
	      gtemp->options[i].order   = order;
	      break;
	    }
      }
      else
      {
        option->section = section;
	option->order   = order;
      }

      free(string);
      string = NULL;
    }
    else if (!strncmp(keyword, "Default", 7))
    {
      if (string == NULL)
        continue;

     /*
      * Drop UI text, if any, from value...
      */

      if (strchr(string, '/') != NULL)
        *strchr(string, '/') = '\0';

     /*
      * Assign the default value as appropriate...
      */

      if (!strcmp(keyword, "DefaultColorSpace"))
      {
       /*
        * Set default colorspace...
	*/

	if (!strcmp(string, "CMY"))
          ppd->colorspace = PPD_CS_CMY;
	else if (!strcmp(string, "CMYK"))
          ppd->colorspace = PPD_CS_CMYK;
	else if (!strcmp(string, "RGB"))
          ppd->colorspace = PPD_CS_RGB;
	else if (!strcmp(string, "RGBK"))
          ppd->colorspace = PPD_CS_RGBK;
	else if (!strcmp(string, "N"))
          ppd->colorspace = PPD_CS_N;
	else
          ppd->colorspace = PPD_CS_GRAY;
      }
      else if (option && !strcmp(keyword + 7, option->keyword))
      {
       /*
        * Set the default as part of the current option...
	*/

	strlcpy(option->defchoice, string, sizeof(option->defchoice));

        DEBUG_printf(("2_ppdOpen: Set %s to %s...", keyword, option->defchoice));
      }
      else
      {
       /*
        * Lookup option and set if it has been defined...
	*/

        ppd_option_t	*toption;	/* Temporary option */

        if ((toption = ppdFindOption(ppd, keyword + 7)) != NULL)
	{
	  if (!_cups_strcasecmp(string, "custom") || !_cups_strncasecmp(string, "custom.", 7))
	  {
	   /*
	    * "*DefaultOption: Custom..." may set the default to a custom value
	    * or (for a very small number of incompatible PPD files) select a
	    * standard choice for the option, which CUPS renames to "_Custom..."
	    * to avoid compatibility issues.  See which this is...
	    */

	    snprintf(toption->defchoice, sizeof(toption->defchoice), "_%s", string);
	    if (!ppdFindChoice(toption, toption->defchoice))
	      strlcpy(toption->defchoice, string, sizeof(toption->defchoice));
	  }
	  else
	  {
	    strlcpy(toption->defchoice, string, sizeof(toption->defchoice));
	  }

	  DEBUG_printf(("2_ppdOpen: Set %s to %s...", keyword, toption->defchoice));
	}
      }
    }
    else if (!strcmp(keyword, "UIConstraints") ||
             !strcmp(keyword, "NonUIConstraints"))
    {
      if (!string)
      {
	pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	goto error;
      }

      if (ppd->num_consts == 0)
	constraint = calloc(2, sizeof(ppd_const_t));
      else
	constraint = realloc(ppd->consts, (size_t)(ppd->num_consts + 2) * sizeof(ppd_const_t));

      if (constraint == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      ppd->consts = constraint;
      constraint += ppd->num_consts;
      ppd->num_consts ++;

      switch (sscanf(string, "%40s%40s%40s%40s", constraint->option1,
                     constraint->choice1, constraint->option2,
		     constraint->choice2))
      {
        default : /* Error */
	    pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	    goto error;

	case 2 : /* Two options... */
	   /*
	    * Check for broken constraints like "* Option"...
	    */

	    if (pg->ppd_conform == PPD_CONFORM_STRICT &&
	        (!strcmp(constraint->option1, "*") ||
	         !strcmp(constraint->choice1, "*")))
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

	   /*
	    * The following strcpy's are safe, as optionN and
	    * choiceN are all the same size (size defined by PPD spec...)
	    */

	    if (constraint->option1[0] == '*')
	      _cups_strcpy(constraint->option1, constraint->option1 + 1);
	    else if (pg->ppd_conform == PPD_CONFORM_STRICT)
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

	    if (constraint->choice1[0] == '*')
	      _cups_strcpy(constraint->option2, constraint->choice1 + 1);
	    else if (pg->ppd_conform == PPD_CONFORM_STRICT)
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

            constraint->choice1[0] = '\0';
            constraint->choice2[0] = '\0';
	    break;

	case 3 : /* Two options, one choice... */
	   /*
	    * Check for broken constraints like "* Option"...
	    */

	    if (pg->ppd_conform == PPD_CONFORM_STRICT &&
	        (!strcmp(constraint->option1, "*") ||
	         !strcmp(constraint->choice1, "*") ||
	         !strcmp(constraint->option2, "*")))
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

	   /*
	    * The following _cups_strcpy's are safe, as optionN and
	    * choiceN are all the same size (size defined by PPD spec...)
	    */

	    if (constraint->option1[0] == '*')
	      _cups_strcpy(constraint->option1, constraint->option1 + 1);
	    else if (pg->ppd_conform == PPD_CONFORM_STRICT)
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

	    if (constraint->choice1[0] == '*')
	    {
	      if (pg->ppd_conform == PPD_CONFORM_STRICT &&
	          constraint->option2[0] == '*')
	      {
		pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
		goto error;
	      }

	      _cups_strcpy(constraint->choice2, constraint->option2);
	      _cups_strcpy(constraint->option2, constraint->choice1 + 1);
              constraint->choice1[0] = '\0';
	    }
	    else
	    {
	      if (constraint->option2[0] == '*')
  	        _cups_strcpy(constraint->option2, constraint->option2 + 1);
	      else if (pg->ppd_conform == PPD_CONFORM_STRICT)
	      {
		pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
		goto error;
	      }

              constraint->choice2[0] = '\0';
	    }
	    break;

	case 4 : /* Two options, two choices... */
	   /*
	    * Check for broken constraints like "* Option"...
	    */

	    if (pg->ppd_conform == PPD_CONFORM_STRICT &&
	        (!strcmp(constraint->option1, "*") ||
	         !strcmp(constraint->choice1, "*") ||
	         !strcmp(constraint->option2, "*") ||
	         !strcmp(constraint->choice2, "*")))
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

	    if (constraint->option1[0] == '*')
	      _cups_strcpy(constraint->option1, constraint->option1 + 1);
	    else if (pg->ppd_conform == PPD_CONFORM_STRICT)
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

            if (pg->ppd_conform == PPD_CONFORM_STRICT &&
	        constraint->choice1[0] == '*')
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

	    if (constraint->option2[0] == '*')
  	      _cups_strcpy(constraint->option2, constraint->option2 + 1);
	    else if (pg->ppd_conform == PPD_CONFORM_STRICT)
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }

            if (pg->ppd_conform == PPD_CONFORM_STRICT &&
	        constraint->choice2[0] == '*')
	    {
	      pg->ppd_status = PPD_BAD_UI_CONSTRAINTS;
	      goto error;
	    }
	    break;
      }

     /*
      * Don't add this one as an attribute...
      */

      free(string);
      string = NULL;
    }
    else if (!strcmp(keyword, "PaperDimension"))
    {
      if (!_cups_strcasecmp(name, "custom") || !_cups_strncasecmp(name, "custom.", 7))
      {
        char cname[PPD_MAX_NAME];	/* Rewrite with a leading underscore */
        snprintf(cname, sizeof(cname), "_%s", name);
        strlcpy(name, cname, sizeof(name));
      }

      if ((size = ppdPageSize(ppd, name)) == NULL)
	size = ppd_add_size(ppd, name);

      if (size == NULL)
      {
       /*
        * Unable to add or find size!
	*/

        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      size->width  = (float)_cupsStrScand(string, &sptr, loc);
      size->length = (float)_cupsStrScand(sptr, NULL, loc);

      free(string);
      string = NULL;
    }
    else if (!strcmp(keyword, "ImageableArea"))
    {
      if (!_cups_strcasecmp(name, "custom") || !_cups_strncasecmp(name, "custom.", 7))
      {
        char cname[PPD_MAX_NAME];	/* Rewrite with a leading underscore */
        snprintf(cname, sizeof(cname), "_%s", name);
        strlcpy(name, cname, sizeof(name));
      }

      if ((size = ppdPageSize(ppd, name)) == NULL)
	size = ppd_add_size(ppd, name);

      if (size == NULL)
      {
       /*
        * Unable to add or find size!
	*/

        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      size->left   = (float)_cupsStrScand(string, &sptr, loc);
      size->bottom = (float)_cupsStrScand(sptr, &sptr, loc);
      size->right  = (float)_cupsStrScand(sptr, &sptr, loc);
      size->top    = (float)_cupsStrScand(sptr, NULL, loc);

      free(string);
      string = NULL;
    }
    else if (option != NULL &&
             (mask & (PPD_KEYWORD | PPD_OPTION | PPD_STRING)) ==
	         (PPD_KEYWORD | PPD_OPTION | PPD_STRING) &&
	     !strcmp(keyword, option->keyword))
    {
      DEBUG_printf(("2_ppdOpen: group=%p, subgroup=%p", group, subgroup));

      if (!_cups_strcasecmp(name, "custom") || !_cups_strncasecmp(name, "custom.", 7))
      {
        char cname[PPD_MAX_NAME];	/* Rewrite with a leading underscore */
        snprintf(cname, sizeof(cname), "_%s", name);
        strlcpy(name, cname, sizeof(name));
      }

      if (!strcmp(keyword, "PageSize"))
      {
       /*
        * Add a page size...
	*/

        if (ppdPageSize(ppd, name) == NULL)
	  ppd_add_size(ppd, name);
      }

     /*
      * Add the option choice...
      */

      if ((choice = ppd_add_choice(option, name)) == NULL)
      {
        pg->ppd_status = PPD_ALLOC_ERROR;

	goto error;
      }

      if (text[0])
        cupsCharsetToUTF8((cups_utf8_t *)choice->text, text,
	                   sizeof(choice->text), encoding);
      else if (!strcmp(name, "True"))
        strlcpy(choice->text, _("Yes"), sizeof(choice->text));
      else if (!strcmp(name, "False"))
        strlcpy(choice->text, _("No"), sizeof(choice->text));
      else
        strlcpy(choice->text, name, sizeof(choice->text));

      if (option->section == PPD_ORDER_JCL)
        ppd_decode(string);		/* Decode quoted string */

      choice->code = string;
      string       = NULL;		/* Don't add as an attribute below */
    }

   /*
    * Add remaining lines with keywords and string values as attributes...
    */

    if (string &&
        (mask & (PPD_KEYWORD | PPD_STRING)) == (PPD_KEYWORD | PPD_STRING))
      ppd_add_attr(ppd, keyword, name, text, string);
    else
      free(string);
  }

 /*
  * Check for a missing CloseUI/JCLCloseUI...
  */

  if (option && pg->ppd_conform == PPD_CONFORM_STRICT)
  {
    pg->ppd_status = PPD_MISSING_CLOSE_UI;
    goto error;
  }

 /*
  * Check for a missing CloseGroup...
  */

  if (group && pg->ppd_conform == PPD_CONFORM_STRICT)
  {
    pg->ppd_status = PPD_MISSING_CLOSE_GROUP;
    goto error;
  }

  free(line.buffer);

 /*
  * Reset language preferences...
  */

#ifdef DEBUG
  if (!cupsFileEOF(fp))
    DEBUG_printf(("1_ppdOpen: Premature EOF at %lu...\n",
                  (unsigned long)cupsFileTell(fp)));
#endif /* DEBUG */

  if (pg->ppd_status != PPD_OK)
  {
   /*
    * Had an error reading the PPD file, cannot continue!
    */

    ppdClose(ppd);

    return (NULL);
  }

 /*
  * Update the filters array as needed...
  */

  if (!ppd_update_filters(ppd, pg))
  {
    ppdClose(ppd);

    return (NULL);
  }

 /*
  * Create the sorted options array and set the option back-pointer for
  * each choice and custom option...
  */

  ppd->options = cupsArrayNew2((cups_array_func_t)ppd_compare_options, NULL,
                               (cups_ahash_func_t)ppd_hash_option,
			       PPD_HASHSIZE);

  for (i = ppd->num_groups, group = ppd->groups;
       i > 0;
       i --, group ++)
  {
    for (j = group->num_options, option = group->options;
         j > 0;
	 j --, option ++)
    {
      ppd_coption_t	*coption;	/* Custom option */


      cupsArrayAdd(ppd->options, option);

      for (k = 0; k < option->num_choices; k ++)
        option->choices[k].option = option;

      if ((coption = ppdFindCustomOption(ppd, option->keyword)) != NULL)
        coption->option = option;
    }
  }

 /*
  * Create an array to track the marked choices...
  */

  ppd->marked = cupsArrayNew((cups_array_func_t)ppd_compare_choices, NULL);

 /*
  * Return the PPD file structure...
  */

  return (ppd);

 /*
  * Common exit point for errors to save code size...
  */

  error:

  free(string);
  free(line.buffer);

  ppdClose(ppd);

  return (NULL);
}


/*
 * 'ppdOpen()' - Read a PPD file into memory.
 */

ppd_file_t *				/* O - PPD file record */
ppdOpen(FILE *fp)			/* I - File to read from */
{
  ppd_file_t	*ppd;			/* PPD file record */
  cups_file_t	*cf;			/* CUPS file */


 /*
  * Reopen the stdio file as a CUPS file...
  */

  if ((cf = cupsFileOpenFd(fileno(fp), "r")) == NULL)
    return (NULL);

 /*
  * Load the PPD file using the newer API...
  */

  ppd = _ppdOpen(cf, _PPD_LOCALIZATION_DEFAULT);

 /*
  * Close the CUPS file and return the PPD...
  */

  cupsFileClose(cf);

  return (ppd);
}


/*
 * 'ppdOpen2()' - Read a PPD file into memory.
 *
 * @since CUPS 1.2/macOS 10.5@
 */

ppd_file_t *				/* O - PPD file record or @code NULL@ if the PPD file could not be opened. */
ppdOpen2(cups_file_t *fp)		/* I - File to read from */
{
  return _ppdOpen(fp, _PPD_LOCALIZATION_DEFAULT);
}


/*
 * 'ppdOpenFd()' - Read a PPD file into memory.
 */

ppd_file_t *				/* O - PPD file record or @code NULL@ if the PPD file could not be opened. */
ppdOpenFd(int fd)			/* I - File to read from */
{
  cups_file_t		*fp;		/* CUPS file pointer */
  ppd_file_t		*ppd;		/* PPD file record */
  _ppd_globals_t	*pg = _ppdGlobals();
					/* Global data */


 /*
  * Set the line number to 0...
  */

  pg->ppd_line = 0;

 /*
  * Range check input...
  */

  if (fd < 0)
  {
    pg->ppd_status = PPD_NULL_FILE;

    return (NULL);
  }

 /*
  * Try to open the file and parse it...
  */

  if ((fp = cupsFileOpenFd(fd, "r")) != NULL)
  {
    ppd = ppdOpen2(fp);

    cupsFileClose(fp);
  }
  else
  {
    pg->ppd_status = PPD_FILE_OPEN_ERROR;
    ppd            = NULL;
  }

  return (ppd);
}


/*
 * '_ppdOpenFile()' - Read a PPD file into memory.
 */

ppd_file_t *				/* O - PPD file record or @code NULL@ if the PPD file could not be opened. */
_ppdOpenFile(const char		  *filename,	/* I - File to read from */
	     _ppd_localization_t  localization)	/* I - Localization to load */
{
  cups_file_t		*fp;		/* File pointer */
  ppd_file_t		*ppd;		/* PPD file record */
  _ppd_globals_t	*pg = _ppdGlobals();
					/* Global data */


 /*
  * Set the line number to 0...
  */

  pg->ppd_line = 0;

 /*
  * Range check input...
  */

  if (filename == NULL)
  {
    pg->ppd_status = PPD_NULL_FILE;

    return (NULL);
  }

 /*
  * Try to open the file and parse it...
  */

  if ((fp = cupsFileOpen(filename, "r")) != NULL)
  {
    ppd = _ppdOpen(fp, localization);

    cupsFileClose(fp);
  }
  else
  {
    pg->ppd_status = PPD_FILE_OPEN_ERROR;
    ppd            = NULL;
  }

  return (ppd);
}


/*
 * 'ppdOpenFile()' - Read a PPD file into memory.
 */

ppd_file_t *				/* O - PPD file record or @code NULL@ if the PPD file could not be opened. */
ppdOpenFile(const char *filename)	/* I - File to read from */
{
  return _ppdOpenFile(filename, _PPD_LOCALIZATION_DEFAULT);
}


/*
 * 'ppdSetConformance()' - Set the conformance level for PPD files.
 *
 * @since CUPS 1.1.20/macOS 10.4@
 */

void
ppdSetConformance(ppd_conform_t c)	/* I - Conformance level */
{
  _ppd_globals_t	*pg = _ppdGlobals();
					/* Global data */


  pg->ppd_conform = c;
}


/*
 * 'ppd_add_attr()' - Add an attribute to the PPD data.
 */

static ppd_attr_t *			/* O - New attribute */
ppd_add_attr(ppd_file_t *ppd,		/* I - PPD file data */
             const char *name,		/* I - Attribute name */
             const char *spec,		/* I - Specifier string, if any */
	     const char *text,		/* I - Text string, if any */
	     const char *value)		/* I - Value of attribute */
{
  ppd_attr_t	**ptr,			/* New array */
		*temp;			/* New attribute */


 /*
  * Range check input...
  */

  if (ppd == NULL || name == NULL || spec == NULL)
    return (NULL);

 /*
  * Create the array as needed...
  */

  if (!ppd->sorted_attrs)
    ppd->sorted_attrs = cupsArrayNew((cups_array_func_t)ppd_compare_attrs,
                                     NULL);

 /*
  * Allocate memory for the new attribute...
  */

  if (ppd->num_attrs == 0)
    ptr = malloc(sizeof(ppd_attr_t *));
  else
    ptr = realloc(ppd->attrs, (size_t)(ppd->num_attrs + 1) * sizeof(ppd_attr_t *));

  if (ptr == NULL)
    return (NULL);

  ppd->attrs = ptr;
  ptr += ppd->num_attrs;

  if ((temp = calloc(1, sizeof(ppd_attr_t))) == NULL)
    return (NULL);

  *ptr = temp;

  ppd->num_attrs ++;

 /*
  * Copy data over...
  */

  if (!_cups_strcasecmp(spec, "custom") || !_cups_strncasecmp(spec, "custom.", 7))
  {
    temp->spec[0] = '_';
    strlcpy(temp->spec + 1, spec, sizeof(temp->spec) - 1);
  }
  else {
      strlcpy(temp->spec, spec, sizeof(temp->spec));
  }

  strlcpy(temp->name, name, sizeof(temp->name));
  strlcpy(temp->text, text, sizeof(temp->text));
  temp->value = (char *)value;

 /*
  * Add the attribute to the sorted array...
  */

  cupsArrayAdd(ppd->sorted_attrs, temp);

 /*
  * Return the attribute...
  */

  return (temp);
}


/*
 * 'ppd_add_choice()' - Add a choice to an option.
 */

static ppd_choice_t *			/* O - Named choice */
ppd_add_choice(ppd_option_t *option,	/* I - Option */
               const char   *name)	/* I - Name of choice */
{
  ppd_choice_t	*choice;		/* Choice */


  if (option->num_choices == 0)
    choice = malloc(sizeof(ppd_choice_t));
  else
    choice = realloc(option->choices, sizeof(ppd_choice_t) * (size_t)(option->num_choices + 1));

  if (choice == NULL)
    return (NULL);

  option->choices = choice;
  choice += option->num_choices;
  option->num_choices ++;

  memset(choice, 0, sizeof(ppd_choice_t));
  strlcpy(choice->choice, name, sizeof(choice->choice));

  return (choice);
}


/*
 * 'ppd_add_size()' - Add a page size.
 */

static ppd_size_t *			/* O - Named size */
ppd_add_size(ppd_file_t *ppd,		/* I - PPD file */
             const char *name)		/* I - Name of size */
{
  ppd_size_t	*size;			/* Size */


  if (ppd->num_sizes == 0)
    size = malloc(sizeof(ppd_size_t));
  else
    size = realloc(ppd->sizes, sizeof(ppd_size_t) * (size_t)(ppd->num_sizes + 1));

  if (size == NULL)
    return (NULL);

  ppd->sizes = size;
  size += ppd->num_sizes;
  ppd->num_sizes ++;

  memset(size, 0, sizeof(ppd_size_t));
  strlcpy(size->name, name, sizeof(size->name));

  return (size);
}


/*
 * 'ppd_compare_attrs()' - Compare two attributes.
 */

static int				/* O - Result of comparison */
ppd_compare_attrs(ppd_attr_t *a,	/* I - First attribute */
                  ppd_attr_t *b)	/* I - Second attribute */
{
  return (_cups_strcasecmp(a->name, b->name));
}


/*
 * 'ppd_compare_choices()' - Compare two choices...
 */

static int				/* O - Result of comparison */
ppd_compare_choices(ppd_choice_t *a,	/* I - First choice */
                    ppd_choice_t *b)	/* I - Second choice */
{
  return (strcmp(a->option->keyword, b->option->keyword));
}


/*
 * 'ppd_compare_coptions()' - Compare two custom options.
 */

static int				/* O - Result of comparison */
ppd_compare_coptions(ppd_coption_t *a,	/* I - First option */
                     ppd_coption_t *b)	/* I - Second option */
{
  return (_cups_strcasecmp(a->keyword, b->keyword));
}


/*
 * 'ppd_compare_options()' - Compare two options.
 */

static int				/* O - Result of comparison */
ppd_compare_options(ppd_option_t *a,	/* I - First option */
                    ppd_option_t *b)	/* I - Second option */
{
  return (_cups_strcasecmp(a->keyword, b->keyword));
}


/*
 * 'ppd_decode()' - Decode a string value...
 */

static int				/* O - Length of decoded string */
ppd_decode(char *string)		/* I - String to decode */
{
  char	*inptr,				/* Input pointer */
	*outptr;			/* Output pointer */


  inptr  = string;
  outptr = string;

  while (*inptr != '\0')
    if (*inptr == '<' && isxdigit(inptr[1] & 255))
    {
     /*
      * Convert hex to 8-bit values...
      */

      inptr ++;
      while (isxdigit(*inptr & 255))
      {
	if (_cups_isalpha(*inptr))
	  *outptr = (char)((tolower(*inptr) - 'a' + 10) << 4);
	else
	  *outptr = (char)((*inptr - '0') << 4);

	inptr ++;

        if (!isxdigit(*inptr & 255))
	  break;

	if (_cups_isalpha(*inptr))
	  *outptr |= (char)(tolower(*inptr) - 'a' + 10);
	else
	  *outptr |= (char)(*inptr - '0');

	inptr ++;
	outptr ++;
      }

      while (*inptr != '>' && *inptr != '\0')
	inptr ++;
      while (*inptr == '>')
	inptr ++;
    }
    else
      *outptr++ = *inptr++;

  *outptr = '\0';

  return ((int)(outptr - string));
}


/*
 * 'ppd_free_filters()' - Free the filters array.
 */

static void
ppd_free_filters(ppd_file_t *ppd)	/* I - PPD file */
{
  int	i;				/* Looping var */
  char	**filter;			/* Current filter */


  if (ppd->num_filters > 0)
  {
    for (i = ppd->num_filters, filter = ppd->filters; i > 0; i --, filter ++)
      free(*filter);

    free(ppd->filters);

    ppd->num_filters = 0;
    ppd->filters     = NULL;
  }
}


/*
 * 'ppd_free_group()' - Free a single UI group.
 */

static void
ppd_free_group(ppd_group_t *group)	/* I - Group to free */
{
  int		i;			/* Looping var */
  ppd_option_t	*option;		/* Current option */
  ppd_group_t	*subgroup;		/* Current sub-group */


  if (group->num_options > 0)
  {
    for (i = group->num_options, option = group->options;
         i > 0;
	 i --, option ++)
      ppd_free_option(option);

    free(group->options);
  }

  if (group->num_subgroups > 0)
  {
    for (i = group->num_subgroups, subgroup = group->subgroups;
         i > 0;
	 i --, subgroup ++)
      ppd_free_group(subgroup);

    free(group->subgroups);
  }
}


/*
 * 'ppd_free_option()' - Free a single option.
 */

static void
ppd_free_option(ppd_option_t *option)	/* I - Option to free */
{
  int		i;			/* Looping var */
  ppd_choice_t	*choice;		/* Current choice */


  if (option->num_choices > 0)
  {
    for (i = option->num_choices, choice = option->choices;
         i > 0;
         i --, choice ++)
    {
      free(choice->code);
    }

    free(option->choices);
  }
}


/*
 * 'ppd_get_coption()' - Get a custom option record.
 */

static ppd_coption_t	*		/* O - Custom option... */
ppd_get_coption(ppd_file_t *ppd,	/* I - PPD file */
                const char *name)	/* I - Name of option */
{
  ppd_coption_t	*copt;			/* New custom option */


 /*
  * See if the option already exists...
  */

  if ((copt = ppdFindCustomOption(ppd, name)) != NULL)
    return (copt);

 /*
  * Not found, so create the custom option record...
  */

  if ((copt = calloc(1, sizeof(ppd_coption_t))) == NULL)
    return (NULL);

  strlcpy(copt->keyword, name, sizeof(copt->keyword));

  copt->params = cupsArrayNew((cups_array_func_t)NULL, NULL);

  cupsArrayAdd(ppd->coptions, copt);

 /*
  * Return the new record...
  */

  return (copt);
}


/*
 * 'ppd_get_cparam()' - Get a custom parameter record.
 */

static ppd_cparam_t *			/* O - Extended option... */
ppd_get_cparam(ppd_coption_t *opt,	/* I - PPD file */
               const char    *param,	/* I - Name of parameter */
	       const char    *text)	/* I - Human-readable text */
{
  ppd_cparam_t	*cparam;		/* New custom parameter */


 /*
  * See if the parameter already exists...
  */

  if ((cparam = ppdFindCustomParam(opt, param)) != NULL)
    return (cparam);

 /*
  * Not found, so create the custom parameter record...
  */

  if ((cparam = calloc(1, sizeof(ppd_cparam_t))) == NULL)
    return (NULL);

  cparam->type = PPD_CUSTOM_UNKNOWN;
  strlcpy(cparam->name, param, sizeof(cparam->name));
  strlcpy(cparam->text, text[0] ? text : param, sizeof(cparam->text));

 /*
  * Add this record to the array...
  */

  cupsArrayAdd(opt->params, cparam);

 /*
  * Return the new record...
  */

  return (cparam);
}


/*
 * 'ppd_get_group()' - Find or create the named group as needed.
 */

static ppd_group_t *			/* O - Named group */
ppd_get_group(ppd_file_t      *ppd,	/* I - PPD file */
              const char      *name,	/* I - Name of group */
	      const char      *text,	/* I - Text for group */
              _ppd_globals_t  *pg,	/* I - Global data */
	      cups_encoding_t encoding)	/* I - Encoding of text */
{
  int		i;			/* Looping var */
  ppd_group_t	*group;			/* Group */


  DEBUG_printf(("7ppd_get_group(ppd=%p, name=\"%s\", text=\"%s\", cg=%p)",
                ppd, name, text, pg));

  for (i = ppd->num_groups, group = ppd->groups; i > 0; i --, group ++)
    if (!strcmp(group->name, name))
      break;

  if (i == 0)
  {
    DEBUG_printf(("8ppd_get_group: Adding group %s...", name));

    if (pg->ppd_conform == PPD_CONFORM_STRICT && strlen(text) >= sizeof(group->text))
    {
      pg->ppd_status = PPD_ILLEGAL_TRANSLATION;

      return (NULL);
    }

    if (ppd->num_groups == 0)
      group = malloc(sizeof(ppd_group_t));
    else
      group = realloc(ppd->groups, (size_t)(ppd->num_groups + 1) * sizeof(ppd_group_t));

    if (group == NULL)
    {
      pg->ppd_status = PPD_ALLOC_ERROR;

      return (NULL);
    }

    ppd->groups = group;
    group += ppd->num_groups;
    ppd->num_groups ++;

    memset(group, 0, sizeof(ppd_group_t));
    strlcpy(group->name, name, sizeof(group->name));

    cupsCharsetToUTF8((cups_utf8_t *)group->text, text,
	               sizeof(group->text), encoding);
  }

  return (group);
}


/*
 * 'ppd_get_option()' - Find or create the named option as needed.
 */

static ppd_option_t *			/* O - Named option */
ppd_get_option(ppd_group_t *group,	/* I - Group */
               const char  *name)	/* I - Name of option */
{
  int		i;			/* Looping var */
  ppd_option_t	*option;		/* Option */


  DEBUG_printf(("7ppd_get_option(group=%p(\"%s\"), name=\"%s\")",
                group, group->name, name));

  for (i = group->num_options, option = group->options; i > 0; i --, option ++)
    if (!strcmp(option->keyword, name))
      break;

  if (i == 0)
  {
    if (group->num_options == 0)
      option = malloc(sizeof(ppd_option_t));
    else
      option = realloc(group->options, (size_t)(group->num_options + 1) * sizeof(ppd_option_t));

    if (option == NULL)
      return (NULL);

    group->options = option;
    option += group->num_options;
    group->num_options ++;

    memset(option, 0, sizeof(ppd_option_t));
    strlcpy(option->keyword, name, sizeof(option->keyword));
  }

  return (option);
}


/*
 * 'ppd_globals_alloc()' - Allocate and initialize global data.
 */

static _ppd_globals_t *		/* O - Pointer to global data */
ppd_globals_alloc(void)
{
  return ((_ppd_globals_t *)calloc(1, sizeof(_ppd_globals_t)));
}


/*
 * 'ppd_globals_free()' - Free global data.
 */

#if defined(HAVE_PTHREAD_H) || defined(_WIN32)
static void
ppd_globals_free(_ppd_globals_t *pg)	/* I - Pointer to global data */
{
  free(pg);
}
#endif /* HAVE_PTHREAD_H || _WIN32 */


#ifdef HAVE_PTHREAD_H
/*
 * 'ppd_globals_init()' - Initialize per-thread globals...
 */

static void
ppd_globals_init(void)
{
 /*
  * Register the global data for this thread...
  */

  pthread_key_create(&ppd_globals_key, (void (*)(void *))ppd_globals_free);
}
#endif /* HAVE_PTHREAD_H */


/*
 * 'ppd_hash_option()' - Generate a hash of the option name...
 */

static int				/* O - Hash index */
ppd_hash_option(ppd_option_t *option)	/* I - Option */
{
  int		hash = 0;		/* Hash index */
  const char	*k;			/* Pointer into keyword */


  for (hash = option->keyword[0], k = option->keyword + 1; *k;)
    hash = (int)(33U * (unsigned)hash) + *k++;

  return (hash & 511);
}


/*
 * 'ppd_read()' - Read a line from a PPD file, skipping comment lines as
 *                necessary.
 */

static int				/* O - Bitmask of fields read */
ppd_read(cups_file_t    *fp,		/* I - File to read from */
         _ppd_line_t    *line,		/* I - Line buffer */
         char           *keyword,	/* O - Keyword from line */
	 char           *option,	/* O - Option from line */
         char           *text,		/* O - Human-readable text from line */
	 char           **string,	/* O - Code/string data */
         int            ignoreblank,	/* I - Ignore blank lines? */
	 _ppd_globals_t *pg)		/* I - Global data */
{
  int		ch,			/* Character from file */
		col,			/* Column in line */
		colon,			/* Colon seen? */
		endquote,		/* Waiting for an end quote */
		mask,			/* Mask to be returned */
		startline,		/* Start line */
		textlen;		/* Length of text */
  char		*keyptr,		/* Keyword pointer */
		*optptr,		/* Option pointer */
		*textptr,		/* Text pointer */
		*strptr,		/* Pointer into string */
		*lineptr;		/* Current position in line buffer */


 /*
  * Now loop until we have a valid line...
  */

  *string   = NULL;
  col       = 0;
  startline = pg->ppd_line + 1;

  if (!line->buffer)
  {
    line->bufsize = 1024;
    line->buffer  = malloc(1024);

    if (!line->buffer)
      return (0);
  }

  do
  {
   /*
    * Read the line...
    */

    lineptr  = line->buffer;
    endquote = 0;
    colon    = 0;

    while ((ch = cupsFileGetChar(fp)) != EOF)
    {
      if (lineptr >= (line->buffer + line->bufsize - 1))
      {
       /*
        * Expand the line buffer...
	*/

        char *temp;			/* Temporary line pointer */


        line->bufsize += 1024;
	if (line->bufsize > 262144)
	{
	 /*
	  * Don't allow lines longer than 256k!
	  */

          pg->ppd_line   = startline;
          pg->ppd_status = PPD_LINE_TOO_LONG;

	  return (0);
	}

        temp = realloc(line->buffer, line->bufsize);
	if (!temp)
	{
          pg->ppd_line   = startline;
          pg->ppd_status = PPD_LINE_TOO_LONG;

	  return (0);
	}

        lineptr      = temp + (lineptr - line->buffer);
	line->buffer = temp;
      }

      if (ch == '\r' || ch == '\n')
      {
       /*
	* Line feed or carriage return...
	*/

        pg->ppd_line ++;
	col = 0;

	if (ch == '\r')
	{
	 /*
          * Check for a trailing line feed...
	  */

	  if ((ch = cupsFilePeekChar(fp)) == EOF)
	  {
	    ch = '\n';
	    break;
	  }

	  if (ch == 0x0a)
	    cupsFileGetChar(fp);
	}

	if (lineptr == line->buffer && ignoreblank)
          continue;			/* Skip blank lines */

	ch = '\n';

	if (!endquote)			/* Continue for multi-line text */
          break;

	*lineptr++ = '\n';
      }
      else if (ch < ' ' && ch != '\t' && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
       /*
        * Other control characters...
	*/

        pg->ppd_line   = startline;
        pg->ppd_status = PPD_ILLEGAL_CHARACTER;

        return (0);
      }
      else if (ch != 0x1a)
      {
       /*
	* Any other character...
	*/

	*lineptr++ = (char)ch;
	col ++;

	if (col > (PPD_MAX_LINE - 1))
	{
	 /*
          * Line is too long...
	  */

          pg->ppd_line   = startline;
          pg->ppd_status = PPD_LINE_TOO_LONG;

          return (0);
	}

	if (ch == ':' && strncmp(line->buffer, "*%", 2) != 0)
	  colon = 1;

	if (ch == '\"' && colon)
	  endquote = !endquote;
      }
    }

    if (endquote)
    {
     /*
      * Didn't finish this quoted string...
      */

      while ((ch = cupsFileGetChar(fp)) != EOF)
        if (ch == '\"')
	  break;
	else if (ch == '\r' || ch == '\n')
	{
	  pg->ppd_line ++;
	  col = 0;

	  if (ch == '\r')
	  {
	   /*
            * Check for a trailing line feed...
	    */

	    if ((ch = cupsFilePeekChar(fp)) == EOF)
	      break;
	    if (ch == 0x0a)
	      cupsFileGetChar(fp);
	  }
	}
	else if (ch < ' ' && ch != '\t' && pg->ppd_conform == PPD_CONFORM_STRICT)
	{
	 /*
          * Other control characters...
	  */

          pg->ppd_line   = startline;
          pg->ppd_status = PPD_ILLEGAL_CHARACTER;

          return (0);
	}
	else if (ch != 0x1a)
	{
	  col ++;

	  if (col > (PPD_MAX_LINE - 1))
	  {
	   /*
            * Line is too long...
	    */

            pg->ppd_line   = startline;
            pg->ppd_status = PPD_LINE_TOO_LONG;

            return (0);
	  }
	}
    }

    if (ch != '\n')
    {
     /*
      * Didn't finish this line...
      */

      while ((ch = cupsFileGetChar(fp)) != EOF)
	if (ch == '\r' || ch == '\n')
	{
	 /*
	  * Line feed or carriage return...
	  */

          pg->ppd_line ++;
	  col = 0;

	  if (ch == '\r')
	  {
	   /*
            * Check for a trailing line feed...
	    */

	    if ((ch = cupsFilePeekChar(fp)) == EOF)
	      break;
	    if (ch == 0x0a)
	      cupsFileGetChar(fp);
	  }

	  break;
	}
	else if (ch < ' ' && ch != '\t' && pg->ppd_conform == PPD_CONFORM_STRICT)
	{
	 /*
          * Other control characters...
	  */

          pg->ppd_line   = startline;
          pg->ppd_status = PPD_ILLEGAL_CHARACTER;

          return (0);
	}
	else if (ch != 0x1a)
	{
	  col ++;

	  if (col > (PPD_MAX_LINE - 1))
	  {
	   /*
            * Line is too long...
	    */

            pg->ppd_line   = startline;
            pg->ppd_status = PPD_LINE_TOO_LONG;

            return (0);
	  }
	}
    }

    if (lineptr > line->buffer && lineptr[-1] == '\n')
      lineptr --;

    *lineptr = '\0';

    DEBUG_printf(("9ppd_read: LINE=\"%s\"", line->buffer));

   /*
    * The dynamically created PPDs for older style macOS
    * drivers include a large blob of data inserted as comments
    * at the end of the file.  As an optimization we can stop
    * reading the PPD when we get to the start of this data.
    */

    if (!strcmp(line->buffer, "*%APLWORKSET START"))
      return (0);

    if (ch == EOF && lineptr == line->buffer)
      return (0);

   /*
    * Now parse it...
    */

    mask    = 0;
    lineptr = line->buffer + 1;

    keyword[0] = '\0';
    option[0]  = '\0';
    text[0]    = '\0';
    *string    = NULL;

    if ((!line->buffer[0] ||		/* Blank line */
         !strncmp(line->buffer, "*%", 2) || /* Comment line */
         !strcmp(line->buffer, "*End")) && /* End of multi-line string */
        ignoreblank)			/* Ignore these? */
    {
      startline = pg->ppd_line + 1;
      continue;
    }

    if (!strcmp(line->buffer, "*"))	/* (Bad) comment line */
    {
      if (pg->ppd_conform == PPD_CONFORM_RELAXED)
      {
	startline = pg->ppd_line + 1;
	continue;
      }
      else
      {
        pg->ppd_line   = startline;
        pg->ppd_status = PPD_ILLEGAL_MAIN_KEYWORD;

        return (0);
      }
    }

    if (line->buffer[0] != '*')		/* All lines start with an asterisk */
    {
     /*
      * Allow lines consisting of just whitespace...
      */

      for (lineptr = line->buffer; *lineptr; lineptr ++)
        if (*lineptr && !_cups_isspace(*lineptr))
	  break;

      if (*lineptr)
      {
        pg->ppd_status = PPD_MISSING_ASTERISK;
        return (0);
      }
      else if (ignoreblank)
        continue;
      else
        return (0);
    }

   /*
    * Get a keyword...
    */

    keyptr = keyword;

    while (*lineptr && *lineptr != ':' && !_cups_isspace(*lineptr))
    {
      if (*lineptr <= ' ' || *lineptr > 126 || *lineptr == '/' ||
          (keyptr - keyword) >= (PPD_MAX_NAME - 1))
      {
        pg->ppd_status = PPD_ILLEGAL_MAIN_KEYWORD;
	return (0);
      }

      *keyptr++ = *lineptr++;
    }

    *keyptr = '\0';

    if (!strcmp(keyword, "End"))
      continue;

    mask |= PPD_KEYWORD;

    if (_cups_isspace(*lineptr))
    {
     /*
      * Get an option name...
      */

      while (_cups_isspace(*lineptr))
        lineptr ++;

      optptr = option;

      while (*lineptr && !_cups_isspace(*lineptr) && *lineptr != ':' &&
             *lineptr != '/')
      {
	if (*lineptr <= ' ' || *lineptr > 126 ||
	    (optptr - option) >= (PPD_MAX_NAME - 1))
        {
          pg->ppd_status = PPD_ILLEGAL_OPTION_KEYWORD;
	  return (0);
	}

        *optptr++ = *lineptr++;
      }

      *optptr = '\0';

      if (_cups_isspace(*lineptr) && pg->ppd_conform == PPD_CONFORM_STRICT)
      {
        pg->ppd_status = PPD_ILLEGAL_WHITESPACE;
	return (0);
      }

      while (_cups_isspace(*lineptr))
	lineptr ++;

      mask |= PPD_OPTION;

      if (*lineptr == '/')
      {
       /*
        * Get human-readable text...
	*/

        lineptr ++;

	textptr = text;

	while (*lineptr != '\0' && *lineptr != '\n' && *lineptr != ':')
	{
	  if (((unsigned char)*lineptr < ' ' && *lineptr != '\t') ||
	      (textptr - text) >= (PPD_MAX_LINE - 1))
	  {
	    pg->ppd_status = PPD_ILLEGAL_TRANSLATION;
	    return (0);
	  }

	  *textptr++ = *lineptr++;
        }

	*textptr = '\0';
	textlen  = ppd_decode(text);

	if (textlen > PPD_MAX_TEXT && pg->ppd_conform == PPD_CONFORM_STRICT)
	{
	  pg->ppd_status = PPD_ILLEGAL_TRANSLATION;
	  return (0);
	}

	mask |= PPD_TEXT;
      }
    }

    if (_cups_isspace(*lineptr) && pg->ppd_conform == PPD_CONFORM_STRICT)
    {
      pg->ppd_status = PPD_ILLEGAL_WHITESPACE;
      return (0);
    }

    while (_cups_isspace(*lineptr))
      lineptr ++;

    if (*lineptr == ':')
    {
     /*
      * Get string after triming leading and trailing whitespace...
      */

      lineptr ++;
      while (_cups_isspace(*lineptr))
        lineptr ++;

      strptr = lineptr + strlen(lineptr) - 1;
      while (strptr >= lineptr && _cups_isspace(*strptr))
        *strptr-- = '\0';

      if (*strptr == '\"')
      {
       /*
        * Quoted string by itself, remove quotes...
	*/

        *strptr = '\0';
	lineptr ++;
      }

      *string = strdup(lineptr);

      mask |= PPD_STRING;
    }
  }
  while (mask == 0);

  return (mask);
}


/*
 * 'ppd_update_filters()' - Update the filters array as needed.
 *
 * This function re-populates the filters array with cupsFilter2 entries that
 * have been stripped of the destination MIME media types and any maxsize hints.
 *
 * (All for backwards-compatibility)
 */

static int				/* O - 1 on success, 0 on failure */
ppd_update_filters(ppd_file_t     *ppd,	/* I - PPD file */
                   _ppd_globals_t *pg)	/* I - Global data */
{
  ppd_attr_t	*attr;			/* Current cupsFilter2 value */
  char		srcsuper[16],		/* Source MIME media type */
		srctype[256],
		dstsuper[16],		/* Destination MIME media type */
		dsttype[256],
		*ptr,			/* Pointer into command to run */
		buffer[1024],		/* Re-written cupsFilter value */
		**filter;		/* Current filter */
  int		cost;			/* Cost of filter */

  char		program[1024] = { 0 };	/* Command to run */

  DEBUG_printf(("4ppd_update_filters(ppd=%p, cg=%p)", ppd, pg));

 /*
  * See if we have any cupsFilter2 lines...
  */

  if ((attr = ppdFindAttr(ppd, "cupsFilter2", NULL)) == NULL)
  {
    DEBUG_puts("5ppd_update_filters: No cupsFilter2 keywords present.");
    return (1);
  }

 /*
  * Yes, free the cupsFilter-defined filters and re-build...
  */

  ppd_free_filters(ppd);

  do
  {
   /*
    * Parse the cupsFilter2 string:
    *
    *   src/type dst/type cost program
    *   src/type dst/type cost maxsize(n) program
    */

    DEBUG_printf(("5ppd_update_filters: cupsFilter2=\"%s\"", attr->value));

    if (sscanf(attr->value, "%15[^/]/%255s%*[ \t]%15[^/]/%255s%d%*[ \t]%1023[^\n]",
	       srcsuper, srctype, dstsuper, dsttype, &cost, program) != 6)
    {
      DEBUG_puts("5ppd_update_filters: Bad cupsFilter2 line.");
      pg->ppd_status = PPD_BAD_VALUE;

      return (0);
    }

    DEBUG_printf(("5ppd_update_filters: srcsuper=\"%s\", srctype=\"%s\", "
                  "dstsuper=\"%s\", dsttype=\"%s\", cost=%d, program=\"%s\"",
		  srcsuper, srctype, dstsuper, dsttype, cost, program));

    if (!strncmp(program, "maxsize(", 8) &&
        (ptr = strchr(program + 8, ')')) != NULL)
    {
      DEBUG_puts("5ppd_update_filters: Found maxsize(nnn).");

      ptr ++;
      while (_cups_isspace(*ptr))
	ptr ++;

      _cups_strcpy(program, ptr);
      DEBUG_printf(("5ppd_update_filters: New program=\"%s\"", program));
    }

   /*
    * Convert to cupsFilter format:
    *
    *   src/type cost program
    */

    snprintf(buffer, sizeof(buffer), "%s/%s %d %s", srcsuper, srctype, cost,
             program);
    DEBUG_printf(("5ppd_update_filters: Adding \"%s\".", buffer));

   /*
    * Add a cupsFilter-compatible string to the filters array.
    */

    if (ppd->num_filters == 0)
      filter = malloc(sizeof(char *));
    else
      filter = realloc(ppd->filters, sizeof(char *) * (size_t)(ppd->num_filters + 1));

    if (filter == NULL)
    {
      DEBUG_puts("5ppd_update_filters: Out of memory.");
      pg->ppd_status = PPD_ALLOC_ERROR;

      return (0);
    }

    ppd->filters     = filter;
    filter           += ppd->num_filters;
    ppd->num_filters ++;

    *filter = strdup(buffer);
  }
  while ((attr = ppdFindNextAttr(ppd, "cupsFilter2", NULL)) != NULL);

  DEBUG_puts("5ppd_update_filters: Completed OK.");
  return (1);
}
