//
// IPP Everywhere printer application for CUPS.
//
// Copyright © 2021-2026 by OpenPrinting.
// Copyright © 2020 by the IEEE-ISTO Printer Working Group.
// Copyright © 2010-2021 by Apple Inc.
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//
// Note: This program began life as the "ippserver" sample code that first
// appeared in CUPS 1.4.  The name has been changed in order to distinguish it
// from the PWG's much more ambitious "ippserver" program, which supports
// different kinds of IPP services and multiple services per instance - the
// "ippeveprinter" program exposes a single print service conforming to the
// current IPP Everywhere specification, thus the new name.
//

#include <cups/cups-private.h>
#include <cups/form.h>
#include <cups/dnssd.h>

#include <limits.h>
#include <sys/stat.h>

#ifdef _WIN32
#  include <fcntl.h>
#  include <io.h>
#  include <process.h>
#  define WEXITSTATUS(s) (s)
#  include <winsock2.h>
typedef ULONG nfds_t;
#  define poll WSAPoll
#else
extern char **environ;

#  include <spawn.h>
#  include <sys/fcntl.h>
#  include <sys/wait.h>
#  include <poll.h>
#endif // _WIN32

#ifndef O_BINARY
#  define O_BINARY 0			// Windows "binary file" nonsense
#endif // !O_BINARY

#ifdef HAVE_SYS_MOUNT_H
#  include <sys/mount.h>
#endif // HAVE_SYS_MOUNT_H
#ifdef HAVE_SYS_STATFS_H
#  include <sys/statfs.h>
#endif // HAVE_SYS_STATFS_H
#ifdef HAVE_SYS_STATVFS_H
#  include <sys/statvfs.h>
#endif // HAVE_SYS_STATVFS_H
#ifdef HAVE_SYS_VFS_H
#  include <sys/vfs.h>
#endif // HAVE_SYS_VFS_H

#if HAVE_LIBPAM
#  ifdef HAVE_PAM_PAM_APPL_H
#    include <pam/pam_appl.h>
#  else
#    include <security/pam_appl.h>
#  endif // HAVE_PAM_PAM_APPL_H
#endif // HAVE_LIBPAM

#include "printer-png.h"
#include "printer-lg-png.h"
#include "printer-sm-png.h"


//
// Constants...
//

enum ippeve_preason_e			// printer-state-reasons bit values
{
  IPPEVE_PREASON_NONE = 0x0000,		// none
  IPPEVE_PREASON_OTHER = 0x0001,	// other
  IPPEVE_PREASON_COVER_OPEN = 0x0002,	// cover-open
  IPPEVE_PREASON_INPUT_TRAY_MISSING = 0x0004,
					// input-tray-missing
  IPPEVE_PREASON_MARKER_SUPPLY_EMPTY = 0x0008,
					// marker-supply-empty
  IPPEVE_PREASON_MARKER_SUPPLY_LOW = 0x0010,
					// marker-supply-low
  IPPEVE_PREASON_MARKER_WASTE_ALMOST_FULL = 0x0020,
					// marker-waste-almost-full
  IPPEVE_PREASON_MARKER_WASTE_FULL = 0x0040,
					// marker-waste-full
  IPPEVE_PREASON_MEDIA_EMPTY = 0x0080,	// media-empty
  IPPEVE_PREASON_MEDIA_JAM = 0x0100,	// media-jam
  IPPEVE_PREASON_MEDIA_LOW = 0x0200,	// media-low
  IPPEVE_PREASON_MEDIA_NEEDED = 0x0400,	// media-needed
  IPPEVE_PREASON_MOVING_TO_PAUSED = 0x0800,
					// moving-to-paused
  IPPEVE_PREASON_PAUSED = 0x1000,	// paused
  IPPEVE_PREASON_SPOOL_AREA_FULL = 0x2000,// spool-area-full
  IPPEVE_PREASON_TONER_EMPTY = 0x4000,	// toner-empty
  IPPEVE_PREASON_TONER_LOW = 0x8000	// toner-low
};
typedef unsigned int ippeve_preason_t;	// Bitfield for printer-state-reasons
static const char * const ippeve_preason_strings[] =
{					// Strings for each bit
  // "none" is implied for no bits set
  "other",
  "cover-open",
  "input-tray-missing",
  "marker-supply-empty",
  "marker-supply-low",
  "marker-waste-almost-full",
  "marker-waste-full",
  "media-empty",
  "media-jam",
  "media-low",
  "media-needed",
  "moving-to-paused",
  "paused",
  "spool-area-full",
  "toner-empty",
  "toner-low"
};


//
// URL scheme for web resources...
//

#define WEB_SCHEME "https"


//
// Structures...
//

#if HAVE_LIBPAM
typedef struct ippeve_authdata_s	// Authentication data
{
  char	username[256],	// Username string
	*password;			// Password string
} ippeve_authdata_t;
#endif // HAVE_LIBPAM

typedef struct ippeve_filter_s		// Attribute filter
{
  cups_array_t		*ra;		// Requested attributes
  ipp_tag_t		group_tag;	// Group to copy
} ippeve_filter_t;

typedef struct ippeve_job_s ippeve_job_t;

typedef struct ippeve_printer_s		// Printer data
{
  // Note: A "real" IPP implementation will support more than one IPv4 and one IPv6 listener
  int			ipv4,		// IPv4 listener
			ipv6;		// IPv6 listener
  cups_dnssd_t		*dnssd;		// DNS-SD context
  cups_dnssd_service_t	*services;	// DNS-SD services
  char			*dnssd_subtypes;// DNS-SD subtypes
  int			dnssd_collision;// Name collision?
  char			*dnssd_name,	// printer-dns-sd-name
			*name,		// printer-name
			*icons[3],	// Icon filenames
			*strings,	// Strings filename
			*directory,	// Spool directory
			*hostname,	// Hostname
			*device_uri,	// Device URI (if any)
			*output_format,	// Output format
			*command;	// Command to run with job file
  int			port;		// Port
  bool			web_forms;	// Enable web interface forms?
  size_t		urilen;		// Length of printer URI
  ipp_t			*attrs;		// Static attributes
  time_t		start_time;	// Startup time
  time_t		config_time;	// printer-config-change-time
  ipp_pstate_t		state;		// printer-state value
  ippeve_preason_t	state_reasons;	// printer-state-reasons values
  char			state_keywords[8][128];
					// Extra printer-state-reasons keywords
  time_t		state_time;	// printer-state-change-time
  cups_array_t		*jobs;		// Jobs
  ippeve_job_t		*active_job;	// Current active/pending job
  int			next_job_id;	// Next job-id value
  cups_rwlock_t		rwlock;		// Printer lock
} ippeve_printer_t;

struct ippeve_job_s			// Job data
{
  int			id;		// Job ID
  const char		*name,		// job-name
			*username,	// job-originating-user-name
			*format;	// document-format
  ipp_jstate_t		state;		// job-state value
  char			*message;	// job-state-message value
  int			msglevel;	// job-state-message log level (0=error, 1=info)
  time_t		created,	// time-at-creation value
			processing,	// time-at-processing value
			completed;	// time-at-completed value
  int			impressions,	// job-impressions value
			impcompleted;	// job-impressions-completed value
  ipp_t			*attrs;		// Static attributes
  char			password[256];	// Document password, if any
  int			cancel;		// Non-zero when job canceled
  char			*filename;	// Print file name
  int			fd;		// Print file descriptor
  ippeve_printer_t	*printer;	// Printer
};

typedef struct ippeve_client_s		// Client data
{
  http_t		*http;		// HTTP connection
  ipp_t			*request,	// IPP request
			*response;	// IPP response
  time_t		start;		// Request start time
  http_state_t		operation;	// Request operation
  ipp_op_t		operation_id;	// IPP operation-id
  char			uri[1024],	// Request URI
			*options,	// URI options
			host_field[256];// Host: header
  int			host_port;	// Port number from Host: header
  http_addr_t		addr;		// Client address
  char			hostname[256],	// Client hostname
			username[256];	// Authenticated username, if any
  ippeve_printer_t	*printer;	// Printer
  ippeve_job_t		*job;		// Current job, if any
} ippeve_client_t;


//
// Local functions...
//

static http_status_t	authenticate_request(ippeve_client_t *client);
static void		clean_jobs(ippeve_printer_t *printer);
static int		compare_jobs(ippeve_job_t *a, ippeve_job_t *b, void *data);
static void		copy_attributes(ipp_t *to, ipp_t *from, cups_array_t *ra, ipp_tag_t group_tag, bool quickcopy);
static void		copy_job_attributes(ippeve_client_t *client, ippeve_job_t *job, cups_array_t *ra);
static ippeve_client_t	*create_client(ippeve_printer_t *printer, int sock);
static ippeve_job_t	*create_job(ippeve_client_t *client);
static int		create_job_file(ippeve_job_t *job, char *fname, size_t fnamesize, const char *dir, const char *ext);
static int		create_listener(const char *name, int port, int family);
static ipp_t		*create_media_col(const char *media, const char *source, const char *type, ipp_t *media_size, int bottom, int left, int right, int top);
static ipp_t		*create_media_size(int width, int length);
static ipp_t		*create_media_size_range(int min_width, int max_width, int min_length, int max_length);
static ippeve_printer_t	*create_printer(const char *servername, int serverport, const char *name, const char *location, const char *icons, const char *strings, cups_array_t *docformats, const char *subtypes, const char *directory, const char *command, const char *device_uri, const char *output_format, ipp_t *attrs);
static void		debug_attributes(const char *title, ipp_t *ipp, int response);
static void		delete_client(ippeve_client_t *client);
static void		delete_job(ippeve_job_t *job);
static void		delete_printer(ippeve_printer_t *printer);
static void		dnssd_callback(cups_dnssd_service_t *service, ippeve_printer_t *printer, cups_dnssd_flags_t flags);
static int		filter_cb(ippeve_filter_t *filter, ipp_t *dst, ipp_attribute_t *attr);
static ippeve_job_t	*find_job(ippeve_client_t *client);
static void		finish_document_data(ippeve_client_t *client, ippeve_job_t *job);
static void		finish_document_uri(ippeve_client_t *client, ippeve_job_t *job);
static void		flush_document_data(ippeve_client_t *client);
static bool		have_document_data(ippeve_client_t *client);
static bool		html_escape(ippeve_client_t *client, const char *s, size_t slen);
static bool		html_footer(ippeve_client_t *client);
static bool		html_header(ippeve_client_t *client, const char *title, int refresh);
static bool		html_printf(ippeve_client_t *client, const char *format, ...) _CUPS_FORMAT(2, 3);
static void		ipp_cancel_job(ippeve_client_t *client);
static void		ipp_cancel_jobs(ippeve_client_t *client);
static void		ipp_close_job(ippeve_client_t *client);
static void		ipp_create_job(ippeve_client_t *client);
static void		ipp_get_job_attributes(ippeve_client_t *client);
static void		ipp_get_jobs(ippeve_client_t *client);
static void		ipp_get_printer_attributes(ippeve_client_t *client);
static void		ipp_identify_printer(ippeve_client_t *client);
static void		ipp_print_job(ippeve_client_t *client);
static void		ipp_print_uri(ippeve_client_t *client);
static void		ipp_send_document(ippeve_client_t *client);
static void		ipp_send_uri(ippeve_client_t *client);
static void		ipp_validate_job(ippeve_client_t *client);
static ipp_t		*load_ippfile_attributes(const char *servername, int serverport, const char *filename, cups_array_t *docformats);
static ipp_t		*load_legacy_attributes(const char *make, const char *model, int ppm, int ppm_color, int duplex, cups_array_t *docformats);
#if HAVE_LIBPAM
static int		pam_func(int, const struct pam_message **, struct pam_response **, void *);
#endif // HAVE_LIBPAM
static size_t		parse_options(ippeve_client_t *client, cups_option_t **options);
static void		process_attr_message(ippeve_job_t *job, char *message);
static void		*process_client(ippeve_client_t *client);
static int		process_http(ippeve_client_t *client);
static int		process_ipp(ippeve_client_t *client);
static void		*process_job(ippeve_job_t *job);
static void		process_state_message(ippeve_printer_t *printer, const char *message);
static bool		register_printer(ippeve_printer_t *printer);
static bool		respond_http(ippeve_client_t *client, http_status_t code, const char *content_coding, const char *type, size_t length);
static void		respond_ignored(ippeve_client_t *client, ipp_attribute_t *attr);
static void		respond_ipp(ippeve_client_t *client, ipp_status_t status, const char *message, ...) _CUPS_FORMAT(3, 4);
static void		respond_unsupported(ippeve_client_t *client, ipp_attribute_t *attr);
static void		run_printer(ippeve_printer_t *printer);
static int		show_media(ippeve_client_t *client);
static int		show_status(ippeve_client_t *client);
static int		show_supplies(ippeve_client_t *client);
#ifndef _WIN32
static void		signal_handler(int signum);
#endif // !_WIN32
static char		*time_string(time_t tv, char *buffer, size_t bufsize);
static int		usage(FILE *out);
static bool		valid_doc_attributes(ippeve_client_t *client);
static bool		valid_job_attributes(ippeve_client_t *client);


//
// Globals...
//

static bool		KeepFiles = false;
					// Keep spooled job files?
static int		MaxVersion = 20,// Maximum IPP version (20 = 2.0, 11 = 1.1, etc.)
			Verbosity = 0;	// Verbosity level
static const char	*PAMService = NULL;
					// PAM service
#ifndef _WIN32
static bool		StopPrinter = false;
					// Stop the printer server?
#endif // !_WIN32


//
// 'main()' - Main entry to the sample server.
//

int					// O - Exit status
main(int  argc,				// I - Number of command-line args
     char *argv[])			// I - Command-line arguments
{
  int		i;			// Looping var
  const char	*opt,			// Current option character
		*attrfile = NULL,	// ippserver attributes file
		*command = NULL,	// Command to run with job files
		*device_uri = NULL,	// Device URI
		*output_format = NULL,	// Output format
		*icon = NULL,		// Icon file
		*keypath = NULL,	// Keychain path
		*location = "",		// Location of printer
		*logfile = NULL,	// Log file
		*make = "Example",	// Manufacturer
		*model = "Printer",	// Model
		*name = NULL,		// Printer name
		*strings = NULL,	// Strings file
		*subtypes = "_print";	// DNS-SD service subtype
  bool		legacy = false,		// Legacy mode?
		duplex = false,		// Duplex mode
		web_forms = true;	// Enable web site forms?
  int		ppm = 10,		// Pages per minute for mono
		ppm_color = 0;		// Pages per minute for color
  ipp_t		*attrs = NULL;		// Printer attributes
  char		directory[1024] = "";	// Spool directory
  cups_array_t	*docformats = NULL;	// Supported formats
  const char	*servername = NULL;	// Server host name
  int		serverport = 0;		// Server port number (0 = auto)
  ippeve_printer_t *printer;		// Printer object


  cupsLangSetLocale(argv);

  // Parse command-line arguments...
  for (i = 1; i < argc; i ++)
  {
    if (!strcmp(argv[i], "--help"))
    {
      return (usage(stdout));
    }
    else if (!strcmp(argv[i], "--no-web-forms"))
    {
      web_forms = false;
    }
    else if (!strcmp(argv[i], "--pam-service"))
    {
      i ++;
      if (i >= argc)
      {
        cupsLangPrintf(stderr, _("%s: Missing service name after '--pam-service'."), "ippeveprinter");
        return (usage(stderr));
      }

      PAMService = argv[i];
    }
    else if (!strcmp(argv[i], "--version"))
    {
      puts(LIBCUPS_VERSION);
      return (0);
    }
    else if (!strncmp(argv[i], "--", 2))
    {
      cupsLangPrintf(stderr, _("%s: Unknown option '%s'."), "ippeveprinter", argv[i]);
      return (usage(stderr));
    }
    else if (argv[i][0] == '-')
    {
      for (opt = argv[i] + 1; *opt; opt ++)
      {
        switch (*opt)
	{
	  case '2' : // -2 (enable 2-sided printing)
	      duplex = true;
	      legacy = true;
	      break;

	  case 'a' : // -a attributes-file
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing IPP file after '-a'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      attrfile = argv[i];
	      break;

          case 'A' : // -A (enable authentication)
              if (!PAMService)
                PAMService = "cups";
	      break;

          case 'c' : // -c command
              i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing command after '-c'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      command = argv[i];
	      break;

	  case 'd' : // -d spool-directory
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing spool directory after '-d'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      cupsCopyString(directory, argv[i], sizeof(directory));
	      break;

          case 'D' : // -D device-uri
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing device URI after '%s'."), "ippeveprinter", "-D");
	        return (usage(stderr));
	      }

	      device_uri = argv[i];
	      break;

	  case 'f' : // -f type/subtype[,...]
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing format list after '-f'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      docformats = cupsArrayNewStrings(argv[i], ',');
	      break;

          case 'F' : // -F output/format
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing output format after '-F'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      output_format = argv[i];
	      break;

	  case 'i' : // -i icon.png
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing icon file(s) after '-i'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      icon = argv[i];
	      break;

	  case 'k' : // -k (keep files)
	      KeepFiles = true;
	      break;

	  case 'K' : // -K keypath
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing keystore path after '-K'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      keypath = argv[i];
	      break;

	  case 'l' : // -l LOCATION
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing location after '-l'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      location = argv[i];
	      break;

	  case 'L' : // -L LOGFILE
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing filename after '-L'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      logfile = argv[i];
	      break;

	  case 'm' : // -m model
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing model name after '-m'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      model  = argv[i];
	      legacy = true;
	      break;

	  case 'M' : // -M manufacturer
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing manufacturer after '-M'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      make   = argv[i];
	      legacy = true;
	      break;

	  case 'n' : // -n hostname
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing server name after '-n'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      servername = argv[i];
	      break;

	  case 'p' : // -p port
	      i ++;
	      if (i >= argc || !isdigit(argv[i][0] & 255))
	      {
	        cupsLangPrintf(stderr, _("%s: Missing port number after '-p'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      serverport = atoi(argv[i]);
	      break;

	  case 'r' : // -r subtype
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing subtype(s) after '-r'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      subtypes = argv[i];
	      break;

	  case 's' : // -s speed[,color-speed]
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing speed(s) after '-s'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      if (sscanf(argv[i], "%d,%d", &ppm, &ppm_color) < 1)
	      {
	        cupsLangPrintf(stderr, _("%s: Invalid speed(s) value \"%s\"."), "ippeveprinter", argv[i]);
	        return (usage(stderr));
	      }

	      legacy = true;
	      break;

	  case 'S' : // -S filename.strings
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing strings filename after '-S'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      strings = argv[i];
	      break;

	  case 'v' : // -v (be verbose)
	      Verbosity ++;
	      break;

          case 'V' : // -V max-version
	      i ++;
	      if (i >= argc)
	      {
	        cupsLangPrintf(stderr, _("%s: Missing version after '-V'."), "ippeveprinter");
	        return (usage(stderr));
	      }

	      if (!strcmp(argv[i], "2.0"))
	      {
                MaxVersion = 20;
	      }
	      else if (!strcmp(argv[i], "1.1"))
              {
                MaxVersion = 11;
	      }
	      else
	      {
	        cupsLangPrintf(stderr, _("%s: Unsupported version \"%s\"."), "ippeveprinter", argv[i]);
	        return (usage(stderr));
	      }
              break;

          default : // Unknown
	      cupsLangPrintf(stderr, _("%s: Unknown option '-%c'."), "ippeveprinter", *opt);
	      return (usage(stderr));
	}
      }
    }
    else if (!name)
    {
      name = argv[i];
    }
    else
    {
      cupsLangPrintf(stderr, _("%s: Unknown option '%s'."), "ippeveprinter", argv[i]);
      return (usage(stderr));
    }
  }

  if (!name)
  {
    cupsLangPrintf(stderr, _("%s: Missing printer name."), "ippeveprinter");
    return (usage(stderr));
  }

  if (attrfile != NULL && legacy)
  {
    cupsLangPrintf(stderr, _("%s: Cannot use '-a' with '-2', '-f', '-m', '-M', or '-s'."), "ippeveprinter");
    return (usage(stderr));
  }

  // Apply defaults as needed...
  if (!directory[0])
  {
    const char *tmpdir;			// Temporary directory

#ifdef _WIN32
    if ((tmpdir = getenv("TEMP")) == NULL)
      tmpdir = "C:/TEMP";
#elif defined(__APPLE__) && TARGET_OS_OSX
    if ((tmpdir = getenv("TMPDIR")) == NULL)
      tmpdir = "/private/tmp";
#else
    if ((tmpdir = getenv("TMPDIR")) == NULL)
      tmpdir = "/tmp";
#endif // _WIN32

    snprintf(directory, sizeof(directory), "%s/ippeveprinter.%d", tmpdir, (int)getpid());

    if (mkdir(directory, 0755) && errno != EEXIST)
    {
      cupsLangPrintf(stderr, _("%s: Unable to create spool directory '%s': %s"), "ippeveprinter", directory, strerror(errno));
      return (usage(stderr));
    }

    if (Verbosity)
      cupsLangPrintf(stderr, _("Using spool directory '%s'."), directory);
  }

  // Create the printer...
  if (attrfile)
    attrs = load_ippfile_attributes(servername, serverport, attrfile, docformats);
  else
    attrs = load_legacy_attributes(make, model, ppm, ppm_color, duplex, docformats);

  if (!docformats && !ippFindAttribute(attrs, "document-format-supported", IPP_TAG_MIMETYPE))
    docformats = cupsArrayNewStrings(ppm_color > 0 ? "image/jpeg,image/pwg-raster,image/urf": "image/pwg-raster,image/urf", ',');

  if ((printer = create_printer(servername, serverport, name, location, icon, strings, docformats, subtypes, directory, command, device_uri, output_format, attrs)) == NULL)
    return (1);

  printer->web_forms = web_forms;

  cupsSetServerCredentials(keypath, printer->hostname, true);

  // If a log file is set, redirect stderr to the file now...
  if (logfile)
  {
    FILE *fp;				// New stderr, if any

    if (*logfile == '+')
      fp = freopen(logfile + 1, "a", stderr);
    else
      fp = freopen(logfile, "w", stderr);

    if (!fp)
    {
      perror(logfile);
      return (1);
    }

    setbuf(fp, NULL);
  }

  // Run the print service...
  run_printer(printer);

  // Destroy the printer and exit...
  delete_printer(printer);

  return (0);
}


//
// 'authenticate_request()' - Try to authenticate the request.
//

static http_status_t			// O - HTTP_STATUS_CONTINUE to keep going, otherwise status to return
authenticate_request(
    ippeve_client_t *client)		// I - Client
{
#if HAVE_LIBPAM
  // If PAM isn't enabled, return 'continue' now...
  const char		*authorization;	// Pointer into Authorization string
  int			userlen;	// Username:password length
  pam_handle_t		*pamh;		// PAM authentication handle
  int			pamerr;		// PAM error code
  struct pam_conv	pamdata;	// PAM conversation data
  ippeve_authdata_t	data;		// Authentication data


  if (!PAMService)
    return (HTTP_STATUS_CONTINUE);

  // Try authenticating using PAM...
  authorization = httpGetField(client->http, HTTP_FIELD_AUTHORIZATION);

  if (!*authorization)
    return (HTTP_STATUS_UNAUTHORIZED);

  if (strncmp(authorization, "Basic ", 6))
  {
    fputs("Unsupported scheme in Authorization header.\n", stderr);
    return (HTTP_STATUS_BAD_REQUEST);
  }

  authorization += 5;
  while (isspace(*authorization & 255))
    authorization ++;

  userlen = sizeof(data.username);
  httpDecode64_2(data.username, &userlen, authorization);

  if ((data.password = strchr(data.username, ':')) == NULL)
  {
    fputs("No password in Authorization header.\n", stderr);
    return (HTTP_STATUS_BAD_REQUEST);
  }

  *(data.password)++ = '\0';

  if (!data.username[0])
  {
    fputs("No username in Authorization header.\n", stderr);
    return (HTTP_STATUS_BAD_REQUEST);
  }

  pamdata.conv        = pam_func;
  pamdata.appdata_ptr = &data;

  if ((pamerr = pam_start(PAMService, data.username, &pamdata, &pamh)) != PAM_SUCCESS)
  {
    fprintf(stderr, "pam_start() returned %d (%s)\n", pamerr, pam_strerror(pamh, pamerr));
    return (HTTP_STATUS_SERVER_ERROR);
  }

  if ((pamerr = pam_authenticate(pamh, PAM_SILENT)) != PAM_SUCCESS)
  {
    fprintf(stderr, "pam_authenticate() returned %d (%s)\n", pamerr, pam_strerror(pamh, pamerr));
    pam_end(pamh, 0);
    return (HTTP_STATUS_UNAUTHORIZED);
  }

  if ((pamerr = pam_acct_mgmt(pamh, PAM_SILENT)) != PAM_SUCCESS)
  {
    fprintf(stderr, "pam_acct_mgmt() returned %d (%s)\n", pamerr, pam_strerror(pamh, pamerr));
    pam_end(pamh, 0);
    return (HTTP_STATUS_SERVER_ERROR);
  }

  cupsCopyString(client->username, data.username, sizeof(client->username));

  pam_end(pamh, PAM_SUCCESS);

  return (HTTP_STATUS_CONTINUE);

#else
  // No authentication support built-in, return 'continue'...
  (void)client;

  return (HTTP_STATUS_CONTINUE);
#endif // HAVE_LIBPAM
}


//
// 'clean_jobs()' - Clean out old (completed) jobs.
//

static void
clean_jobs(ippeve_printer_t *printer)	// I - Printer
{
  ippeve_job_t	*job;			// Current job
  time_t	cleantime;		// Clean time


  if (cupsArrayGetCount(printer->jobs) == 0)
    return;

  cleantime = time(NULL) - 60;

  cupsRWLockWrite(&(printer->rwlock));
  for (job = (ippeve_job_t *)cupsArrayGetFirst(printer->jobs);
       job;
       job = (ippeve_job_t *)cupsArrayGetNext(printer->jobs))
    if (job->completed && job->completed < cleantime)
    {
      cupsArrayRemove(printer->jobs, job);
      delete_job(job);
    }
    else
      break;
  cupsRWUnlock(&(printer->rwlock));
}


//
// 'compare_jobs()' - Compare two jobs.
//

static int				// O - Result of comparison
compare_jobs(ippeve_job_t *a,		// I - First job
             ippeve_job_t *b,		// I - Second job
             void         *data)	// I - Callback data (unused)
{
  (void)data;

  return (b->id - a->id);
}


//
// 'copy_attributes()' - Copy attributes from one request to another.
//

static void
copy_attributes(ipp_t        *to,	// I - Destination request
	        ipp_t        *from,	// I - Source request
	        cups_array_t *ra,	// I - Requested attributes
	        ipp_tag_t    group_tag,	// I - Group to copy
	        bool         quickcopy)	// I - Do a quick copy?
{
  ippeve_filter_t	filter;			// Filter data


  filter.ra        = ra;
  filter.group_tag = group_tag;

  ippCopyAttributes(to, from, quickcopy, (ipp_copy_cb_t)filter_cb, &filter);
}


//
// 'copy_job_attrs()' - Copy job attributes to the response.
//

static void
copy_job_attributes(
    ippeve_client_t *client,		// I - Client
    ippeve_job_t    *job,			// I - Job
    cups_array_t  *ra)			// I - requested-attributes
{
  copy_attributes(client->response, job->attrs, ra, IPP_TAG_JOB, 0);

  if (!ra || cupsArrayFind(ra, "date-time-at-completed"))
  {
    if (job->completed)
      ippAddDate(client->response, IPP_TAG_JOB, "date-time-at-completed", ippTimeToDate(job->completed));
    else
      ippAddOutOfBand(client->response, IPP_TAG_JOB, IPP_TAG_NOVALUE, "date-time-at-completed");
  }

  if (!ra || cupsArrayFind(ra, "date-time-at-processing"))
  {
    if (job->processing)
      ippAddDate(client->response, IPP_TAG_JOB, "date-time-at-processing", ippTimeToDate(job->processing));
    else
      ippAddOutOfBand(client->response, IPP_TAG_JOB, IPP_TAG_NOVALUE, "date-time-at-processing");
  }

  if (!ra || cupsArrayFind(ra, "job-impressions"))
    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-impressions", job->impressions);

  if (!ra || cupsArrayFind(ra, "job-impressions-completed"))
    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-impressions-completed", job->impcompleted);

  if (!ra || cupsArrayFind(ra, "job-printer-up-time"))
    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-printer-up-time", (int)(time(NULL) - client->printer->start_time));

  if (!ra || cupsArrayFind(ra, "job-state"))
    ippAddInteger(client->response, IPP_TAG_JOB, IPP_TAG_ENUM, "job-state", (int)job->state);

  if (!ra || cupsArrayFind(ra, "job-state-message"))
  {
    if (job->message)
    {
      ippAddString(client->response, IPP_TAG_JOB, IPP_TAG_TEXT, "job-state-message", NULL, job->message);
    }
    else
    {
      switch (job->state)
      {
	case IPP_JSTATE_PENDING :
	    ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job pending.");
	    break;

	case IPP_JSTATE_HELD :
	    if (job->fd >= 0)
	      ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job incoming.");
	    else if (ippFindAttribute(job->attrs, "job-hold-until", IPP_TAG_ZERO))
	      ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job held.");
	    else
	      ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job created.");
	    break;

	case IPP_JSTATE_PROCESSING :
	    if (job->cancel)
	      ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job canceling.");
	    else
	      ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job printing.");
	    break;

	case IPP_JSTATE_STOPPED :
	    ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job stopped.");
	    break;

	case IPP_JSTATE_CANCELED :
	    ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job canceled.");
	    break;

	case IPP_JSTATE_ABORTED :
	    ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job aborted.");
	    break;

	case IPP_JSTATE_COMPLETED :
	    ippAddString(client->response, IPP_TAG_JOB, IPP_CONST_TAG(IPP_TAG_TEXT), "job-state-message", NULL, "Job completed.");
	    break;
      }
    }
  }

  if (!ra || cupsArrayFind(ra, "job-state-reasons"))
  {
    switch (job->state)
    {
      case IPP_JSTATE_PENDING :
	  ippAddString(client->response, IPP_TAG_JOB,
	               IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
		       NULL, "none");
	  break;

      case IPP_JSTATE_HELD :
          if (job->fd >= 0)
	    ippAddString(client->response, IPP_TAG_JOB,
	                 IPP_CONST_TAG(IPP_TAG_KEYWORD),
	                 "job-state-reasons", NULL, "job-incoming");
	  else if (ippFindAttribute(job->attrs, "job-hold-until", IPP_TAG_ZERO))
	    ippAddString(client->response, IPP_TAG_JOB,
	                 IPP_CONST_TAG(IPP_TAG_KEYWORD),
	                 "job-state-reasons", NULL, "job-hold-until-specified");
          else
	    ippAddString(client->response, IPP_TAG_JOB,
	                 IPP_CONST_TAG(IPP_TAG_KEYWORD),
	                 "job-state-reasons", NULL, "job-data-insufficient");
	  break;

      case IPP_JSTATE_PROCESSING :
	  if (job->cancel)
	    ippAddString(client->response, IPP_TAG_JOB,
	                 IPP_CONST_TAG(IPP_TAG_KEYWORD),
	                 "job-state-reasons", NULL, "processing-to-stop-point");
	  else
	    ippAddString(client->response, IPP_TAG_JOB,
	                 IPP_CONST_TAG(IPP_TAG_KEYWORD),
	                 "job-state-reasons", NULL, "job-printing");
	  break;

      case IPP_JSTATE_STOPPED :
	  ippAddString(client->response, IPP_TAG_JOB,
	               IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
		       NULL, "job-stopped");
	  break;

      case IPP_JSTATE_CANCELED :
	  ippAddString(client->response, IPP_TAG_JOB,
	               IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
		       NULL, "job-canceled-by-user");
	  break;

      case IPP_JSTATE_ABORTED :
	  ippAddString(client->response, IPP_TAG_JOB,
	               IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
		       NULL, "aborted-by-system");
	  break;

      case IPP_JSTATE_COMPLETED :
	  ippAddString(client->response, IPP_TAG_JOB,
	               IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-state-reasons",
		       NULL, "job-completed-successfully");
	  break;
    }
  }

  if (!ra || cupsArrayFind(ra, "time-at-completed"))
    ippAddInteger(client->response, IPP_TAG_JOB,
                  job->completed ? IPP_TAG_INTEGER : IPP_TAG_NOVALUE,
                  "time-at-completed", (int)(job->completed - client->printer->start_time));

  if (!ra || cupsArrayFind(ra, "time-at-processing"))
    ippAddInteger(client->response, IPP_TAG_JOB,
                  job->processing ? IPP_TAG_INTEGER : IPP_TAG_NOVALUE,
                  "time-at-processing", (int)(job->processing - client->printer->start_time));
}


//
// 'create_client()' - Accept a new network connection and create a client
//                     object.
//

static ippeve_client_t *			// O - Client
create_client(ippeve_printer_t *printer,	// I - Printer
              int            sock)	// I - Listen socket
{
  ippeve_client_t	*client;		// Client


  if ((client = calloc(1, sizeof(ippeve_client_t))) == NULL)
  {
    perror("Unable to allocate memory for client");
    return (NULL);
  }

  client->printer = printer;

  // Accept the client and get the remote address...
  if ((client->http = httpAcceptConnection(sock, 1)) == NULL)
  {
    perror("Unable to accept client connection");

    free(client);

    return (NULL);
  }

  httpGetHostname(client->http, client->hostname, sizeof(client->hostname));

  if (Verbosity)
    fprintf(stderr, "Accepted connection from %s\n", client->hostname);

  return (client);
}


//
// 'create_job()' - Create a new job object from a Print-Job or Create-Job
//                  request.
//

static ippeve_job_t *			// O - Job
create_job(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// Job
  ipp_attribute_t	*attr;		// Job attribute
  char			uri[1024],	// job-uri value
			uuid[64];	// job-uuid value


  cupsRWLockWrite(&(client->printer->rwlock));
  if (client->printer->active_job &&
      client->printer->active_job->state < IPP_JSTATE_CANCELED)
  {
    // Only accept a single job at a time...
    cupsRWUnlock(&(client->printer->rwlock));
    return (NULL);
  }

  // Allocate and initialize the job object...
  if ((job = calloc(1, sizeof(ippeve_job_t))) == NULL)
  {
    perror("Unable to allocate memory for job");
    cupsRWUnlock(&(client->printer->rwlock));
    return (NULL);
  }

  job->printer    = client->printer;
  job->attrs      = ippNew();
  job->state      = IPP_JSTATE_HELD;
  job->fd         = -1;

  // Copy all of the job attributes...
  copy_attributes(job->attrs, client->request, NULL, IPP_TAG_JOB, 0);

  // Get the requesting-user-name, document format, and priority...
  if ((attr = ippFindAttribute(client->request, "requesting-user-name", IPP_TAG_NAME)) != NULL)
    job->username = ippGetString(attr, 0, NULL);
  else
    job->username = "anonymous";

  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_NAME, "job-originating-user-name", NULL, job->username);

  if ((attr = ippFindAttribute(client->request, "requesting-user-uri", IPP_TAG_ZERO)) != NULL)
  {
    const char *user_uri = ippGetString(attr, 0, /*language*/NULL);
					// String value

    if (ippGetGroupTag(attr) == IPP_TAG_OPERATION && ippGetValueTag(attr) == IPP_TAG_URI && ippGetCount(attr) == 1 && user_uri && !strncmp(user_uri, "mailto:", 7))
      ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-originating-user-uri", NULL, user_uri);
    else
      respond_ignored(client, attr);
  }

  if ((attr = ippFindAttribute(client->request, "document-metadata", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetGroupTag(attr) == IPP_TAG_OPERATION && ippGetValueTag(attr) == IPP_TAG_STRING)
    {
      if ((attr = ippCopyAttribute(job->attrs, attr, /*quickcopy*/false)) != NULL)
        ippSetGroupTag(job->attrs, &attr, IPP_TAG_JOB);
    }
    else
    {
      respond_ignored(client, attr);
    }
  }

  if ((attr = ippFindAttribute(client->request, "document-password", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetGroupTag(attr) == IPP_TAG_OPERATION && ippGetValueTag(attr) == IPP_TAG_STRING && ippGetCount(attr) == 1)
    {
      void	*dataptr;		// octetString pointer
      size_t	datalen;		// Length of octetString

      if ((dataptr = ippGetOctetString(attr, 0, &datalen)) != NULL && datalen < (sizeof(job->password) - 1))
        memcpy(job->password, dataptr, datalen);
      else
	respond_ignored(client, attr);
    }
    else
    {
      respond_ignored(client, attr);
    }
  }

  if (ippGetOperation(client->request) != IPP_OP_CREATE_JOB)
  {
    if ((attr = ippFindAttribute(job->attrs, "document-format-detected", IPP_TAG_MIMETYPE)) != NULL)
      job->format = ippGetString(attr, 0, NULL);
    else if ((attr = ippFindAttribute(job->attrs, "document-format-supplied", IPP_TAG_MIMETYPE)) != NULL)
      job->format = ippGetString(attr, 0, NULL);
    else
      job->format = "application/octet-stream";
  }

  if ((attr = ippFindAttribute(client->request, "job-impressions", IPP_TAG_INTEGER)) != NULL)
    job->impressions = ippGetInteger(attr, 0);

  if ((attr = ippFindAttribute(client->request, "job-name", IPP_TAG_NAME)) != NULL)
    job->name = ippGetString(attr, 0, NULL);

  // Add job description attributes and add to the jobs array...
  job->id = client->printer->next_job_id ++;

  if ((attr = ippFindAttribute(client->request, "printer-uri", IPP_TAG_URI)) != NULL)
    snprintf(uri, sizeof(uri), "%s/%d", ippGetString(attr, 0, NULL), job->id);
  else
    httpAssembleURIf(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp", NULL, client->printer->hostname, client->printer->port, "/ipp/print/%d", job->id);

  httpAssembleUUID(client->printer->hostname, client->printer->port, client->printer->name, job->id, uuid, sizeof(uuid));

  ippAddDate(job->attrs, IPP_TAG_JOB, "date-time-at-creation", ippTimeToDate(time(&job->created)));
  ippAddInteger(job->attrs, IPP_TAG_JOB, IPP_TAG_INTEGER, "job-id", job->id);
  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-uri", NULL, uri);
  ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-uuid", NULL, uuid);
  if ((attr = ippFindAttribute(client->request, "printer-uri", IPP_TAG_URI)) != NULL)
  {
    ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-printer-uri", NULL, ippGetString(attr, 0, NULL));
  }
  else
  {
    char printer_uri[1024];		// job-printer-uri value

    httpAssembleURI(HTTP_URI_CODING_ALL, printer_uri, sizeof(printer_uri), "ipp", NULL, client->printer->hostname, client->printer->port, "/ipp/print");
    ippAddString(job->attrs, IPP_TAG_JOB, IPP_TAG_URI, "job-printer-uri", NULL, printer_uri);
  }

  ippAddInteger(job->attrs, IPP_TAG_JOB, IPP_TAG_INTEGER, "time-at-creation", (int)(job->created - client->printer->start_time));

  cupsArrayAdd(client->printer->jobs, job);
  client->printer->active_job = job;

  cupsRWUnlock(&(client->printer->rwlock));

  return (job);
}


//
// 'create_job_file()' - Create a file for the document in a job.
//

static int				// O - File descriptor or -1 on error
create_job_file(
    ippeve_job_t     *job,		// I - Job
    char             *fname,		// I - Filename buffer
    size_t           fnamesize,		// I - Size of filename buffer
    const char       *directory,	// I - Directory to store in
    const char       *ext)		// I - Extension (`NULL` for default)
{
  char			name[256],	// "Safe" filename
			*nameptr;	// Pointer into filename
  const char		*job_name;	// job-name value


  // Make a name from the job-name attribute...
  if ((job_name = ippGetString(ippFindAttribute(job->attrs, "job-name", IPP_TAG_NAME), 0, NULL)) == NULL)
    job_name = "untitled";

  for (nameptr = name; *job_name && nameptr < (name + sizeof(name) - 1); job_name ++)
  {
    if (isalnum(*job_name & 255) || *job_name == '-')
    {
      *nameptr++ = (char)tolower(*job_name & 255);
    }
    else
    {
      *nameptr++ = '_';

      while (job_name[1] && !isalnum(job_name[1] & 255) && job_name[1] != '-')
        job_name ++;
    }
  }

  *nameptr = '\0';

  // Figure out the extension...
  if (!ext)
  {
    if (!strcasecmp(job->format, "image/jpeg"))
      ext = "jpg";
    else if (!strcasecmp(job->format, "image/png"))
      ext = "png";
    else if (!strcasecmp(job->format, "image/pwg-raster"))
      ext = "pwg";
    else if (!strcasecmp(job->format, "image/urf"))
      ext = "urf";
    else if (!strcasecmp(job->format, "application/pdf"))
      ext = "pdf";
    else if (!strcasecmp(job->format, "application/postscript"))
      ext = "ps";
    else if (!strcasecmp(job->format, "application/vnd.hp-pcl"))
      ext = "pcl";
    else
      ext = "dat";
  }

  // Create a filename with the job-id, job-name, and document-format (extension)...
  snprintf(fname, fnamesize, "%s/%d-%s.%s", directory, job->id, name, ext);

  return (open(fname, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666));
}


//
// 'create_listener()' - Create a listener socket.
//

static int				// O - Listener socket or -1 on error
create_listener(const char *name,	// I - Host name (`NULL` for any address)
                int        port,	// I - Port number
                int        family)	// I - Address family
{
  int			sock;		// Listener socket
  http_addrlist_t	*addrlist;	// Listen address
  char			service[255];	// Service port


  snprintf(service, sizeof(service), "%d", port);
  if ((addrlist = httpAddrGetList(name, family, service)) == NULL)
    return (-1);

  sock = httpAddrListen(&(addrlist->addr), port);

  httpAddrFreeList(addrlist);

  return (sock);
}


//
// 'create_media_col()' - Create a media-col value.
//

static ipp_t *				// O - media-col collection
create_media_col(const char *media,	// I - Media name
		 const char *source,	// I - Media source, if any
		 const char *type,	// I - Media type, if any
		 ipp_t      *media_size,// I - media-size collection value
		 int        bottom,	// I - Bottom margin in 2540ths
		 int        left,	// I - Left margin in 2540ths
		 int        right,	// I - Right margin in 2540ths
		 int        top)	// I - Top margin in 2540ths
{
  ipp_t		*media_col = ippNew();	// media-col value
  char		media_key[256];		// media-key value
  const char	*media_key_suffix = "";	// media-key suffix


  if (bottom == 0 && left == 0 && right == 0 && top == 0)
    media_key_suffix = "_borderless";

  if (media)
  {
    if (type && source)
      snprintf(media_key, sizeof(media_key), "%s_%s_%s%s", media, source, type, media_key_suffix);
    else if (type)
      snprintf(media_key, sizeof(media_key), "%s__%s%s", media, type, media_key_suffix);
    else if (source)
      snprintf(media_key, sizeof(media_key), "%s_%s%s", media, source, media_key_suffix);
    else
      snprintf(media_key, sizeof(media_key), "%s%s", media, media_key_suffix);

    ippAddString(media_col, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "media-key", NULL, media_key);
  }
  ippAddCollection(media_col, IPP_TAG_PRINTER, "media-size", media_size);
  if (media)
    ippAddString(media_col, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "media-size-name", NULL, media);
  if (bottom >= 0)
    ippAddInteger(media_col, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-bottom-margin", bottom);
  if (left >= 0)
    ippAddInteger(media_col, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-left-margin", left);
  if (right >= 0)
    ippAddInteger(media_col, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-right-margin", right);
  if (top >= 0)
    ippAddInteger(media_col, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-top-margin", top);
  if (source)
    ippAddString(media_col, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "media-source", NULL, source);
  if (type)
    ippAddString(media_col, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "media-type", NULL, type);

  ippDelete(media_size);

  return (media_col);
}


//
// 'create_media_size()' - Create a media-size value.
//

static ipp_t *				// O - media-col collection
create_media_size(int width,		// I - x-dimension in 2540ths
		  int length)		// I - y-dimension in 2540ths
{
  ipp_t	*media_size = ippNew();		// media-size value


  ippAddInteger(media_size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "x-dimension", width);
  ippAddInteger(media_size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "y-dimension", length);

  return (media_size);
}


//
// 'create_media_size_range()' - Create a ranged media-size value.
//

static ipp_t *				// O - media-col collection
create_media_size_range(int min_width,	// I - Minimum x-dimension in 2540ths
			int max_width,	// I - Maximum x-dimension in 2540ths
			int min_length,	// I - Minimum x-dimension in 2540ths
			int max_length)	// I - Maximum y-dimension in 2540ths
{
  ipp_t	*media_size = ippNew();		// media-size value


  ippAddRange(media_size, IPP_TAG_ZERO, "x-dimension", min_width, max_width);
  ippAddRange(media_size, IPP_TAG_ZERO, "y-dimension", min_length, max_length);

  return (media_size);
}


//
// 'create_printer()' - Create, register, and listen for connections to a
//                      printer object.
//

static ippeve_printer_t *		// O - Printer
create_printer(
    const char   *servername,		// I - Server hostname (NULL for default)
    int          serverport,		// I - Server port
    const char   *name,			// I - printer-name
    const char   *location,		// I - printer-location
    const char   *icons,		// I - printer-icons
    const char   *strings,		// I - printer-strings-uri
    cups_array_t *docformats,		// I - document-format-supported
    const char   *subtypes,		// I - DNS-SD service subtype(s)
    const char   *directory,		// I - Spool directory
    const char   *command,		// I - Command to run on job files, if any
    const char   *device_uri,		// I - Output device, if any
    const char   *output_format,	// I - Output format, if any
    ipp_t        *attrs)		// I - Capability attributes
{
  ippeve_printer_t	*printer;	// Printer
  size_t		i;		// Looping var
  ipp_attribute_t	*attr;		// Current attribute
#ifndef _WIN32
  char			path[1024];	// Full path to command
#endif // !_WIN32
  unsigned char		sha256[32];	// SHA-256 digest/sum
  char			uuid_data[1024],// Data to hash for printer-uuid
			uuid[128],	// printer-uuid
			*iconsptr;	// Pointer into icons string
  int			k_supported;	// Maximum file size supported
  size_t		num_formats = 0;// Number of supported document formats
  const char		*formats[100],	// Supported document formats
			*format;	// Current format
  bool			has_jpeg = false,
					// Supports JPEG?
			has_pdf = false;// Supports PDF?
  size_t		num_sup_attrs;	// Number of supported attributes
  const char		*sup_attrs[100];// Supported attributes
  char			xxx_supported[256];
					// Name of -supported attribute
#ifdef HAVE_STATVFS
  struct statvfs	spoolinfo;	// FS info for spool directory
  double		spoolsize;	// FS size
#elif defined(HAVE_STATFS)
  struct statfs		spoolinfo;	// FS info for spool directory
  double		spoolsize;	// FS size
#endif // HAVE_STATVFS
  static const char * const versions[] =// ipp-versions-supported values
  {
    "1.1",
    "2.0"
  };
  static const char * const features[] =// ipp-features-supported values
  {
    "ipp-everywhere"
  };
  static const int	ops[] =		// operations-supported values
  {
    IPP_OP_PRINT_JOB,
    IPP_OP_PRINT_URI,
    IPP_OP_VALIDATE_JOB,
    IPP_OP_CREATE_JOB,
    IPP_OP_SEND_DOCUMENT,
    IPP_OP_SEND_URI,
    IPP_OP_CANCEL_JOB,
    IPP_OP_GET_JOB_ATTRIBUTES,
    IPP_OP_GET_JOBS,
    IPP_OP_GET_PRINTER_ATTRIBUTES,
    IPP_OP_CANCEL_JOBS,
    IPP_OP_CANCEL_MY_JOBS,
    IPP_OP_CLOSE_JOB,
    IPP_OP_IDENTIFY_PRINTER
  };
  static const char * const charsets[] =// charset-supported values
  {
    "us-ascii",
    "utf-8"
  };
  static const char * const compressions[] =// compression-supported values
  {
    "deflate",
    "gzip",
    "none"
  };
  static const char * const identify_actions[] =
  {
    "display",
    "sound"
  };
  static const char * const job_creation[] =
  {					// job-creation-attributes-supported values
    "copies",
    "document-access",
    "document-charset",
    "document-format",
    "document-message",
    "document-metadata",
    "document-name",
    "document-natural-language",
    "document-password",
    "finishings",
    "finishings-col",
    "ipp-attribute-fidelity",
    "job-account-id",
    "job-account-type",
    "job-accouunting-sheets",
    "job-accounting-user-id",
    "job-authorization-uri",
    "job-error-action",
    "job-error-sheet",
    "job-hold-until",
    "job-hold-until-time",
    "job-mandatory-attributes",
    "job-message-to-operator",
    "job-name",
    "job-pages-per-set",
    "job-password",
    "job-password-encryption",
    "job-phone-number",
    "job-priority",
    "job-recipient-name",
    "job-resource-ids",
    "job-sheet-message",
    "job-sheets",
    "job-sheets-col",
    "media",
    "media-col",
    "multiple-document-handling",
    "number-up",
    "orientation-requested",
    "output-bin",
    "output-device",
    "overrides",
    "page-delivery",
    "page-ranges",
    "presentation-direction-number-up",
    "print-color-mode",
    "print-content-optimize",
    "print-quality",
    "print-rendering-intent",
    "print-scaling",
    "printer-resolution",
    "proof-print",
    "separator-sheets",
    "sides",
    "x-image-position",
    "x-image-shift",
    "x-side1-image-shift",
    "x-side2-image-shift",
    "y-image-position",
    "y-image-shift",
    "y-side1-image-shift",
    "y-side2-image-shift"
  };
  static const char * const job_history[] =
  {					// job-history-attributes-configured/supported
    "date-time-at-creation",
    "date-time-at-processing",
    "date-time-at-completed",
    "job-id",
    "job-name",
    "job-originating-user-name",
    "job-originating-user-uri",
    "job-printer-uri",
    "job-state",
    "job-state-reasons",
    "job-uri",
    "time-at-creation",
    "time-at-processing",
    "time-at-completed"
  };
  static const char * const media_col_supported[] =
  {					// media-col-supported values
    "media-bottom-margin",
    "media-left-margin",
    "media-right-margin",
    "media-size",
    "media-size-name",
    "media-source",
    "media-top-margin",
    "media-type"
  };
  static const char * const multiple_document_handling[] =
  {					// multiple-document-handling-supported values
    "separate-documents-uncollated-copies",
    "separate-documents-collated-copies"
  };
  static const char * const reference_uri_schemes_supported[] =
  {					// reference-uri-schemes-supported
    "file",
    "ftp",
    "http",
    "https"
  };
  static const char * const uri_authentication_supported[] =
  {					// uri-authentication-supported values
    "none",
    "none"
  };
  static const char * const uri_authentication_basic[] =
  {					// uri-authentication-supported values with authentication
    "basic",
    "basic"
  };
  static const char * const uri_security_supported[] =
  {					// uri-security-supported values
    "none",
    "tls"
  };
  static const char * const which_jobs[] =
  {					// which-jobs-supported values
    "completed",
    "not-completed",
    "aborted",
    "all",
    "canceled",
    "pending",
    "pending-held",
    "processing",
    "processing-stopped"
  };


#ifndef _WIN32
  // If a command was specified, make sure it exists and is executable...
  if (command && access(command, X_OK))
  {
    if (cupsFileFind(command, getenv("PATH"), true, path, sizeof(path)))
    {
      // Found the command in the current path...
      command = path;
    }
    else
    {
      // No command...
      cupsLangPrintf(stderr, _("Unable to execute \"%s\": %s"), command, strerror(errno));
      return (NULL);
    }
  }
#endif // !_WIN32

  // Allocate memory for the printer...
  if ((printer = calloc(1, sizeof(ippeve_printer_t))) == NULL)
  {
    cupsLangPrintf(stderr, _("%s: Out of memory."), "ippeveprinter");
    return (NULL);
  }

  printer->ipv4           = -1;
  printer->ipv6           = -1;
  printer->name           = strdup(name);
  printer->dnssd          = cupsDNSSDNew(NULL, NULL);
  printer->dnssd_name     = strdup(name);
  printer->dnssd_subtypes = subtypes ? strdup(subtypes) : NULL;
  printer->command        = command ? strdup(command) : NULL;
  printer->device_uri     = device_uri ? strdup(device_uri) : NULL;
  printer->output_format  = output_format ? strdup(output_format) : NULL;
  printer->directory      = strdup(directory);
  printer->icons[0]       = icons ? strdup(icons) : NULL;
  printer->strings        = strings ? strdup(strings) : NULL;
  printer->port           = serverport;
  printer->start_time     = time(NULL);
  printer->config_time    = printer->start_time;
  printer->state          = IPP_PSTATE_IDLE;
  printer->state_reasons  = IPPEVE_PREASON_NONE;
  printer->state_time     = printer->start_time;
  printer->jobs           = cupsArrayNew((cups_array_cb_t)compare_jobs, NULL, NULL, 0, NULL, NULL);
  printer->next_job_id    = 1;

  if (printer->icons[0])
  {
    // Extract up to 3 icons...
    for (i = 1, iconsptr = strchr(printer->icons[0], ','); iconsptr && i < 3; i ++, iconsptr = strchr(iconsptr, ','))
    {
      *iconsptr++ = '\0';
      printer->icons[i] = iconsptr;
    }

    if (iconsptr)
      *iconsptr = '\0';			// Strip any icons after the third...

    while (i < 3)
    {
      printer->icons[i] = printer->icons[i - 1];
      i ++;
    }
  }

  if (servername)
  {
    printer->hostname = strdup(servername);
  }
  else
  {
    char	temp[1024];		// Temporary string

    printer->hostname = strdup(cupsDNSSDCopyHostName(printer->dnssd, temp, sizeof(temp)));
  }

  cupsRWInit(&(printer->rwlock));

  // Create the listener sockets...
  if (printer->port)
  {
    if ((printer->ipv4 = create_listener(servername, printer->port, AF_INET)) < 0)
    {
      perror("Unable to create IPv4 listener");
      goto bad_printer;
    }
  }
  else
  {
#ifdef _WIN32
    // Windows is almost always used as a single user system, so use a default
    // port number of 8631.
    serverport = 8631;

#else
    // Use 8000 + UID mod 1000 for the default port number...
    serverport = 8000 + ((int)getuid() % 1000);
#endif // _WIN32

    while (serverport < 10000)
    {
      if ((printer->ipv4 = create_listener(servername, serverport, AF_INET)) >= 0)
        break;

      serverport ++;
    }

    if (serverport < 10000)
    {
      cupsLangPrintf(stderr, _("Listening on port %d."), serverport);
      printer->port = serverport;
    }
    else
    {
      perror("Unable to create IPv4 listener");
      goto bad_printer;
    }
  }

  if ((printer->ipv6 = create_listener(servername, printer->port, AF_INET6)) < 0)
  {
    perror("Unable to create IPv6 listener");
    goto bad_printer;
  }

  // Prepare values for the printer attributes...
  snprintf(uuid_data, sizeof(uuid_data), "_IPPEVEPRINTER_:%s:%d:%s", printer->hostname, printer->port, printer->name);
  cupsHashData("sha2-256", (unsigned char *)uuid_data, strlen(uuid_data), sha256, sizeof(sha256));
  snprintf(uuid, sizeof(uuid), "urn:uuid:%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", sha256[0], sha256[1], sha256[3], sha256[4], sha256[5], sha256[6], (sha256[10] & 15) | 0x30, sha256[11], (sha256[15] & 0x3f) | 0x40, sha256[16], sha256[20], sha256[21], sha256[25], sha256[26], sha256[30], sha256[31]);

  if (Verbosity)
  {
    fprintf(stderr, "printer-uri-supported=\"ipp://%s:%d/ipp/print\",\"ipps://%s:%d/ipp/print\"\n", printer->hostname, printer->port, printer->hostname, printer->port);
    fprintf(stderr, "printer-uuid=\"%s\"\n", uuid);
  }

  // Get the maximum spool size based on the size of the filesystem used for
  // the spool directory.  If the host OS doesn't support the statfs call
  // or the filesystem is larger than 2TiB, always report INT_MAX.
#ifdef HAVE_STATVFS
  if (statvfs(printer->directory, &spoolinfo))
    k_supported = INT_MAX;
  else if ((spoolsize = (double)spoolinfo.f_frsize *
                        spoolinfo.f_blocks / 1024) > INT_MAX)
    k_supported = INT_MAX;
  else
    k_supported = (int)spoolsize;

#elif defined(HAVE_STATFS)
  if (statfs(printer->directory, &spoolinfo))
    k_supported = INT_MAX;
  else if ((spoolsize = (double)spoolinfo.f_bsize *
                        spoolinfo.f_blocks / 1024) > INT_MAX)
    k_supported = INT_MAX;
  else
    k_supported = (int)spoolsize;

#else
  k_supported = INT_MAX;
#endif // HAVE_STATVFS

  // Assemble the final list of document formats...
  if (docformats)
  {
    if (!cupsArrayFind(docformats, (void *)"application/octet-stream"))
      cupsArrayAdd(docformats, (void *)"application/octet-stream");

    for (format = (const char *)cupsArrayGetFirst(docformats); format && num_formats < (sizeof(formats) / sizeof(formats[0])); format = (const char *)cupsArrayGetNext(docformats))
      formats[num_formats ++] = format;
  }
  else if ((attr = ippFindAttribute(attrs, "document-format-supported", IPP_TAG_MIMETYPE)) != NULL)
  {
    size_t count = ippGetCount(attr);	// Number of formats

    for (i = 0; i < count && i < (sizeof(formats) / sizeof(formats[0])); i ++)
      formats[i] = ippGetString(attr, i, /*language*/NULL);

    num_formats = i;
  }

  for (i = 0; i < num_formats; i ++)
  {
    if (!strcasecmp(formats[i], "image/jpeg"))
      has_jpeg = true;
    else if (!strcasecmp(formats[i], "application/pdf"))
      has_pdf = true;
  }

  // Get the list of attributes that can be used when creating a job...
  num_sup_attrs = 0;
  sup_attrs[num_sup_attrs ++] = "document-access";
  sup_attrs[num_sup_attrs ++] = "document-charset";
  sup_attrs[num_sup_attrs ++] = "document-format";
  sup_attrs[num_sup_attrs ++] = "document-message";
  sup_attrs[num_sup_attrs ++] = "document-metadata";
  sup_attrs[num_sup_attrs ++] = "document-name";
  sup_attrs[num_sup_attrs ++] = "document-natural-language";
  if (has_pdf)
    sup_attrs[num_sup_attrs ++] = "document-password";
  sup_attrs[num_sup_attrs ++] = "ipp-attribute-fidelity";
  sup_attrs[num_sup_attrs ++] = "job-name";
  sup_attrs[num_sup_attrs ++] = "job-priority";

  for (i = 0; i < (int)(sizeof(job_creation) / sizeof(job_creation[0])) && num_sup_attrs < (int)(sizeof(sup_attrs) / sizeof(sup_attrs[0])); i ++)
  {
    snprintf(xxx_supported, sizeof(xxx_supported), "%s-supported", job_creation[i]);
    if (ippFindAttribute(attrs, xxx_supported, IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = job_creation[i];
  }

  // Fill out the rest of the printer attributes.
  printer->attrs = attrs;

  // charset-configured
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_CHARSET), "charset-configured", NULL, "utf-8");

  // charset-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_CHARSET), "charset-supported", sizeof(charsets) / sizeof(charsets[0]), NULL, charsets);

  // compression-supported
  if (!ippFindAttribute(printer->attrs, "compression-supported", IPP_TAG_ZERO))
    ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "compression-supported", (int)(sizeof(compressions) / sizeof(compressions[0])), NULL, compressions);

  // device-uuid
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URI, "device-uuid", /*language*/NULL, uuid);

  if (docformats)
  {
    // document-format-default
    if (!ippFindAttribute(printer->attrs, "document-format-default", IPP_TAG_MIMETYPE))
      ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_MIMETYPE), "document-format-default", NULL, "application/octet-stream");

    // document-format-supported
    if ((attr = ippFindAttribute(printer->attrs, "document-format-supported", IPP_TAG_MIMETYPE)) != NULL)
      ippDeleteAttribute(printer->attrs, attr);

    ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_MIMETYPE, "document-format-supported", num_formats, NULL, formats);
  }

  // generated-natural-language-supported
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_LANGUAGE), "generated-natural-language-supported", NULL, "en");

  // identify-actions-default
  ippAddString (printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "identify-actions-default", NULL, "sound");

  // identify-actions-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "identify-actions-supported", sizeof(identify_actions) / sizeof(identify_actions[0]), NULL, identify_actions);

  // ipp-features-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "ipp-features-supported", sizeof(features) / sizeof(features[0]), NULL, features);

  // ipp-versions-supported
  if (MaxVersion == 11)
    ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "ipp-versions-supported", NULL, "1.1");
  else
    ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "ipp-versions-supported", (int)(sizeof(versions) / sizeof(versions[0])), NULL, versions);

  // job-creation-attributes-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-creation-attributes-supported", num_sup_attrs, NULL, sup_attrs);

  // job-history-attributes-configured
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-history-attributes-configured", sizeof(job_history) / sizeof(job_history[0]), /*language*/NULL, job_history);

  // job-history-attributes-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-history-attributes-supported", sizeof(job_history) / sizeof(job_history[0]), /*language*/NULL, job_history);

  // job-history-interval-configured
  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "job-history-interval-configured", 60);

  // job-history-interval-supported
  ippAddRange(printer->attrs, IPP_TAG_PRINTER, "job-history-interval-supported", 0, 3600);

  // job-ids-supported
  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "job-ids-supported", true);

  // job-k-octets-supported
  ippAddRange(printer->attrs, IPP_TAG_PRINTER, "job-k-octets-supported", 0, k_supported);

  // job-mandatory-attributes-supported
  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "job-mandatory-attributes-supported", true);

  // job-priority-default
  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "job-priority-default", 50);

  // job-priority-supported
  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "job-priority-supported", 1);

  // job-sheets-default
//  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_NAME), "job-sheets-default", NULL, "none");

  // job-sheets-supported
//  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_NAME), "job-sheets-supported", NULL, "none");

  // job-spooling-supported
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "job-spooling-supported", /*language*/NULL, "stream");

  if (has_jpeg)
  {
    // jpeg-features-supported
    if (!ippFindAttribute(printer->attrs, "jpeg-features-supported", IPP_TAG_ZERO))
      ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "jpeg-features-supported", /*language*/NULL, "none");

    // jpeg-k-octets-supported
    if (!ippFindAttribute(printer->attrs, "jpeg-k-octets-supported", IPP_TAG_ZERO))
      ippAddRange(printer->attrs, IPP_TAG_PRINTER, "jpeg-k-octets-supported", 0, k_supported);

    // jpeg-x-dimension-supported
    if (!ippFindAttribute(printer->attrs, "jpeg-x-dimension-supported", IPP_TAG_ZERO))
      ippAddRange(printer->attrs, IPP_TAG_PRINTER, "jpeg-x-dimension-supported", 0, 16384);

    // jpeg-y-dimension-supported
    if (!ippFindAttribute(printer->attrs, "jpeg-y-dimension-supported", IPP_TAG_ZERO))
      ippAddRange(printer->attrs, IPP_TAG_PRINTER, "jpeg-y-dimension-supported", 1, 16384);
  }

  // media-col-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-col-supported", (int)(sizeof(media_col_supported) / sizeof(media_col_supported[0])), NULL, media_col_supported);

  // multiple-document-handling-default
  if (!ippFindAttribute(printer->attrs, "multiple-document-handling-default", IPP_TAG_ZERO))
    ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "multiple-document-handling-default", NULL, "separate-documents-collated-copies");

  // multiple-document-handling-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "multiple-document-handling-supported", sizeof(multiple_document_handling) / sizeof(multiple_document_handling[0]), NULL, multiple_document_handling);

  // multiple-document-jobs-supported
  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "multiple-document-jobs-supported", false);

  // multiple-operation-time-out
  ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "multiple-operation-time-out", 60);

  // multiple-operation-time-out-action
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "multiple-operation-time-out-action", NULL, "abort-job");

  // natural-language-configured
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_LANGUAGE), "natural-language-configured", NULL, "en");

  // operations-supported
  ippAddIntegers(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "operations-supported", sizeof(ops) / sizeof(ops[0]), ops);

  if (has_pdf)
  {
    // document-password-supported
    if (!ippFindAttribute(printer->attrs, "document-password-supported", IPP_TAG_ZERO))
      ippAddInteger(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "document-password-supported", 255);

    // pdf-k-octets-supported
    if (!ippFindAttribute(printer->attrs, "pdf-k-octets-supported", IPP_TAG_ZERO))
      ippAddRange(printer->attrs, IPP_TAG_PRINTER, "pdf-k-octets-supported", 0, k_supported);

    // pdf-versions-supported
    if (!ippFindAttribute(printer->attrs, "pdf-versions-supported", IPP_TAG_ZERO))
      ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "pdf-versions-supported", /*language*/NULL, "iso-32000-2_2017");
  }

  // pdl-override-supported
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "pdl-override-supported", NULL, "attempted");

  // preferred-attributes-supported
  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "preferred-attributes-supported", false);

  // print-processing-attributes-supported
  if (!ippFindAttribute(printer->attrs, "print-processing-attributes-supported", IPP_TAG_ZERO))
  {
    num_sup_attrs = 0;
    if (ippFindAttribute(printer->attrs, "media-overprint-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "media-overprint";
    if (ippFindAttribute(printer->attrs, "print-color-mode-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "print-color-mode";
     if (ippFindAttribute(printer->attrs, "print-content-optimize-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "print-content-optimize";
   if (ippFindAttribute(printer->attrs, "print-darkness-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "print-darkness";
    if (ippFindAttribute(printer->attrs, "print-rendering-intent-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "print-rendering-intent";
    if (ippFindAttribute(printer->attrs, "print-quality-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "print-quality";
    if (ippFindAttribute(printer->attrs, "print-speed-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "print-speed";
    if (ippFindAttribute(printer->attrs, "printer-resolution-supported", IPP_TAG_ZERO))
      sup_attrs[num_sup_attrs ++] = "printer-resolution";

    if (num_sup_attrs > 0)
      ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-processing-attributes-supported", num_sup_attrs, /*language*/NULL, sup_attrs);
  }

  // print-scaling-default
  if (!ippFindAttribute(printer->attrs, "print-scaling-default", IPP_TAG_ZERO))
    ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-scaling-default", /*language*/NULL, "none");

  // print-scaling-supported
  if (!ippFindAttribute(printer->attrs, "print-scaling-supported", IPP_TAG_ZERO))
    ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-scaling-supported", /*language*/NULL, "none");

  // printer-get-attributes-supported
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "printer-get-attributes-supported", NULL, "document-format");

  // printer-geo-location
  ippAddOutOfBand(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_UNKNOWN, "printer-geo-location");

  // printer-is-accepting-jobs
  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "printer-is-accepting-jobs", true);

  // printer-info
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_TEXT, "printer-info", NULL, name);

  // printer-kind
  if (!ippFindAttribute(printer->attrs, "printer-kind", IPP_TAG_ZERO))
    ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "printer-kind", /*language*/NULL, "document");

  // printer-location
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_TEXT, "printer-location", NULL, location);

  // printer-name
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_NAME, "printer-name", NULL, name);

  // printer-organization
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-organization", NULL, "");

  // printer-organizational-unit
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-organizational-unit", NULL, "");

  // printer-strings-languages-supported
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_LANGUAGE, "printer-strings-languages-supported", NULL, "en");

  // printer-uuid
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-uuid", NULL, uuid);

  // reference-uri-schemes-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_URISCHEME), "reference-uri-schemes-supported", (int)(sizeof(reference_uri_schemes_supported) / sizeof(reference_uri_schemes_supported[0])), NULL, reference_uri_schemes_supported);

  // requesting-user-uri-supported
  ippAddBoolean(printer->attrs, IPP_TAG_PRINTER, "requesting-user-uri-supported", true);

  // requesting-user-uri-scheme-supported
  ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_URISCHEME, "requesting-user-uri-schemes-supported", NULL, "mailto");

  // uri-authentication-supported
  if (PAMService)
    ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "uri-authentication-supported", 2, NULL, uri_authentication_basic);
  else
    ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "uri-authentication-supported", 2, NULL, uri_authentication_supported);

  // uri-security-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "uri-security-supported", 2, NULL, uri_security_supported);

  // which-jobs-supported
  ippAddStrings(printer->attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "which-jobs-supported", sizeof(which_jobs) / sizeof(which_jobs[0]), NULL, which_jobs);

  debug_attributes("Printer", printer->attrs, 0);

  // Register the printer with DNS-SD...
  if (!register_printer(printer))
    goto bad_printer;

  // Return it!
  return (printer);


  // If we get here we were unable to create the printer...
  bad_printer:

  delete_printer(printer);

  return (NULL);
}


//
// 'debug_attributes()' - Print attributes in a request or response.
//

static void
debug_attributes(const char *title,	// I - Title
                 ipp_t      *ipp,	// I - Request/response
                 int        type)	// I - 0 = object, 1 = request, 2 = response
{
  ipp_tag_t		group_tag;	// Current group
  ipp_attribute_t	*attr;		// Current attribute
  char			buffer[2048];	// String buffer for value
  int			major, minor;	// Version


  if (Verbosity <= 1)
    return;

  fprintf(stderr, "%s:\n", title);
  major = ippGetVersion(ipp, &minor);
  fprintf(stderr, "  version=%d.%d\n", major, minor);
  if (type == 1)
    fprintf(stderr, "  operation-id=%s(%04x)\n",
            ippOpString(ippGetOperation(ipp)), ippGetOperation(ipp));
  else if (type == 2)
    fprintf(stderr, "  status-code=%s(%04x)\n",
            ippErrorString(ippGetStatusCode(ipp)), ippGetStatusCode(ipp));
  fprintf(stderr, "  request-id=%d\n\n", ippGetRequestId(ipp));

  for (attr = ippGetFirstAttribute(ipp), group_tag = IPP_TAG_ZERO;
       attr;
       attr = ippGetNextAttribute(ipp))
  {
    if (ippGetGroupTag(attr) != group_tag)
    {
      group_tag = ippGetGroupTag(attr);
      fprintf(stderr, "  %s\n", ippTagString(group_tag));
    }

    if (ippGetName(attr))
    {
      ippAttributeString(attr, buffer, sizeof(buffer));
      fprintf(stderr, "    %s (%s%s) %s\n", ippGetName(attr),
	      ippGetCount(attr) > 1 ? "1setOf " : "",
	      ippTagString(ippGetValueTag(attr)), buffer);
    }
  }
}


//
// 'delete_client()' - Close the socket and free all memory used by a client
//                     object.
//

static void
delete_client(ippeve_client_t *client)	// I - Client
{
  if (Verbosity)
    fprintf(stderr, "Closing connection from %s\n", client->hostname);

  // Flush pending writes before closing...
  httpFlushWrite(client->http);

  // Free memory...
  httpClose(client->http);

  ippDelete(client->request);
  ippDelete(client->response);

  free(client);
}


//
// 'delete_job()' - Remove from the printer and free all memory used by a job
//                  object.
//

static void
delete_job(ippeve_job_t *job)		// I - Job
{
  if (Verbosity)
    fprintf(stderr, "[Job %d] Removing job from history.\n", job->id);

  ippDelete(job->attrs);

  free(job->message);

  if (job->filename)
  {
    if (!KeepFiles)
      unlink(job->filename);

    free(job->filename);
  }

  free(job);
}


//
// 'delete_printer()' - Unregister, close listen sockets, and free all memory
//                      used by a printer object.
//

static void
delete_printer(
    ippeve_printer_t *printer)		// I - Printer
{
  if (printer->ipv4 >= 0)
    httpAddrClose(/*addr*/NULL, printer->ipv4);

  if (printer->ipv6 >= 0)
    httpAddrClose(/*addr*/NULL, printer->ipv6);

  cupsDNSSDDelete(printer->dnssd);

  free(printer->dnssd_name);
  free(printer->name);
  free(printer->icons[0]);
  free(printer->strings);
  free(printer->command);
  free(printer->device_uri);
  free(printer->directory);
  free(printer->hostname);

  ippDelete(printer->attrs);
  cupsArrayDelete(printer->jobs);

  free(printer);
}


//
// 'dnssd_callback()' - Handle DNS-SD registration events.
//

static void
dnssd_callback(
    cups_dnssd_service_t *service,	// I - Service registration
    ippeve_printer_t     *printer,	// I - Printer
    cups_dnssd_flags_t   flags)		// I - Status flags
{
  (void)service;

  if (flags & CUPS_DNSSD_FLAGS_COLLISION)
  {
    fputs("DNS-SD service name collision detected.\n", stderr);
    printer->dnssd_collision = 1;
  }
}


//
// 'filter_cb()' - Filter printer attributes based on the requested array.
//

static int				// O - 1 to copy, 0 to ignore
filter_cb(ippeve_filter_t   *filter,	// I - Filter parameters
          ipp_t           *dst,		// I - Destination (unused)
	  ipp_attribute_t *attr)	// I - Source attribute
{
  // Filter attributes as needed...
#ifndef _WIN32 // Avoid MS compiler bug
  (void)dst;
#endif // !_WIN32

  ipp_tag_t group = ippGetGroupTag(attr);
  const char *name = ippGetName(attr);

  if ((filter->group_tag != IPP_TAG_ZERO && group != filter->group_tag && group != IPP_TAG_ZERO) || !name || (!strcmp(name, "media-col-database") && !cupsArrayFind(filter->ra, (void *)name)))
    return (0);

  return (!filter->ra || cupsArrayFind(filter->ra, (void *)name) != NULL);
}


//
// 'find_job()' - Find a job specified in a request.
//

static ippeve_job_t *			// O - Job or NULL
find_job(ippeve_client_t *client)		// I - Client
{
  ipp_attribute_t	*attr;		// job-id or job-uri attribute
  ippeve_job_t		key,		// Job search key
			*job;		// Matching job, if any


  if ((attr = ippFindAttribute(client->request, "job-uri", IPP_TAG_URI)) != NULL)
  {
    const char *uri = ippGetString(attr, 0, NULL);
					// URI value
    const char *uriptr = strrchr(uri, '/');
					// Pointer to the last slash in the URI

    if (uriptr && isdigit(uriptr[1] & 255))
      key.id = atoi(uriptr + 1);
    else
      return (NULL);
  }
  else if ((attr = ippFindAttribute(client->request, "job-id", IPP_TAG_INTEGER)) != NULL)
    key.id = ippGetInteger(attr, 0);

  cupsRWLockRead(&(client->printer->rwlock));
  job = (ippeve_job_t *)cupsArrayFind(client->printer->jobs, &key);
  cupsRWUnlock(&(client->printer->rwlock));

  return (job);
}


//
// 'finish_document()' - Finish receiving a document file and start processing.
//

static void
finish_document_data(
    ippeve_client_t *client,		// I - Client
    ippeve_job_t    *job)		// I - Job
{
  char			filename[1024],	// Filename buffer
			buffer[4096];	// Copy buffer
  ssize_t		bytes;		// Bytes read
  cups_array_t		*ra;		// Attributes to send in response
  cups_thread_t        t;              // Thread


  // Create a file for the request data...
  if ((job->fd = create_job_file(job, filename, sizeof(filename), client->printer->directory, NULL)) < 0)
  {
    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to create print file: %s", strerror(errno));
    goto abort_job;
  }

  if (Verbosity)
    fprintf(stderr, "Created job file \"%s\", format \"%s\".\n", filename, job->format);

  while ((bytes = httpRead(client->http, buffer, sizeof(buffer))) > 0)
  {
    if (write(job->fd, buffer, (size_t)bytes) < bytes)
    {
      respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to write print file: %s", strerror(errno));
      goto cleanup_and_abort;
    }
  }

  if (bytes < 0)
  {
    // Got an error while reading the print data, so abort this job.
    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to read print file.");
    goto cleanup_and_abort;
  }

  // Close the file descriptor
  if (close(job->fd))
  {
    job->fd = -1;
    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to close print file: %s", strerror(errno));
    goto cleanup_and_abort;
  }

  job->fd       = -1;
  job->filename = strdup(filename);
  job->state    = IPP_JSTATE_PENDING;

  // Process the job...
  t = cupsThreadCreate((cups_thread_func_t)process_job, job);

  if (t)
  {
    cupsThreadDetach(t);
  }
  else
  {
    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to process job.");
    goto abort_job;
  }

  // Return the job info...
  respond_ipp(client, IPP_STATUS_OK, NULL);

  ra = cupsArrayNew(_cupsArrayStrcmp, NULL, NULL, 0, NULL, NULL);
  cupsArrayAdd(ra, "job-id");
  cupsArrayAdd(ra, "job-state");
  cupsArrayAdd(ra, "job-state-message");
  cupsArrayAdd(ra, "job-state-reasons");
  cupsArrayAdd(ra, "job-uri");

  copy_job_attributes(client, job, ra);
  cupsArrayDelete(ra);
  return;

  // Cleanup file on error, then abort job...
  cleanup_and_abort:

  if (job->fd >= 0)
  {
    close(job->fd);
    job->fd = -1;
  }

  unlink(filename);

  // If we get here we had to abort the job...
  abort_job:

  job->state     = IPP_JSTATE_ABORTED;
  job->completed = time(NULL);

  ra = cupsArrayNew(_cupsArrayStrcmp, NULL, NULL, 0, NULL, NULL);
  cupsArrayAdd(ra, "job-id");
  cupsArrayAdd(ra, "job-state");
  cupsArrayAdd(ra, "job-state-reasons");
  cupsArrayAdd(ra, "job-uri");

  copy_job_attributes(client, job, ra);
  cupsArrayDelete(ra);
}


//
// 'finish_uri()' - Finish fetching a document URI and start processing.
//

static void
finish_document_uri(
    ippeve_client_t *client,		// I - Client
    ippeve_job_t    *job)		// I - Job
{
  ipp_attribute_t	*uri;		// document-uri
  char			scheme[256],	// URI scheme
			userpass[256],	// Username and password info
			hostname[256],	// Hostname
			resource[1024];	// Resource path
  int			port;		// Port number
  http_uri_status_t	uri_status;	// URI decode status
  http_encryption_t	encryption;	// Encryption to use, if any
  http_t		*http;		// Connection for http/https URIs
  http_status_t		status;		// Access status for http/https URIs
  int			infile;		// Input file for local file URIs
  char			filename[1024],	// Filename buffer
			buffer[4096];	// Copy buffer
  ssize_t		bytes;		// Bytes read
  ipp_attribute_t	*attr;		// Current attribute
  cups_array_t		*ra;		// Attributes to send in response


  // Do we have a file to print?
  if (have_document_data(client))
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Unexpected document data following request.");

    goto abort_job;
  }

  // Do we have a document URI?
  if ((uri = ippFindAttribute(client->request, "document-uri", IPP_TAG_URI)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Missing document-uri.");

    goto abort_job;
  }

  if (ippGetCount(uri) != 1)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Too many document-uri values.");

    goto abort_job;
  }

  if ((uri_status = httpSeparateURI(HTTP_URI_CODING_ALL, ippGetString(uri, 0, NULL), scheme, sizeof(scheme), userpass, sizeof(userpass), hostname, sizeof(hostname), &port, resource, sizeof(resource))) < HTTP_URI_STATUS_OK)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Bad document-uri: %s", httpURIStatusString(uri_status));

    goto abort_job;
  }

  if (strcmp(scheme, "file") && strcmp(scheme, "https") && strcmp(scheme, "http"))
  {
    respond_ipp(client, IPP_STATUS_ERROR_URI_SCHEME, "URI scheme '%s' not supported.", scheme);

    goto abort_job;
  }

  // Get the document format for the job...
  cupsRWLockWrite(&(client->printer->rwlock));

  if ((attr = ippFindAttribute(job->attrs, "document-format", IPP_TAG_MIMETYPE)) != NULL)
    job->format = ippGetString(attr, 0, NULL);
  else
    job->format = "application/octet-stream";

  // Create a file for the request data...
  if ((job->fd = create_job_file(job, filename, sizeof(filename), client->printer->directory, NULL)) < 0)
  {
    cupsRWUnlock(&(client->printer->rwlock));

    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to create print file: %s", strerror(errno));

    goto abort_job;
  }

  cupsRWUnlock(&(client->printer->rwlock));

  if (!strcmp(scheme, "file"))
  {
    if ((infile = open(resource, O_RDONLY | O_BINARY)) < 0)
    {
      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS, "Unable to access URI '%s': %s", ippGetString(uri, 0, NULL), strerror(errno));

      goto abort_job;
    }

    do
    {
      if ((bytes = read(infile, buffer, sizeof(buffer))) < 0 && (errno == EAGAIN || errno == EINTR))
      {
        bytes = 1;
      }
      else if (bytes > 0 && write(job->fd, buffer, (size_t)bytes) < bytes)
      {
	respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to write print file: %s", strerror(errno));
	close(infile);

        goto cleanup_and_abort;
      }
    }
    while (bytes > 0);

    close(infile);
  }
  else
  {
    if (port == 443 || !strcmp(scheme, "https"))
      encryption = HTTP_ENCRYPTION_ALWAYS;
    else
      encryption = HTTP_ENCRYPTION_IF_REQUESTED;

    if ((http = httpConnect(hostname, port, NULL, AF_UNSPEC, encryption, 1, 30000, NULL)) == NULL)
    {
      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS, "Unable to connect to '%s' on port %d: %s", hostname, port, cupsGetErrorString());
      goto cleanup_and_abort;
    }

    httpClearFields(http);
    httpSetField(http, HTTP_FIELD_ACCEPT_LANGUAGE, "en");
    if (!httpWriteRequest(http, "GET", resource))
    {
      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS, "Unable to GET URI: %s", strerror(errno));
      httpClose(http);

      goto cleanup_and_abort;
    }

    while ((status = httpUpdate(http)) == HTTP_STATUS_CONTINUE);

    if (status != HTTP_STATUS_OK)
    {
      respond_ipp(client, IPP_STATUS_ERROR_DOCUMENT_ACCESS, "Unable to GET URI: %s", httpStatusString(status));
      httpClose(http);

      goto cleanup_and_abort;
    }

    while ((bytes = httpRead(http, buffer, sizeof(buffer))) > 0)
    {
      if (write(job->fd, buffer, (size_t)bytes) < bytes)
      {
	respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to write print file: %s", strerror(errno));
	httpClose(http);

        goto cleanup_and_abort;
      }
    }

    httpClose(http);
  }

  if (close(job->fd))
  {
    job->fd = -1;
    respond_ipp(client, IPP_STATUS_ERROR_INTERNAL, "Unable to close print file: %s", strerror(errno));
    goto cleanup_and_abort;
  }

  cupsRWLockWrite(&(client->printer->rwlock));

  job->fd       = -1;
  job->filename = strdup(filename);
  job->state    = IPP_JSTATE_PENDING;

  cupsRWUnlock(&(client->printer->rwlock));

  // Process the job...
  process_job(job);

  // Return the job info...
  respond_ipp(client, IPP_STATUS_OK, NULL);

  ra = cupsArrayNew(_cupsArrayStrcmp, NULL, NULL, 0, NULL, NULL);
  cupsArrayAdd(ra, "job-id");
  cupsArrayAdd(ra, "job-state");
  cupsArrayAdd(ra, "job-state-reasons");
  cupsArrayAdd(ra, "job-uri");

  copy_job_attributes(client, job, ra);
  cupsArrayDelete(ra);
  return;

  // Cleanup and then abort...
  cleanup_and_abort:

  if (job->fd >= 0)
  {
    close(job->fd);
    job->fd = -1;
  }

  unlink(filename);

  // If we get here we had to abort the job...
  abort_job:

  job->state     = IPP_JSTATE_ABORTED;
  job->completed = time(NULL);

  ra = cupsArrayNew(_cupsArrayStrcmp, NULL, NULL, 0, NULL, NULL);
  cupsArrayAdd(ra, "job-id");
  cupsArrayAdd(ra, "job-state");
  cupsArrayAdd(ra, "job-state-reasons");
  cupsArrayAdd(ra, "job-uri");

  copy_job_attributes(client, job, ra);
  cupsArrayDelete(ra);
}


//
// 'flush_document_data()' - Safely flush remaining document data.
//

static void
flush_document_data(
    ippeve_client_t *client)		// I - Client
{
  char	buffer[8192];			// Read buffer


  if (httpGetState(client->http) == HTTP_STATE_POST_RECV)
  {
    while (httpRead(client->http, buffer, sizeof(buffer)) > 0);
  }
}


//
// 'have_document_data()' - Determine whether we have more document data.
//

static bool				// O - `true` if data is present, `false` otherwise
have_document_data(
    ippeve_client_t *client)		// I - Client
{
  char temp;				// Data


  if (httpGetState(client->http) != HTTP_STATE_POST_RECV)
    return (false);
  else
    return (httpPeek(client->http, &temp, 1) > 0);
}


//
// 'html_escape()' - Write a HTML-safe string.
//

static bool				// O - `true` on success, `false` on error
html_escape(ippeve_client_t *client,	// I - Client
	    const char    *s,		// I - String to write
	    size_t        slen)		// I - Number of characters to write
{
  const char	*start,			// Start of segment
		*end;			// End of string


  start = s;
  end   = s + (slen > 0 ? slen : strlen(s));

  while (*s && s < end)
  {
    if (*s == '&' || *s == '<')
    {
      if (s > start)
      {
        if (httpWrite(client->http, start, (size_t)(s - start)) < 0)
          return (false);
      }

      if (*s == '&')
      {
        if (httpWrite(client->http, "&amp;", 5) < 0)
          return (false);
      }
      else if (httpWrite(client->http, "&lt;", 4) < 0)
        return (false);

      start = s + 1;
    }

    s ++;
  }

  if (s > start)
  {
    if (httpWrite(client->http, start, (size_t)(s - start)) < 0)
      return (false);
  }

  return (true);
}


//
// 'html_footer()' - Show the web interface footer.
//
// This function also writes the trailing 0-length chunk.
//

static bool				// O - `true` on success, `false` on error
html_footer(ippeve_client_t *client)	// I - Client
{
  if (!html_printf(client,
		   "</div>\n"
		   "</body>\n"
		   "</html>\n"))
    return (false);

  return (httpWrite(client->http, "", 0) >= 0);
}


//
// 'html_header()' - Show the web interface header and title.
//

static bool				// O - `true` on success, `false` on error
html_header(ippeve_client_t *client,	// I - Client
            const char    *title,	// I - Title
            int           refresh)	// I - Refresh timer, if any
{
  bool ret = true;			// Return value

  ret &= html_printf(client,
		     "<!doctype html>\n"
		     "<html>\n"
		     "<head>\n"
		     "<title>%s</title>\n"
		     "<link rel=\"shortcut icon\" href=\"/icon.png\" type=\"image/png\">\n"
		     "<link rel=\"apple-touch-icon\" href=\"/icon.png\" type=\"image/png\">\n"
		     "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=9\">\n", title);
  if (refresh > 0)
    ret &= html_printf(client, "<meta http-equiv=\"refresh\" content=\"%d\">\n", refresh);
  ret &= html_printf(client,
		     "<meta name=\"viewport\" content=\"width=device-width\">\n"
		     "<style>\n"
		     "body { font-family: sans-serif; margin: 0; }\n"
		     "div.body { padding: 0px 10px 10px; }\n"
		     "span.badge { background: #090; border-radius: 5px; color: #fff; padding: 5px 10px; }\n"
		     "span.bar { box-shadow: 0px 1px 5px #333; font-size: 75%%; }\n"
		     "table.form { border-collapse: collapse; margin-left: auto; margin-right: auto; margin-top: 10px; width: auto; }\n"
		     "table.form td, table.form th { padding: 5px 2px; }\n"
		     "table.form td.meter { border-right: solid 1px #ccc; padding: 0px; width: 400px; }\n"
		     "table.form th { text-align: right; }\n"
		     "table.striped { border-bottom: solid thin black; border-collapse: collapse; width: 100%%; }\n"
		     "table.striped tr:nth-child(even) { background: #fcfcfc; }\n"
		     "table.striped tr:nth-child(odd) { background: #f0f0f0; }\n"
		     "table.striped th { background: white; border-bottom: solid thin black; text-align: left; vertical-align: bottom; }\n"
		     "table.striped td { margin: 0; padding: 5px; vertical-align: top; }\n"
		     "table.nav { border-collapse: collapse; width: 100%%; }\n"
		     "table.nav td { margin: 0; text-align: center; }\n"
		     "td.nav a, td.nav a:active, td.nav a:hover, td.nav a:hover:link, td.nav a:hover:link:visited, td.nav a:link, td.nav a:link:visited, td.nav a:visited { background: inherit; color: inherit; font-size: 80%%; text-decoration: none; }\n"
		     "td.nav { background: #333; color: #fff; padding: 4px 8px; width: 33%%; }\n"
		     "td.nav.sel { background: #fff; color: #000; font-weight: bold; }\n"
		     "td.nav:hover { background: #666; color: #fff; }\n"
		     "td.nav:active { background: #000; color: #ff0; }\n"
		     "</style>\n"
		     "</head>\n"
		     "<body>\n"
		     "<table class=\"nav\"><tr>"
		     "<td class=\"nav%s\"><a href=\"/\">Status</a></td>"
		     "<td class=\"nav%s\"><a href=\"/supplies\">Supplies</a></td>"
		     "<td class=\"nav%s\"><a href=\"/media\">Media</a></td>"
		     "</tr></table>\n"
		     "<div class=\"body\">\n", !strcmp(client->uri, "/") ? " sel" : "", !strcmp(client->uri, "/supplies") ? " sel" : "", !strcmp(client->uri, "/media") ? " sel" : "");

  return (ret);
}


//
// 'html_printf()' - Send formatted text to the client, quoting as needed.
//

static bool				// O - `true` on success, `false` on error
html_printf(ippeve_client_t *client,	// I - Client
	    const char      *format,	// I - Printf-style format string
	    ...)			// I - Additional arguments as needed
{
  bool		ret = false;		// Return value
  va_list	ap;			// Pointer to arguments
  const char	*start;			// Start of string
  char		size,			// Size character (h, l, L)
		type;			// Format type character
  int		width,			// Width of field
		prec;			// Number of characters of precision
  char		tformat[100],		// Temporary format string for sprintf()
		*tptr,			// Pointer into temporary format
		temp[1024];		// Buffer for formatted numbers
  char		*s;			// Pointer to string


  // Loop through the format string, formatting as needed...
  va_start(ap, format);
  start = format;

  while (*format)
  {
    if (*format == '%')
    {
      if (format > start)
      {
        if (httpWrite(client->http, start, (size_t)(format - start)) < 0)
	  goto error;
      }

      tptr    = tformat;
      *tptr++ = *format++;

      if (*format == '%')
      {
        if (httpWrite(client->http, "%", 1) < 0)
	  goto error;

        format ++;
	start = format;
	continue;
      }
      else if (strchr(" -+#\'", *format))
        *tptr++ = *format++;

      if (*format == '*')
      {
        // Get width from argument...
	format ++;
	width = va_arg(ap, int);

	snprintf(tptr, sizeof(tformat) - (size_t)(tptr - tformat), "%d", width);
	tptr += strlen(tptr);
      }
      else
      {
	width = 0;

	while (isdigit(*format & 255))
	{
	  if (tptr < (tformat + sizeof(tformat) - 1))
	    *tptr++ = *format;

	  width = width * 10 + *format++ - '0';
	}
      }

      if (*format == '.')
      {
	if (tptr < (tformat + sizeof(tformat) - 1))
	  *tptr++ = *format;

        format ++;

        if (*format == '*')
	{
          // Get precision from argument...
	  format ++;
	  prec = va_arg(ap, int);

	  snprintf(tptr, sizeof(tformat) - (size_t)(tptr - tformat), "%d", prec);
	  tptr += strlen(tptr);
	}
	else
	{
	  prec = 0;

	  while (isdigit(*format & 255))
	  {
	    if (tptr < (tformat + sizeof(tformat) - 1))
	      *tptr++ = *format;

	    prec = prec * 10 + *format++ - '0';
	  }
	}
      }

      if (*format == 'l' && format[1] == 'l')
      {
        size = 'L';

	if (tptr < (tformat + sizeof(tformat) - 2))
	{
	  *tptr++ = 'l';
	  *tptr++ = 'l';
	}

	format += 2;
      }
      else if (*format == 'h' || *format == 'l' || *format == 'L')
      {
	if (tptr < (tformat + sizeof(tformat) - 1))
	  *tptr++ = *format;

        size = *format++;
      }
      else
        size = 0;

      if (!*format)
      {
        start = format;
        break;
      }

      if (tptr < (tformat + sizeof(tformat) - 1))
        *tptr++ = *format;

      type  = *format++;
      *tptr = '\0';
      start = format;

      switch (type)
      {
	case 'E' : // Floating point formats
	case 'G' :
	case 'e' :
	case 'f' :
	case 'g' :
	    if ((size_t)(width + 2) > sizeof(temp))
	      break;

	    snprintf(temp, sizeof(temp), tformat, va_arg(ap, double));

            if (httpWrite(client->http, temp, strlen(temp)) < 0)
	      goto error;
	    break;

        case 'B' : // Integer formats
	case 'X' :
	case 'b' :
        case 'd' :
	case 'i' :
	case 'o' :
	case 'u' :
	case 'x' :
	    if ((size_t)(width + 2) > sizeof(temp))
	      break;

#  ifdef HAVE_LONG_LONG
            if (size == 'L')
	      snprintf(temp, sizeof(temp), tformat, va_arg(ap, long long));
	    else
#  endif // HAVE_LONG_LONG
            if (size == 'l')
	      snprintf(temp, sizeof(temp), tformat, va_arg(ap, long));
	    else
	      snprintf(temp, sizeof(temp), tformat, va_arg(ap, int));

            if (httpWrite(client->http, temp, strlen(temp)) < 0)
	      goto error;
	    break;

	case 'p' : // Pointer value
	    if ((size_t)(width + 2) > sizeof(temp))
	      break;

	    snprintf(temp, sizeof(temp), tformat, va_arg(ap, void *));

            if (httpWrite(client->http, temp, strlen(temp)) < 0)
	      goto error;
	    break;

        case 'c' : // Character or character array
            if (width <= 1)
            {
              temp[0] = (char)va_arg(ap, int);
              temp[1] = '\0';
              html_escape(client, temp, 1);
            }
            else
              html_escape(client, va_arg(ap, char *), (size_t)width);
	    break;

	case 's' : // String
	    if ((s = va_arg(ap, char *)) == NULL)
	      s = "(null)";

            html_escape(client, s, strlen(s));
	    break;
      }
    }
    else
      format ++;
  }

  if (format > start)
  {
    if (httpWrite(client->http, start, (size_t)(format - start)) < 0)
      goto error;
  }

  ret = true;

  error:

  va_end(ap);

  return (ret);
}


//
// 'ipp_cancel_job()' - Cancel a job.
//

static void
ipp_cancel_job(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// Job information


  // Get the job...
  if ((job = find_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
    return;
  }

  // See if the job is already completed, canceled, or aborted; if so, we can't cancel...
  switch (job->state)
  {
    case IPP_JSTATE_CANCELED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is already canceled - can\'t cancel.", job->id);
        break;

    case IPP_JSTATE_ABORTED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is already aborted - can\'t cancel.", job->id);
        break;

    case IPP_JSTATE_COMPLETED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is already completed - can\'t cancel.", job->id);
        break;

    default :
        // Cancel the job...
	cupsRWLockWrite(&(client->printer->rwlock));

	if (job->state == IPP_JSTATE_PROCESSING ||
	    (job->state == IPP_JSTATE_HELD && job->fd >= 0))
          job->cancel = 1;
	else
	{
	  job->state     = IPP_JSTATE_CANCELED;
	  job->completed = time(NULL);
	}

	cupsRWUnlock(&(client->printer->rwlock));

	respond_ipp(client, IPP_STATUS_OK, NULL);
        break;
  }
}


//
// 'ipp_cancel_jobs()' - Cancel all jobs.
//
// Note: Since ippeveprinter doesn't do spooling, this really just cancels the
// current job.
//

static void
ipp_cancel_jobs(
    ippeve_client_t *client)		// I - Client
{
  ippeve_job_t		*job;		// Job information


  cupsRWLockWrite(&client->printer->rwlock);

  if ((job = client->printer->active_job) != NULL)
  {
    // See if the job is already completed, canceled, or aborted; if so, we can't cancel...
    if (job->state < IPP_JSTATE_CANCELED)
    {
      // Cancel the job...
      if (job->state == IPP_JSTATE_PROCESSING || (job->state == IPP_JSTATE_HELD && job->fd >= 0))
      {
	job->cancel = 1;
      }
      else
      {
	job->state     = IPP_JSTATE_CANCELED;
	job->completed = time(NULL);
      }
    }
  }

  respond_ipp(client, IPP_STATUS_OK, NULL);

  cupsRWUnlock(&client->printer->rwlock);
}


//
// 'ipp_close_job()' - Close an open job.
//

static void
ipp_close_job(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// Job information


  // Get the job...
  if ((job = find_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
    return;
  }

  // See if the job is already completed, canceled, or aborted; if so, we can't cancel...
  switch (job->state)
  {
    case IPP_JSTATE_CANCELED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is canceled - can\'t close.", job->id);
        break;

    case IPP_JSTATE_ABORTED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is aborted - can\'t close.", job->id);
        break;

    case IPP_JSTATE_COMPLETED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is completed - can\'t close.", job->id);
        break;

    case IPP_JSTATE_PROCESSING :
    case IPP_JSTATE_STOPPED :
	respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE,
		    "Job #%d is already closed.", job->id);
        break;

    default :
	respond_ipp(client, IPP_STATUS_OK, NULL);
        break;
  }
}


//
// 'ipp_create_job()' - Create a job object.
//

static void
ipp_create_job(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// New job
  cups_array_t		*ra;		// Attributes to send in response


  // Do we have a file to print?
  if (have_document_data(client))
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST,
                "Unexpected document data following request.");
    return;
  }

  // Validate print job attributes...
  if (!valid_job_attributes(client))
    return;

  // Create the job...
  if ((job = create_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BUSY, "Currently printing another job.");
    return;
  }

  // Return the job info...
  respond_ipp(client, IPP_STATUS_OK, NULL);

  ra = cupsArrayNew(_cupsArrayStrcmp, NULL, NULL, 0, NULL, NULL);
  cupsArrayAdd(ra, "job-id");
  cupsArrayAdd(ra, "job-state");
  cupsArrayAdd(ra, "job-state-message");
  cupsArrayAdd(ra, "job-state-reasons");
  cupsArrayAdd(ra, "job-uri");

  copy_job_attributes(client, job, ra);
  cupsArrayDelete(ra);
}


//
// 'ipp_get_job_attributes()' - Get the attributes for a job object.
//

static void
ipp_get_job_attributes(
    ippeve_client_t *client)		// I - Client
{
  ippeve_job_t	*job;			// Job
  cups_array_t	*ra;			// requested-attributes


  if ((job = find_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job not found.");
    return;
  }

  respond_ipp(client, IPP_STATUS_OK, NULL);

  ra = ippCreateRequestedArray(client->request);
  copy_job_attributes(client, job, ra);
  cupsArrayDelete(ra);
}


//
// 'ipp_get_jobs()' - Get a list of job objects.
//

static void
ipp_get_jobs(ippeve_client_t *client)	// I - Client
{
  ipp_attribute_t	*attr;		// Current attribute
  const char		*which_jobs = NULL;
					// which-jobs values
  int			job_comparison;	// Job comparison
  ipp_jstate_t		job_state;	// job-state value
  int			first_index,	// First job index (1-based)
			limit,		// Maximum number of jobs to return
			matches;	// Number of jobs that match
  size_t		i,		// Current job index
			first_i,	// First job index (0-based)
			count;		// Number of jobs
  const char		*username;	// Username
  ippeve_job_t		*job;		// Current job pointer
  cups_array_t		*ra;		// Requested attributes array


  // See if the "which-jobs" attribute have been specified...
  if ((attr = ippFindAttribute(client->request, "which-jobs", IPP_TAG_KEYWORD)) != NULL)
  {
    which_jobs = ippGetString(attr, 0, NULL);
    fprintf(stderr, "%s Get-Jobs which-jobs=%s", client->hostname, which_jobs);
  }

  if (!which_jobs || !strcmp(which_jobs, "not-completed"))
  {
    job_comparison = -1;
    job_state      = IPP_JSTATE_STOPPED;
  }
  else if (!strcmp(which_jobs, "completed"))
  {
    job_comparison = 1;
    job_state      = IPP_JSTATE_CANCELED;
  }
  else if (!strcmp(which_jobs, "aborted"))
  {
    job_comparison = 0;
    job_state      = IPP_JSTATE_ABORTED;
  }
  else if (!strcmp(which_jobs, "all"))
  {
    job_comparison = 1;
    job_state      = IPP_JSTATE_PENDING;
  }
  else if (!strcmp(which_jobs, "canceled"))
  {
    job_comparison = 0;
    job_state      = IPP_JSTATE_CANCELED;
  }
  else if (!strcmp(which_jobs, "pending"))
  {
    job_comparison = 0;
    job_state      = IPP_JSTATE_PENDING;
  }
  else if (!strcmp(which_jobs, "pending-held"))
  {
    job_comparison = 0;
    job_state      = IPP_JSTATE_HELD;
  }
  else if (!strcmp(which_jobs, "processing"))
  {
    job_comparison = 0;
    job_state      = IPP_JSTATE_PROCESSING;
  }
  else if (!strcmp(which_jobs, "processing-stopped"))
  {
    job_comparison = 0;
    job_state      = IPP_JSTATE_STOPPED;
  }
  else
  {
    respond_ipp(client, IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES,
                "The which-jobs value \"%s\" is not supported.", which_jobs);
    ippAddString(client->response, IPP_TAG_UNSUPPORTED_GROUP, IPP_TAG_KEYWORD,
                 "which-jobs", NULL, which_jobs);
    return;
  }

  // See if they want to limit the number of jobs reported...
  if ((attr = ippFindAttribute(client->request, "limit", IPP_TAG_INTEGER)) != NULL)
  {
    limit = ippGetInteger(attr, 0);

    fprintf(stderr, "%s Get-Jobs limit=%d", client->hostname, limit);
  }
  else
  {
    limit = 0;
  }

  if ((attr = ippFindAttribute(client->request, "first-index", IPP_TAG_INTEGER)) != NULL && (first_index = ippGetInteger(attr, 0)) >= 1)
  {
    first_i = (size_t)first_index - 1;

    fprintf(stderr, "%s Get-Jobs first-index=%d", client->hostname, first_index);
  }
  else
  {
    first_i = 0;
  }

  // See if we only want to see jobs for a specific user...
  username = NULL;

  if ((attr = ippFindAttribute(client->request, "my-jobs", IPP_TAG_BOOLEAN)) != NULL)
  {
    int my_jobs = ippGetBoolean(attr, 0);

    fprintf(stderr, "%s Get-Jobs my-jobs=%s\n", client->hostname, my_jobs ? "true" : "false");

    if (my_jobs)
    {
      if ((attr = ippFindAttribute(client->request, "requesting-user-name", IPP_TAG_NAME)) == NULL)
      {
	respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Need requesting-user-name with my-jobs.");
	return;
      }

      username = ippGetString(attr, 0, NULL);

      fprintf(stderr, "%s Get-Jobs requesting-user-name=\"%s\"\n", client->hostname, username);
    }
  }

  // OK, build a list of jobs for this printer...
  ra = ippCreateRequestedArray(client->request);

  respond_ipp(client, IPP_STATUS_OK, NULL);

  cupsRWLockRead(&(client->printer->rwlock));

  for (i = 0, count = cupsArrayGetCount(client->printer->jobs), matches = 0; i < count && (limit <= 0 || matches < limit); i ++)
  {
    // Filter out jobs that don't match...
    job = (ippeve_job_t *)cupsArrayGetElement(client->printer->jobs, i);

    if ((job_comparison < 0 && job->state > job_state) ||
	(job_comparison == 0 && job->state != job_state) ||
	(job_comparison > 0 && job->state < job_state) ||
	i < first_i ||
	(username && job->username && strcasecmp(username, job->username)))
      continue;

    if (matches > 0)
      ippAddSeparator(client->response);

    matches ++;
    copy_job_attributes(client, job, ra);
  }

  cupsArrayDelete(ra);

  cupsRWUnlock(&(client->printer->rwlock));
}


//
// 'ipp_get_printer_attributes()' - Get the attributes for a printer object.
//

static void
ipp_get_printer_attributes(
    ippeve_client_t *client)		// I - Client
{
  cups_array_t		*ra;		// Requested attributes array
  ippeve_printer_t	*printer;	// Printer


  // Send the attributes...
  ra      = ippCreateRequestedArray(client->request);
  printer = client->printer;

  respond_ipp(client, IPP_STATUS_OK, NULL);

  cupsRWLockRead(&(printer->rwlock));

  copy_attributes(client->response, printer->attrs, ra, IPP_TAG_ZERO,
		  IPP_TAG_CUPS_CONST);

  if (!ra || cupsArrayFind(ra, "printer-config-change-date-time"))
    ippAddDate(client->response, IPP_TAG_PRINTER, "printer-config-change-date-time", ippTimeToDate(printer->config_time));

  if (!ra || cupsArrayFind(ra, "printer-config-change-time"))
    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "printer-config-change-time", (int)(printer->config_time - printer->start_time));

  if (!ra || cupsArrayFind(ra, "printer-current-time"))
    ippAddDate(client->response, IPP_TAG_PRINTER, "printer-current-time", ippTimeToDate(time(NULL)));

  if (!ra || cupsArrayFind(ra, "printer-dns-sd-name"))
    ippAddString(client->response, IPP_TAG_PRINTER, IPP_TAG_NAME, "printer-dns-sd-name", /*language*/NULL, printer->dnssd_name);

  if (!ra || cupsArrayFind(ra, "printer-icons"))
  {
    char	uris[3][1024];		// Buffers for URIs
    const char	*values[3];		// Values for attribute

    httpAssembleURI(HTTP_URI_CODING_ALL, uris[0], sizeof(uris[0]), WEB_SCHEME, NULL, client->host_field, client->host_port, "/icon-sm.png");
    httpAssembleURI(HTTP_URI_CODING_ALL, uris[1], sizeof(uris[1]), WEB_SCHEME, NULL, client->host_field, client->host_port, "/icon.png");
    httpAssembleURI(HTTP_URI_CODING_ALL, uris[2], sizeof(uris[2]), WEB_SCHEME, NULL, client->host_field, client->host_port, "/icon-lg.png");

    values[0] = uris[0];
    values[1] = uris[1];
    values[2] = uris[2];

    ippAddStrings(client->response, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-icons", 3, NULL, values);
  }

  if (!ra || cupsArrayFind(ra, "printer-more-info"))
  {
    char	uri[1024];		// URI value

    httpAssembleURI(HTTP_URI_CODING_ALL, uri, sizeof(uri), WEB_SCHEME, NULL, client->host_field, client->host_port, "/");
    ippAddString(client->response, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-more-info", NULL, uri);
  }

  if (!ra || cupsArrayFind(ra, "printer-state"))
    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_ENUM, "printer-state", (int)printer->state);

  if (!ra || cupsArrayFind(ra, "printer-state-change-date-time"))
    ippAddDate(client->response, IPP_TAG_PRINTER, "printer-state-change-date-time", ippTimeToDate(printer->state_time));

  if (!ra || cupsArrayFind(ra, "printer-state-change-time"))
    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "printer-state-change-time", (int)(printer->state_time - printer->start_time));

  if (!ra || cupsArrayFind(ra, "printer-state-message"))
  {
    static const char * const messages[] = { "Idle.", "Printing.", "Stopped." };

    ippAddString(client->response, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-state-message", NULL, messages[printer->state - IPP_PSTATE_IDLE]);
  }

  if (!ra || cupsArrayFind(ra, "printer-state-reasons"))
  {
    ipp_attribute_t	*attr = NULL;		// printer-state-reasons
    ippeve_preason_t	bit;			// Reason bit
    int			i;			// Looping var
    char		reason[128];		// Reason string

    for (i = 0, bit = 1; i < (int)(sizeof(ippeve_preason_strings) / sizeof(ippeve_preason_strings[0])); i ++, bit *= 2)
    {
      if (printer->state_reasons & bit)
      {
	snprintf(reason, sizeof(reason), "%s-%s", ippeve_preason_strings[i], printer->state == IPP_PSTATE_IDLE ? "report" : printer->state == IPP_PSTATE_PROCESSING ? "warning" : "error");
	if (attr)
	  ippSetString(client->response, &attr, ippGetCount(attr), reason);
	else
	  attr = ippAddString(client->response, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "printer-state-reasons", NULL, reason);
      }
    }
    for (i = 0; i < (int)(sizeof(printer->state_keywords) / sizeof(printer->state_keywords[0])); i ++)
    {
      if (!printer->state_keywords[i][0])
        continue;

      snprintf(reason, sizeof(reason), "%s-%s", printer->state_keywords[i], printer->state == IPP_PSTATE_IDLE ? "report" : printer->state == IPP_PSTATE_PROCESSING ? "warning" : "error");
      if (attr)
	ippSetString(client->response, &attr, ippGetCount(attr), reason);
      else
	attr = ippAddString(client->response, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "printer-state-reasons", NULL, reason);
    }

    if (!attr)
      ippAddString(client->response, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "printer-state-reasons", NULL, "none");
  }

  if (!ra || cupsArrayFind(ra, "printer-strings-uri"))
  {
    char	uri[1024];		// URI value

    httpAssembleURI(HTTP_URI_CODING_ALL, uri, sizeof(uri), WEB_SCHEME, NULL, client->host_field, client->host_port, "/en.strings");
    ippAddString(client->response, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-strings-uri", NULL, uri);
  }

  if (!ra || cupsArrayFind(ra, "printer-supply-info-uri"))
  {
    char	uri[1024];		// URI value

    httpAssembleURI(HTTP_URI_CODING_ALL, uri, sizeof(uri), WEB_SCHEME, NULL, client->host_field, client->host_port, "/supplies");
    ippAddString(client->response, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-supply-info-uri", NULL, uri);
  }

  if (!ra || cupsArrayFind(ra, "printer-up-time"))
    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "printer-up-time", (int)(time(NULL) - printer->start_time));

  if (!ra || cupsArrayFind(ra, "printer-uri-supported"))
  {
    char	uris[2][1024];		// Buffers for URIs
    const char	*values[2];		// Values for attribute
    size_t	num_values = 0;		// Number of values

    httpAssembleURI(HTTP_URI_CODING_ALL, uris[0], sizeof(uris[0]), "ipp", NULL, client->host_field, client->host_port, "/ipp/print");
    values[num_values ++] = uris[0];

    httpAssembleURI(HTTP_URI_CODING_ALL, uris[1], sizeof(uris[1]), "ipps", NULL, client->host_field, client->host_port, "/ipp/print");
    values[num_values ++] = uris[1];

    ippAddStrings(client->response, IPP_TAG_PRINTER, IPP_TAG_URI, "printer-uri-supported", num_values, NULL, values);
  }

  if (!ra || cupsArrayFind(ra, "queued-job-count"))
    ippAddInteger(client->response, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "queued-job-count", printer->active_job && printer->active_job->state < IPP_JSTATE_CANCELED);

  cupsRWUnlock(&(printer->rwlock));

  cupsArrayDelete(ra);
}


//
// 'ipp_identify_printer()' - Beep or display a message.
//

static void
ipp_identify_printer(
    ippeve_client_t *client)		// I - Client
{
  ipp_attribute_t	*actions,	// identify-actions
			*message;	// message


  actions = ippFindAttribute(client->request, "identify-actions", IPP_TAG_KEYWORD);
  message = ippFindAttribute(client->request, "message", IPP_TAG_TEXT);

  if (!actions || ippContainsString(actions, "sound"))
  {
#ifdef __APPLE__
    pid_t	pid;			// Process ID for "afplay" utility
    static const char * const afplay[3] =
    {					// Arguments for "afplay" utility
      "/usr/bin/afplay",
      "/System/Library/Sounds/Ping.aiff",
      NULL
    };

    posix_spawn(&pid, afplay[0], NULL, NULL, (char **)afplay, NULL);

#else
    putchar(0x07);
    fflush(stdout);
#endif // __APPLE__
  }

  if (ippContainsString(actions, "display"))
    printf("IDENTIFY from %s: %s\n", client->hostname, message ? ippGetString(message, 0, NULL) : "No message supplied");

  respond_ipp(client, IPP_STATUS_OK, NULL);
}


//
// 'ipp_print_job()' - Create a job object with an attached document.
//

static void
ipp_print_job(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// New job


  // Validate print job attributes...
  if (!valid_job_attributes(client))
  {
    flush_document_data(client);
    return;
  }

  // Do we have a file to print?
  if (!have_document_data(client))
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "No file in request.");
    return;
  }

  // Create the job...
  if ((job = create_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BUSY, "Currently printing another job.");
    return;
  }

  // Then finish getting the document data and process things...
  finish_document_data(client, job);
}


//
// 'ipp_print_uri()' - Create a job object with a referenced document.
//

static void
ipp_print_uri(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// New job


  // Validate print job attributes...
  if (!valid_job_attributes(client))
  {
    flush_document_data(client);
    return;
  }

  // Create the job...
  if ((job = create_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BUSY, "Currently printing another job.");
    return;
  }

  // Then finish getting the document data and process things...
  finish_document_uri(client, job);
}


//
// 'ipp_send_document()' - Add an attached document to a job object created with
//                         Create-Job.
//

static void
ipp_send_document(
    ippeve_client_t *client)		// I - Client
{
  ippeve_job_t		*job;		// Job information
  ipp_attribute_t	*attr;		// Current attribute
  int			have_data;	// Have document data?


  // Get the job...
  if ((job = find_job(client)) == NULL)
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
    return;
  }

  // See if we already have a document for this job or the job has already in a terminating state...
  have_data = have_document_data(client);

  if ((job->filename || job->fd >= 0) && have_data)
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_MULTIPLE_JOBS_NOT_SUPPORTED, "Multiple document jobs are not supported.");
    return;
  }
  else if (job->state > IPP_JSTATE_HELD && have_data)
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Job is not in a pending state.");
    return;
  }

  // Make sure we have the "last-document" operation attribute...
  if ((attr = ippFindAttribute(client->request, "last-document", IPP_TAG_ZERO)) == NULL)
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Missing required last-document attribute.");
    return;
  }
  else if (ippGetGroupTag(attr) != IPP_TAG_OPERATION)
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "The last-document attribute is not in the operation group.");
    return;
  }
  else if (ippGetValueTag(attr) != IPP_TAG_BOOLEAN || ippGetCount(attr) != 1)
  {
    flush_document_data(client);
    respond_unsupported(client, attr);
    return;
  }

  // Validate document attributes...
  if (have_data && !valid_doc_attributes(client))
  {
    flush_document_data(client);
    return;
  }

  if (!have_data && !job->filename)
    job->state = IPP_JSTATE_ABORTED;

  // Then finish getting the document data and process things...
  cupsRWLockWrite(&(client->printer->rwlock));

  copy_attributes(job->attrs, client->request, NULL, IPP_TAG_JOB, 0);

  if ((attr = ippFindAttribute(job->attrs, "document-format-detected", IPP_TAG_MIMETYPE)) != NULL)
    job->format = ippGetString(attr, 0, NULL);
  else if ((attr = ippFindAttribute(job->attrs, "document-format-supplied", IPP_TAG_MIMETYPE)) != NULL)
    job->format = ippGetString(attr, 0, NULL);
  else
    job->format = "application/octet-stream";

  cupsRWUnlock(&(client->printer->rwlock));

  if (have_data)
    finish_document_data(client, job);
}


//
// 'ipp_send_uri()' - Add a referenced document to a job object created with
//                    Create-Job.
//

static void
ipp_send_uri(ippeve_client_t *client)	// I - Client
{
  ippeve_job_t		*job;		// Job information
  ipp_attribute_t	*attr;		// Current attribute


  // Get the job...
  if ((job = find_job(client)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "Job does not exist.");
    return;
  }

  // See if we already have a document for this job or the job has already in a non-terminating state...
  if (job->filename || job->fd >= 0)
  {
    respond_ipp(client, IPP_STATUS_ERROR_MULTIPLE_JOBS_NOT_SUPPORTED, "Multiple document jobs are not supported.");
    return;
  }
  else if (job->state > IPP_JSTATE_HELD)
  {
    flush_document_data(client);
    respond_ipp(client, IPP_STATUS_ERROR_NOT_POSSIBLE, "Job is not in a pending state.");
    return;
  }

  if ((attr = ippFindAttribute(client->request, "last-document", IPP_TAG_ZERO)) == NULL)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Missing required last-document attribute.");
    return;
  }
  else if (ippGetGroupTag(attr) != IPP_TAG_OPERATION)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "The last-document attribute is not in the operation group.");
    return;
  }
  else if (ippGetValueTag(attr) != IPP_TAG_BOOLEAN || ippGetCount(attr) != 1)
  {
    respond_unsupported(client, attr);
    return;
  }

  // Validate document attributes...
  if (!valid_doc_attributes(client))
  {
    flush_document_data(client);
    return;
  }

  // Then finish getting the document data and process things...
  cupsRWLockWrite(&(client->printer->rwlock));

  copy_attributes(job->attrs, client->request, NULL, IPP_TAG_JOB, 0);

  if ((attr = ippFindAttribute(job->attrs, "document-format-detected", IPP_TAG_MIMETYPE)) != NULL)
    job->format = ippGetString(attr, 0, NULL);
  else if ((attr = ippFindAttribute(job->attrs, "document-format-supplied", IPP_TAG_MIMETYPE)) != NULL)
    job->format = ippGetString(attr, 0, NULL);
  else
    job->format = "application/octet-stream";

  cupsRWUnlock(&(client->printer->rwlock));

  finish_document_uri(client, job);
}


//
// 'ipp_validate_job()' - Validate job creation attributes.
//

static void
ipp_validate_job(ippeve_client_t *client)	// I - Client
{
  if (valid_job_attributes(client))
    respond_ipp(client, IPP_STATUS_OK, NULL);
}


//
// 'ippfile_attr_cb()' - Determine whether an attribute should be loaded.
//

static bool				// O - `true` to use, `false` to ignore
ippfile_attr_cb(
    ipp_file_t *f,			// I - IPP file
    void       *user_data,		// I - User data pointer (unused)
    const char *attr)			// I - Attribute name
{
  size_t	i;			// Current element
  static const char * const ignored[] =
  {					// Ignored attributes
    "attributes-charset",
    "attributes-natural-language",
    "charset-configured",
    "charset-supported",
    "device-service-count",
    "device-uuid",
    "document-format-varying-attributes",
    "generated-natural-language-supported",
    "identify-actions-default",
    "identify-actions-supported",
    "ipp-features-supported",
    "ipp-versions-supported",
    "ippget-event-life",
    "job-creation-attributes-supported",
    "job-hold-until-supported",
    "job-hold-until-time-supported",
    "job-ids-supported",
    "job-k-octets-supported",
    "job-priority-default",
    "job-priority-supported",
    "job-settable-attributes-supported",
    "job-sheets-default",
    "job-sheets-supported",
    "max-page-ranges-supported",
    "media-col-supported",
    "multiple-document-handling-supported",
    "multiple-document-jobs-supported",
    "multiple-operation-time-out",
    "multiple-operation-time-out-action",
    "natural-language-configured",
    "notify-attributes-supported",
    "notify-events-default",
    "notify-events-supported",
    "notify-lease-duration-default",
    "notify-lease-duration-supported",
    "notify-max-events-supported",
    "notify-pull-method-supported",
    "operations-supported",
    "overrides-supported",
    "page-ranges-supported",
    "pdl-override-supported",
    "preferred-attributes-supported",
    "printer-alert",
    "printer-alert-description",
    "printer-camera-image-uri",
    "printer-charge-info",
    "printer-charge-info-uri",
    "printer-config-change-date-time",
    "printer-config-change-time",
    "printer-current-time",
    "printer-detailed-status-messages",
    "printer-dns-sd-name",
    "printer-fax-log-uri",
    "printer-geo-location",
    "printer-get-attributes-supported",
    "printer-icons",
    "printer-id",
    "printer-info",
    "printer-is-accepting-jobs",
    "printer-location",
    "printer-message-date-time",
    "printer-message-from-operator",
    "printer-message-time",
    "printer-more-info",
    "printer-name",
    "printer-organization",
    "printer-organizational-unit",
    "printer-service-type",
    "printer-settable-attributes-supported",
    "printer-state",
    "printer-state-change-date-time",
    "printer-state-change-time",
    "printer-state-message",
    "printer-state-reasons",
    "printer-static-resource-directory-uri",
    "printer-static-resource-k-octets-free",
    "printer-static-resource-k-octets-supported",
    "printer-strings-languages-supported",
    "printer-strings-uri",
    "printer-supply-info-uri",
    "printer-up-time",
    "printer-uri-supported",
    "printer-uuid",
    "printer-xri-supported",
    "queued-job-count",
    "reference-uri-schemes-supported",
    "requesting-user-uri-supported",
    "uri-authentication-supported",
    "uri-security-supported",
    "which-jobs-supported",
    "xri-authentication-supported",
    "xri-security-supported",
    "xri-uri-scheme-supported"
  };


  (void)f;
  (void)user_data;

  for (i = 0; i < (sizeof(ignored) / sizeof(ignored[0])); i ++)
  {
    if (!strcmp(attr, ignored[i]))
      return (false);
  }

  return (true);
}


//
// 'ippfile_error_cb()' - Log an error message.
//

static bool				// O - `true` to continue, `false` to stop
ippfile_error_cb(
    ipp_file_t *f,			// I - IPP file data
    void       *user_data,		// I - User data pointer (unused)
    const char *error)			// I - Error message
{
  (void)f;
  (void)user_data;

  cupsLangPrintf(stderr, "%s\n", error);

  return (false);
}


//
// 'load_ippfile_attributes()' - Load IPP attributes from an ippserver file.
//

static ipp_t *				// O - IPP attributes or `NULL` on error
load_ippfile_attributes(
    const char   *servername,		// I - Server name or `NULL` for default
    int          serverport,		// I - Server port number
    const char   *filename,		// I - ippserver attribute filename
    cups_array_t *docformats)		// I - document-format-supported values
{
  ipp_file_t	*file;			// IPP data file
  ipp_t		*attrs;			// IPP attributes
  char		temp[256];		// Temporary string


  (void)docformats; // for now

  // Setup callbacks and variables for the printer configuration file...
  //
  // The following additional variables are supported:
  //
  // - SERVERNAME: The host name of the server.
  // - SERVERPORT: The default port of the server.
  attrs = ippNew();
  file  = ippFileNew(NULL, (ipp_fattr_cb_t)ippfile_attr_cb, (ipp_ferror_cb_t)ippfile_error_cb, NULL);

  ippFileSetAttributes(file, attrs);
  ippFileSetGroupTag(file, IPP_TAG_PRINTER);

  if (servername)
  {
    ippFileSetVar(file, "SERVERNAME", servername);
  }
  else
  {
    httpGetHostname(NULL, temp, sizeof(temp));
    ippFileSetVar(file, "SERVERNAME", temp);
  }

  snprintf(temp, sizeof(temp), "%d", serverport);
  ippFileSetVar(file, "SERVERPORT", temp);

  // Load attributes and values for the printer...
  ippFileOpen(file, filename, "r");
  ippFileRead(file, NULL, false);

  // Free memory and return...
  ippFileDelete(file);

  return (attrs);
}


//
// 'load_legacy_attributes()' - Load IPP attributes using the old ippserver
//                              options.
//

static ipp_t *				// O - IPP attributes or `NULL` on error
load_legacy_attributes(
    const char   *make,			// I - Manufacturer name
    const char   *model,		// I - Model name
    int          ppm,			// I - pages-per-minute
    int          ppm_color,		// I - pages-per-minute-color
    int          duplex,		// I - Duplex support?
    cups_array_t *docformats)		// I - document-format-supported values
{
  size_t		i;		// Looping var
  ipp_t			*attrs,		// IPP attributes
			*col;		// Collection value
  ipp_attribute_t	*attr;		// Current attribute
  char			device_id[1024],// printer-device-id
			*ptr,		// Pointer into device ID
			make_model[128];// printer-make-and-model
  const char		*format,	// Current document format
			*prefix;	// Prefix for device ID
  size_t		num_media;	// Number of media
  const char * const	*media;		// List of media
  size_t		num_ready;	// Number of loaded media
  const char * const	*ready;		// List of loaded media
  pwg_media_t		*pwg;		// PWG media size information
  static const char * const media_supported[] =
  {					// media-supported values
    "na_letter_8.5x11in",		// Letter
    "na_legal_8.5x14in",		// Legal
    "iso_a4_210x297mm",			// A4
    "na_number-10_4.125x9.5in",		// #10 Envelope
    "iso_dl_110x220mm"			// DL Envelope
  };
  static const char * const media_supported_color[] =
  {					// media-supported values
    "na_letter_8.5x11in",		// Letter
    "na_legal_8.5x14in",		// Legal
    "iso_a4_210x297mm",			// A4
    "na_number-10_4.125x9.5in",		// #10 Envelope
    "iso_dl_110x220mm",			// DL Envelope
    "na_index-3x5_3x5in",		// Photo 3x5
    "oe_photo-l_3.5x5in",		// Photo L
    "na_index-4x6_4x6in",		// Photo 4x6
    "iso_a6_105x148mm",			// A6
    "na_5x7_5x7in",			// Photo 5x7 aka 2L
    "iso_a5_148x210mm",			// A5
    "roll_min_4x1in",			// Roll
    "roll_max_8.5x39.6in"		// Roll
  };
  static const char * const media_ready[] =
  {					// media-ready values
    "na_letter_8.5x11in",		// Letter
    "na_number-10_4.125x9.5in"		// #10
  };
  static const char * const media_ready_color[] =
  {					// media-ready values
    "na_letter_8.5x11in",		// Letter
    "na_index-4x6_4x6in",		// Photo 4x6
    "roll_current_8.5x0in"		// 8.5" roll
  };
  static const char * const media_source_supported[] =
  {					// media-source-supported values
    "auto",
    "main",
    "manual",
    "by-pass-tray"			// AKA multi-purpose tray
  };
  static const char * const media_source_supported_color[] =
  {					// media-source-supported values
    "auto",
    "main",
    "photo",
    "roll"
  };
  static const char * const media_type_supported[] =
  {					// media-type-supported values
    "auto",
    "cardstock",
    "envelope",
    "labels",
    "other",
    "stationery",
    "stationery-letterhead",
    "transparency"
  };
  static const char * const media_type_supported_color[] =
  {					// media-type-supported values
    "auto",
    "cardstock",
    "envelope",
    "labels",
    "other",
    "stationery",
    "stationery-letterhead",
    "transparency",
    "photographic-glossy",
    "photographic-high-gloss",
    "photographic-matte",
    "photographic-satin",
    "photographic-semi-gloss"
  };
  static const int	media_bottom_margin_supported[] =
  {					// media-bottom-margin-supported values
    635					// 1/4"
  };
  static const int	media_bottom_margin_supported_color[] =
  {					// media-bottom/top-margin-supported values
    0,					// Borderless
    1168				// 0.46" (common HP inkjet bottom margin)
  };
  static const int	media_lr_margin_supported[] =
  {					// media-left/right-margin-supported values
    340,				// 3.4mm (historical HP PCL A4 margin)
    635					// 1/4"
  };
  static const int	media_lr_margin_supported_color[] =
  {					// media-left/right-margin-supported values
    0,					// Borderless
    340,				// 3.4mm (historical HP PCL A4 margin)
    635					// 1/4"
  };
  static const int	media_top_margin_supported[] =
  {					// media-top-margin-supported values
    635					// 1/4"
  };
  static const int	media_top_margin_supported_color[] =
  {					// media-top/top-margin-supported values
    0,					// Borderless
    102					// 0.04" (common HP inkjet top margin
  };
  static const int	orientation_requested_supported[4] =
  {					// orientation-requested-supported values
    IPP_ORIENT_PORTRAIT,
    IPP_ORIENT_LANDSCAPE,
    IPP_ORIENT_REVERSE_LANDSCAPE,
    IPP_ORIENT_REVERSE_PORTRAIT
  };
  static const char * const overrides_supported[] =
  {					// overrides-supported values
    "document-numbers",
    "media",
    "media-col",
    "orientation-requested",
    "pages"
  };
  static const char * const print_color_mode_supported[] =
  {					// print-color-mode-supported values
    "monochrome"
  };
  static const char * const print_color_mode_supported_color[] =
  {					// print-color-mode-supported values
    "auto",
    "color",
    "monochrome"
  };
  static const int	print_quality_supported[] =
  {					// print-quality-supported values
    IPP_QUALITY_DRAFT,
    IPP_QUALITY_NORMAL,
    IPP_QUALITY_HIGH
  };
  static const char * const printer_input_tray[] =
  {					// printer-input-tray values
    "type=sheetFeedAutoRemovableTray;mediafeed=0;mediaxfeed=0;maxcapacity=-2;level=-2;status=0;name=auto",
    "type=sheetFeedAutoRemovableTray;mediafeed=0;mediaxfeed=0;maxcapacity=250;level=100;status=0;name=main",
    "type=sheetFeedManual;mediafeed=0;mediaxfeed=0;maxcapacity=1;level=-2;status=0;name=manual",
    "type=sheetFeedAutoNonRemovableTray;mediafeed=0;mediaxfeed=0;maxcapacity=25;level=-2;status=0;name=by-pass-tray"
  };
  static const char * const printer_input_tray_color[] =
  {					// printer-input-tray values
    "type=sheetFeedAutoRemovableTray;mediafeed=0;mediaxfeed=0;maxcapacity=-2;level=-2;status=0;name=auto",
    "type=sheetFeedAutoRemovableTray;mediafeed=0;mediaxfeed=0;maxcapacity=250;level=-2;status=0;name=main",
    "type=sheetFeedAutoRemovableTray;mediafeed=0;mediaxfeed=0;maxcapacity=25;level=-2;status=0;name=photo",
    "type=continuousRoll;mediafeed=0;mediaxfeed=0;maxcapacity=100;level=-2;status=0;name=roll"
  };
  static const char * const printer_supply[] =
  {					// printer-supply values
    "index=1;class=receptacleThatIsFilled;type=wasteToner;unit=percent;"
        "maxcapacity=100;level=25;colorantname=unknown;",
    "index=2;class=supplyThatIsConsumed;type=toner;unit=percent;"
        "maxcapacity=100;level=75;colorantname=black;"
  };
  static const char * const printer_supply_color[] =
  {					// printer-supply values
    "index=1;class=receptacleThatIsFilled;type=wasteInk;unit=percent;"
        "maxcapacity=100;level=25;colorantname=unknown;",
    "index=2;class=supplyThatIsConsumed;type=ink;unit=percent;"
        "maxcapacity=100;level=75;colorantname=black;",
    "index=3;class=supplyThatIsConsumed;type=ink;unit=percent;"
        "maxcapacity=100;level=50;colorantname=cyan;",
    "index=4;class=supplyThatIsConsumed;type=ink;unit=percent;"
        "maxcapacity=100;level=33;colorantname=magenta;",
    "index=5;class=supplyThatIsConsumed;type=ink;unit=percent;"
        "maxcapacity=100;level=67;colorantname=yellow;"
  };
  static const char * const printer_supply_description[] =
  {					// printer-supply-description values
    "Toner Waste Tank",
    "Black Toner"
  };
  static const char * const printer_supply_description_color[] =
  {					// printer-supply-description values
    "Ink Waste Tank",
    "Black Ink",
    "Cyan Ink",
    "Magenta Ink",
    "Yellow Ink"
  };
  static const int	pwg_raster_document_resolution_supported[] =
  {
    300,
    600
  };
  static const char * const pwg_raster_document_type_supported[] =
  {
    "black_1",
    "sgray_8"
  };
  static const char * const pwg_raster_document_type_supported_color[] =
  {
    "black_1",
    "sgray_8",
    "srgb_8",
    "srgb_16"
  };
  static const char * const sides_supported[] =
  {					// sides-supported values
    "one-sided",
    "two-sided-long-edge",
    "two-sided-short-edge"
  };
  static const char * const urf_supported[] =
  {					// urf-supported values
    "CP1",
    "IS1-4-5-19",
    "MT1-2-3-4-5-6",
    "RS600",
    "V1.4",
    "W8"
  };
  static const char * const urf_supported_color[] =
  {					// urf-supported values
    "CP1",
    "IS1-4-5-7-19",
    "MT1-2-3-4-5-6-8-9-10-11-12-13",
    "RS600",
    "SRGB24",
    "V1.4",
    "W8"
  };
  static const char * const urf_supported_color_duplex[] =
  {					// urf-supported values
    "CP1",
    "IS1-4-5-7-19",
    "MT1-2-3-4-5-6-8-9-10-11-12-13",
    "RS600",
    "SRGB24",
    "V1.4",
    "W8",
    "DM3"
  };
  static const char * const urf_supported_duplex[] =
  {					// urf-supported values
    "CP1",
    "IS1-4-5-19",
    "MT1-2-3-4-5-6",
    "RS600",
    "V1.4",
    "W8",
    "DM1"
  };


  attrs = ippNew();

  if (ppm_color > 0)
  {
    num_media = (int)(sizeof(media_supported_color) / sizeof(media_supported_color[0]));
    media     = media_supported_color;
    num_ready = (int)(sizeof(media_ready_color) / sizeof(media_ready_color[0]));
    ready     = media_ready_color;
  }
  else
  {
    num_media = (int)(sizeof(media_supported) / sizeof(media_supported[0]));
    media     = media_supported;
    num_ready = (int)(sizeof(media_ready) / sizeof(media_ready[0]));
    ready     = media_ready;
  }

  // color-supported
  ippAddBoolean(attrs, IPP_TAG_PRINTER, "color-supported", ppm_color > 0);

  // copies-default
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "copies-default", 1);

  // copies-supported
  ippAddRange(attrs, IPP_TAG_PRINTER, "copies-supported", 1, (cupsArrayFind(docformats, (void *)"application/pdf") != NULL || cupsArrayFind(docformats, (void *)"image/jpeg") != NULL) ? 999 : 1);

  // document-password-supported
  if (cupsArrayFind(docformats, (void *)"application/pdf"))
    ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "document-password-supported", 1023);

  // finishing-template-supported
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "finishing-template-supported", NULL, "none");

  // finishings-col-database
  col = ippNew();
  ippAddString(col, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "finishing-template", NULL, "none");
  ippAddCollection(attrs, IPP_TAG_PRINTER, "finishings-col-database", col);
  ippDelete(col);

  // finishings-col-default
  col = ippNew();
  ippAddString(col, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "finishing-template", NULL, "none");
  ippAddCollection(attrs, IPP_TAG_PRINTER, "finishings-col-default", col);
  ippDelete(col);

  // finishings-col-ready
  col = ippNew();
  ippAddString(col, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "finishing-template", NULL, "none");
  ippAddCollection(attrs, IPP_TAG_PRINTER, "finishings-col-ready", col);
  ippDelete(col);

  // finishings-col-supported
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "finishings-col-supported", NULL, "finishing-template");

  // finishings-default
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-default", IPP_FINISHINGS_NONE);

  // finishings-ready
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-ready", IPP_FINISHINGS_NONE);

  // finishings-supported
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-supported", IPP_FINISHINGS_NONE);

  // max-page-ranges-supported
  if (!ippFindAttribute(attrs, "max-page-ranges-supported", IPP_TAG_ZERO) && cupsArrayFind(docformats, (void *)"application/pdf"))
    ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "max-page-ranges-supported", 1);

  // media-bottom-margin-supported
  if (ppm_color > 0)
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-bottom-margin-supported", (int)(sizeof(media_bottom_margin_supported) / sizeof(media_bottom_margin_supported[0])), media_bottom_margin_supported);
  else
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-bottom-margin-supported", (int)(sizeof(media_bottom_margin_supported_color) / sizeof(media_bottom_margin_supported_color[0])), media_bottom_margin_supported_color);

  // media-col-database and media-col-default
  for (i = 0, attr = NULL; i < num_media; i ++)
  {
    int		bottom, left,		// media-xxx-margins
		right, top;
    const char	*source;		// media-source, if any

    if ((pwg = pwgMediaForPWG(media[i])) == NULL)
      continue;

    if (pwg->width < 21000 && pwg->length < 21000)
    {
      source = "photo";			// Photo size media from photo tray
      bottom =				// Borderless margins
      left   =
      right  =
      top    = 0;
    }
    else if (pwg->width < 21000)
    {
      source = "by-pass-tray";		// Envelopes from multi-purpose tray
      bottom = ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0];
      left   =				// Left/right margins are standard
      right  = media_lr_margin_supported[1];
      top    = ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0];
    }
    else if (pwg->width == 21000)
    {
      source = NULL;			// A4 from any tray
      bottom = ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0];
      left   =				// Left/right margins are reduced
      right  = media_lr_margin_supported[0];
      top    = ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0];
    }
    else
    {
      source = NULL;			// Other size media from any tray
      bottom = ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0];
      left   =				// Left/right margins are standard
      right  = media_lr_margin_supported[1];
      top    = ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0];
    }

    if (!strncmp(media[i], "roll_min_", 9) && i < (num_media - 1))
    {
      // Roll min/max range...
      pwg_media_t	*pwg2;		// Max size
      ipp_t		*media_size;	// media-size member attribute

      i ++;
      if ((pwg2 = pwgMediaForPWG(media[i])) == NULL)
        continue;

      media_size = ippNew();
      ippAddRange(media_size, IPP_TAG_ZERO, "x-dimension", pwg->width, pwg2->width);
      ippAddRange(media_size, IPP_TAG_ZERO, "y-dimension", pwg->length, pwg2->length);

      col = create_media_col(NULL, source, NULL, media_size, bottom, left, right, top);
    }
    else
    {
      // Sheet size
      col = create_media_col(media[i], source, NULL, create_media_size(pwg->width, pwg->length), bottom, left, right, top);
    }

    if (attr)
      ippSetCollection(attrs, &attr, ippGetCount(attr), col);
    else
      attr = ippAddCollection(attrs, IPP_TAG_PRINTER, "media-col-database", col);

    ippDelete(col);
  }

  // media-col-default
  if ((pwg = pwgMediaForPWG(ready[0])) != NULL)
  {
    if (pwg->width == 21000)
      col = create_media_col(ready[0], "main", "stationery", create_media_size(pwg->width, pwg->length), ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0], media_lr_margin_supported[0], media_lr_margin_supported[0], ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0]);
    else
      col = create_media_col(ready[0], "main", "stationery", create_media_size(pwg->width, pwg->length), ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0], media_lr_margin_supported[1], media_lr_margin_supported[1], ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0]);

    ippAddCollection(attrs, IPP_TAG_PRINTER, "media-col-default", col);

    ippDelete(col);
  }

  // media-col-ready
  for (i = 0, attr = NULL; i < num_ready; i ++)
  {
    int		bottom, left,		// media-xxx-margins
		right, top;
    const char	*source,		// media-source
		*type;			// media-type

    if ((pwg = pwgMediaForPWG(ready[i])) == NULL)
      continue;

    if (pwg->width < 21000 && pwg->length < 21000)
    {
      source = "photo";			// Photo size media from photo tray
      type   = "photographic-glossy";	// Glossy photo paper
      bottom =				// Borderless margins
      left   =
      right  =
      top    = 0;
    }
    else if (pwg->width < 21000)
    {
      source = "by-pass-tray";		// Envelopes from multi-purpose tray
      type   = "envelope";		// Envelope
      bottom = ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0];
      left   =				// Left/right margins are standard
      right  = media_lr_margin_supported[1];
      top    = ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0];
    }
    else if (pwg->width == 21000)
    {
      source = "main";			// A4 from main tray
      type   = "stationery";		// Plain paper
      bottom = ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0];
      left   =				// Left/right margins are reduced
      right  = media_lr_margin_supported[0];
      top    = ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0];
    }
    else
    {
      source = "main";			// A4 from main tray
      type   = "stationery";		// Plain paper
      bottom = ppm_color > 0 ? media_bottom_margin_supported_color[1] : media_bottom_margin_supported[0];
      left   =				// Left/right margins are standard
      right  = media_lr_margin_supported[1];
      top    = ppm_color > 0 ? media_top_margin_supported_color[1] : media_top_margin_supported[0];
    }

    col = create_media_col(ready[i], source, type, create_media_size(pwg->width, pwg->length), bottom, left, right, top);

    if (attr)
      ippSetCollection(attrs, &attr, ippGetCount(attr), col);
    else
      attr = ippAddCollection(attrs, IPP_TAG_PRINTER, "media-col-ready", col);

    ippDelete(col);
  }

  // media-default
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-default", NULL, media[0]);

  // media-left/right-margin-supported
  if (ppm_color > 0)
  {
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-left-margin-supported", (int)(sizeof(media_lr_margin_supported_color) / sizeof(media_lr_margin_supported_color[0])), media_lr_margin_supported_color);
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-right-margin-supported", (int)(sizeof(media_lr_margin_supported_color) / sizeof(media_lr_margin_supported_color[0])), media_lr_margin_supported_color);
  }
  else
  {
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-left-margin-supported", (int)(sizeof(media_lr_margin_supported) / sizeof(media_lr_margin_supported[0])), media_lr_margin_supported);
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-right-margin-supported", (int)(sizeof(media_lr_margin_supported) / sizeof(media_lr_margin_supported[0])), media_lr_margin_supported);
  }

  // media-ready
  ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "media-ready", num_ready, NULL, ready);

  // media-supported
  ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-supported", num_media, NULL, media);

  // media-size-supported
  for (i = 0, attr = NULL; i < num_media; i ++)
  {
    if ((pwg = pwgMediaForPWG(media[i])) == NULL)
      continue;

    if (!strncmp(media[i], "roll_min_", 9) && i < (num_media - 1))
    {
      // Roll min/max range...
      pwg_media_t	*pwg2;		// Max size

      i ++;
      if ((pwg2 = pwgMediaForPWG(media[i])) == NULL)
        continue;

      col = create_media_size_range(pwg->width, pwg2->width, pwg->length, pwg2->length);
    }
    else
    {
      // Sheet size...
      col = create_media_size(pwg->width, pwg->length);
    }

    if (attr)
      ippSetCollection(attrs, &attr, ippGetCount(attr), col);
    else
      attr = ippAddCollection(attrs, IPP_TAG_PRINTER, "media-size-supported", col);

    ippDelete(col);
  }

  // media-source-supported
  if (ppm_color > 0)
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-source-supported", (int)(sizeof(media_source_supported_color) / sizeof(media_source_supported_color[0])), NULL, media_source_supported_color);
  else
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-source-supported", (int)(sizeof(media_source_supported) / sizeof(media_source_supported[0])), NULL, media_source_supported);

  // media-top-margin-supported
  if (ppm_color > 0)
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-top-margin-supported", (int)(sizeof(media_top_margin_supported) / sizeof(media_top_margin_supported[0])), media_top_margin_supported);
  else
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "media-top-margin-supported", (int)(sizeof(media_top_margin_supported_color) / sizeof(media_top_margin_supported_color[0])), media_top_margin_supported_color);

  // media-type-supported
  if (ppm_color > 0)
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-type-supported", (int)(sizeof(media_type_supported_color) / sizeof(media_type_supported_color[0])), NULL, media_type_supported_color);
  else
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-type-supported", (int)(sizeof(media_type_supported) / sizeof(media_type_supported[0])), NULL, media_type_supported);

  // orientation-requested-default
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "orientation-requested-default", IPP_ORIENT_PORTRAIT);

  // orientation-requested-supported
  if (cupsArrayFind(docformats, (void *)"application/pdf") || cupsArrayFind(docformats, (void *)"image/jpeg"))
    ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "orientation-requested-supported", (int)(sizeof(orientation_requested_supported) / sizeof(orientation_requested_supported[0])), orientation_requested_supported);
  else
    ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "orientation-requested-supported", IPP_ORIENT_PORTRAIT);

  // output-bin-default
  if (ppm_color > 0)
    ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "output-bin-default", NULL, "face-up");
  else
    ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "output-bin-default", NULL, "face-down");

  // output-bin-supported
  if (ppm_color > 0)
    ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "output-bin-supported", NULL, "face-up");
  else
    ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "output-bin-supported", NULL, "face-down");

  // overrides-supported
  if (cupsArrayFind(docformats, (void *)"application/pdf"))
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "overrides-supported", (int)(sizeof(overrides_supported) / sizeof(overrides_supported[0])), NULL, overrides_supported);

  // page-ranges-supported
  ippAddBoolean(attrs, IPP_TAG_PRINTER, "page-ranges-supported", cupsArrayFind(docformats, (void *)"application/pdf") != NULL);

  // pages-per-minute
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "pages-per-minute", ppm);

  // pages-per-minute-color
  if (ppm_color > 0)
    ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, "pages-per-minute-color", ppm_color);

  // print-color-mode-default
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-color-mode-default", NULL, ppm_color > 0 ? "auto" : "monochrome");

  // print-color-mode-supported
  if (ppm_color > 0)
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-color-mode-supported", (int)(sizeof(print_color_mode_supported_color) / sizeof(print_color_mode_supported_color[0])), NULL, print_color_mode_supported_color);
  else
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-color-mode-supported", (int)(sizeof(print_color_mode_supported) / sizeof(print_color_mode_supported[0])), NULL, print_color_mode_supported);

  // print-content-optimize-default
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-content-optimize-default", NULL, "auto");

  // print-content-optimize-supported
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-content-optimize-supported", NULL, "auto");

  // print-quality-default
  ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "print-quality-default", IPP_QUALITY_NORMAL);

  // print-quality-supported
  ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "print-quality-supported", (int)(sizeof(print_quality_supported) / sizeof(print_quality_supported[0])), print_quality_supported);

  // print-rendering-intent-default
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-rendering-intent-default", NULL, "auto");

  // print-rendering-intent-supported
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "print-rendering-intent-supported", NULL, "auto");

  // printer-device-id
  snprintf(device_id, sizeof(device_id), "MFG:%s;MDL:%s;", make, model);
  ptr    = device_id + strlen(device_id);
  prefix = "CMD:";
  for (format = (const char *)cupsArrayGetFirst(docformats); format; format = (const char *)cupsArrayGetNext(docformats))
  {
    if (!strcasecmp(format, "application/pdf"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sPDF", prefix);
    else if (!strcasecmp(format, "application/postscript"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sPS", prefix);
    else if (!strcasecmp(format, "application/vnd.hp-PCL"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sPCL", prefix);
    else if (!strcasecmp(format, "image/jpeg"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sJPEG", prefix);
    else if (!strcasecmp(format, "image/png"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sPNG", prefix);
    else if (!strcasecmp(format, "image/pwg-raster"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sPWG", prefix);
    else if (!strcasecmp(format, "image/urf"))
      snprintf(ptr, sizeof(device_id) - (size_t)(ptr - device_id), "%sURF", prefix);
    else
      continue;

    ptr += strlen(ptr);
    prefix = ",";
  }
  if (ptr < (device_id + sizeof(device_id) - 1))
  {
    *ptr++ = ';';
    *ptr = '\0';
  }
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_TAG_TEXT, "printer-device-id", NULL, device_id);

  // printer-input-tray
  if (ppm_color > 0)
  {
    attr = ippAddOctetString(attrs, IPP_TAG_PRINTER, "printer-input-tray", printer_input_tray_color[0], strlen(printer_input_tray_color[0]));
    for (i = 1; i < (int)(sizeof(printer_input_tray_color) / sizeof(printer_input_tray_color[0])); i ++)
      ippSetOctetString(attrs, &attr, i, printer_input_tray_color[i], strlen(printer_input_tray_color[i]));
  }
  else
  {
    attr = ippAddOctetString(attrs, IPP_TAG_PRINTER, "printer-input-tray", printer_input_tray[0], strlen(printer_input_tray[0]));
    for (i = 1; i < (int)(sizeof(printer_input_tray) / sizeof(printer_input_tray[0])); i ++)
      ippSetOctetString(attrs, &attr, i, printer_input_tray[i], strlen(printer_input_tray[i]));
  }

  // printer-make-and-model
  snprintf(make_model, sizeof(make_model), "%s %s", make, model);
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_TAG_TEXT, "printer-make-and-model", NULL, make_model);

  // printer-resolution-default
  ippAddResolution(attrs, IPP_TAG_PRINTER, "printer-resolution-default", IPP_RES_PER_INCH, 600, 600);

  // printer-resolution-supported
  ippAddResolution(attrs, IPP_TAG_PRINTER, "printer-resolution-supported", IPP_RES_PER_INCH, 600, 600);

  // printer-supply and printer-supply-description
  if (ppm_color > 0)
  {
    attr = ippAddOctetString(attrs, IPP_TAG_PRINTER, "printer-supply", printer_supply_color[0], strlen(printer_supply_color[0]));
    for (i = 1; i < (int)(sizeof(printer_supply_color) / sizeof(printer_supply_color[0])); i ++)
      ippSetOctetString(attrs, &attr, i, printer_supply_color[i], strlen(printer_supply_color[i]));

    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-supply-description", (int)(sizeof(printer_supply_description_color) / sizeof(printer_supply_description_color[0])), NULL, printer_supply_description_color);
  }
  else
  {
    attr = ippAddOctetString(attrs, IPP_TAG_PRINTER, "printer-supply", printer_supply[0], strlen(printer_supply[0]));
    for (i = 1; i < (int)(sizeof(printer_supply) / sizeof(printer_supply[0])); i ++)
      ippSetOctetString(attrs, &attr, i, printer_supply[i], strlen(printer_supply[i]));

    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_TEXT), "printer-supply-description", (int)(sizeof(printer_supply_description) / sizeof(printer_supply_description[0])), NULL, printer_supply_description);
  }

  // pwg-raster-document-xxx-supported
  if (cupsArrayFind(docformats, (void *)"image/pwg-raster"))
  {
    ippAddResolutions(attrs, IPP_TAG_PRINTER, "pwg-raster-document-resolution-supported", (int)(sizeof(pwg_raster_document_resolution_supported) / sizeof(pwg_raster_document_resolution_supported[0])), IPP_RES_PER_INCH, pwg_raster_document_resolution_supported, pwg_raster_document_resolution_supported);

    if (ppm_color > 0 && duplex)
      ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "pwg-raster-document-sheet-back", NULL, "rotated");
    else if (duplex)
      ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "pwg-raster-document-sheet-back", NULL, "normal");

    if (ppm_color > 0)
      ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "pwg-raster-document-type-supported", (int)(sizeof(pwg_raster_document_type_supported_color) / sizeof(pwg_raster_document_type_supported_color[0])), NULL, pwg_raster_document_type_supported_color);
    else
      ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "pwg-raster-document-type-supported", (int)(sizeof(pwg_raster_document_type_supported) / sizeof(pwg_raster_document_type_supported[0])), NULL, pwg_raster_document_type_supported);
  }

  // sides-default
  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "sides-default", NULL, "one-sided");

  // sides-supported
  if (duplex)
    ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "sides-supported", (int)(sizeof(sides_supported) / sizeof(sides_supported[0])), NULL, sides_supported);
  else
    ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "sides-supported", NULL, "one-sided");

  // urf-supported
  if (cupsArrayFind(docformats, (void *)"image/urf"))
  {
    if (ppm_color > 0)
    {
      if (duplex)
	ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "urf-supported", (int)(sizeof(urf_supported_color_duplex) / sizeof(urf_supported_color_duplex[0])), NULL, urf_supported_color_duplex);
      else
	ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "urf-supported", (int)(sizeof(urf_supported_color) / sizeof(urf_supported_color[0])), NULL, urf_supported_color);
    }
    else if (duplex)
    {
      ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "urf-supported", (int)(sizeof(urf_supported_duplex) / sizeof(urf_supported_duplex[0])), NULL, urf_supported_duplex);
    }
    else
    {
      ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "urf-supported", (int)(sizeof(urf_supported) / sizeof(urf_supported[0])), NULL, urf_supported);
    }
  }

  return (attrs);
}


#if HAVE_LIBPAM
//
// 'pam_func()' - PAM conversation function.
//

static int				// O - Success or failure
pam_func(
    int                      num_msg,	// I - Number of messages
    const struct pam_message **msg,	// I - Messages
    struct pam_response      **resp,	// O - Responses
    void                     *appdata_ptr)
					// I - Pointer to connection
{
  int			i;		// Looping var
  struct pam_response	*replies;	// Replies
  ippeve_authdata_t	*data;		// Pointer to auth data


  // Allocate memory for the responses...
  if ((replies = malloc(sizeof(struct pam_response) * (size_t)num_msg)) == NULL)
    return (PAM_CONV_ERR);

  // Answer all of the messages...
  data = (ippeve_authdata_t *)appdata_ptr;

  for (i = 0; i < num_msg; i ++)
  {
    switch (msg[i]->msg_style)
    {
      case PAM_PROMPT_ECHO_ON:
          replies[i].resp_retcode = PAM_SUCCESS;
          replies[i].resp         = strdup(data->username);
          break;

      case PAM_PROMPT_ECHO_OFF:
          replies[i].resp_retcode = PAM_SUCCESS;
          replies[i].resp         = strdup(data->password);
          break;

      case PAM_TEXT_INFO:
          replies[i].resp_retcode = PAM_SUCCESS;
          replies[i].resp         = NULL;
          break;

      case PAM_ERROR_MSG:
          replies[i].resp_retcode = PAM_SUCCESS;
          replies[i].resp         = NULL;
          break;

      default:
          free(replies);
          return (PAM_CONV_ERR);
    }
  }

  // Return the responses back to PAM...
  *resp = replies;

  return (PAM_SUCCESS);
}
#endif // HAVE_LIBPAM


//
// 'parse_options()' - Parse URL options into CUPS options.
//
// The client->options string is destroyed by this function.
//

static size_t				// O - Number of options
parse_options(ippeve_client_t *client,	// I - Client
              cups_option_t   **options)// O - Options
{
  *options = NULL;

  return (cupsFormDecode(client->options, options));
}


//
// 'process_attr_message()' - Process an ATTR: message from a command.
//

static void
process_attr_message(
    ippeve_job_t *job,			// I - Job
    char       *message)		// I - Message
{
  size_t	i,			// Looping var
		num_options = 0;	// Number of name=value pairs
  cups_option_t	*options = NULL,	// name=value pairs from message
		*option;		// Current option
  ipp_attribute_t *attr;		// Current attribute


  // Grab attributes from the message line...
  num_options = cupsParseOptions(message + 5, /*end*/NULL, num_options, &options);

  // Loop through the options and record them in the printer or job objects...
  for (i = num_options, option = options; i > 0; i --, option ++)
  {
    if (!strcmp(option->name, "job-impressions"))
    {
      // Update job-impressions attribute...
      job->impressions = atoi(option->value);
    }
    else if (!strcmp(option->name, "job-impressions-completed"))
    {
      // Update job-impressions-completed attribute...
      job->impcompleted = atoi(option->value);
    }
    else if (!strncmp(option->name, "marker-", 7) || !strcmp(option->name, "printer-alert") || !strcmp(option->name, "printer-alert-description") || !strcmp(option->name, "printer-supply") || !strcmp(option->name, "printer-supply-description"))
    {
      // Update Printer Status attribute...
      cupsRWLockWrite(&job->printer->rwlock);

      if ((attr = ippFindAttribute(job->printer->attrs, option->name, IPP_TAG_ZERO)) != NULL)
        ippDeleteAttribute(job->printer->attrs, attr);

      cupsEncodeOption(job->printer->attrs, IPP_TAG_PRINTER, option->name, option->value);

      cupsRWUnlock(&job->printer->rwlock);
    }
    else
    {
      // Something else that isn't currently supported...
      fprintf(stderr, "[Job %d] Ignoring update of attribute \"%s\" with value \"%s\".\n", job->id, option->name, option->value);
    }
  }

  cupsFreeOptions(num_options, options);
}


//
// 'process_client()' - Process client requests on a thread.
//

static void *				// O - Exit status
process_client(ippeve_client_t *client)	// I - Client
{
  // Loop until we are out of requests or timeout (30 seconds)...
  bool first_time = true;		// First time request?

  while (httpWait(client->http, 30000))
  {
    if (first_time)
    {
      // See if we need to negotiate a TLS connection...
      char buf[1];			// First byte from client

      if (recv(httpGetFd(client->http), buf, 1, MSG_PEEK) == 1 && (!buf[0] || !strchr("DGHOPT", buf[0])))
      {
        char	security[256];		// Security description

        fprintf(stderr, "%s Starting HTTPS session.\n", client->hostname);

	if (!httpSetEncryption(client->http, HTTP_ENCRYPTION_ALWAYS))
	{
	  fprintf(stderr, "%s Unable to encrypt connection: %s\n", client->hostname, cupsGetErrorString());
	  break;
        }

	fprintf(stderr, "%s Connection now encrypted (%s).\n", client->hostname, httpGetSecurity(client->http, security, sizeof(security)));
      }

      first_time = false;
    }

    if (!process_http(client))
      break;
  }

  // Close the conection to the client and return...
  delete_client(client);

  return (NULL);
}


//
// 'process_http()' - Process a HTTP request.
//

int					// O - 1 on success, 0 on failure
process_http(ippeve_client_t *client)	// I - Client connection
{
  char			uri[1024];	// URI
  http_state_t		http_state;	// HTTP state
  http_status_t		http_status;	// HTTP status
  ipp_state_t		ipp_state;	// State of IPP transfer
  char			scheme[32],	// Method/scheme
			userpass[128],	// Username:password
			hostname[HTTP_MAX_HOST],
					// Hostname
			*ptr;		// Pointer into value
  int			port;		// Port number


  // Clear state variables...
  client->username[0] = '\0';

  ippDelete(client->request);
  ippDelete(client->response);

  client->request   = NULL;
  client->response  = NULL;
  client->operation = HTTP_STATE_WAITING;

  // Read a request from the connection...
  while ((http_state = httpReadRequest(client->http, uri, sizeof(uri))) == HTTP_STATE_WAITING)
    usleep(1);

  // Parse the request line...
  if (http_state == HTTP_STATE_ERROR)
  {
    if (httpGetError(client->http) == EPIPE)
      fprintf(stderr, "%s Client closed connection.\n", client->hostname);
    else
      fprintf(stderr, "%s Bad request line (%s).\n", client->hostname, strerror(httpGetError(client->http)));

    return (0);
  }
  else if (http_state == HTTP_STATE_UNKNOWN_METHOD)
  {
    fprintf(stderr, "%s Bad/unknown operation.\n", client->hostname);
    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }
  else if (http_state == HTTP_STATE_UNKNOWN_VERSION)
  {
    fprintf(stderr, "%s Bad HTTP version.\n", client->hostname);
    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }

  fprintf(stderr, "%s %s %s\n", client->hostname, httpStateString(http_state), uri);

  // Separate the URI into its components...
  if (httpSeparateURI(HTTP_URI_CODING_MOST, uri, scheme, sizeof(scheme), userpass, sizeof(userpass), hostname, sizeof(hostname), &port, client->uri, sizeof(client->uri)) < HTTP_URI_STATUS_OK && (http_state != HTTP_STATE_OPTIONS || strcmp(uri, "*")))
  {
    fprintf(stderr, "%s Bad URI \"%s\".\n", client->hostname, uri);
    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }

  if ((client->options = strchr(client->uri, '?')) != NULL)
    *(client->options)++ = '\0';

  // Process the request...
  client->start     = time(NULL);
  client->operation = httpGetState(client->http);

  // Parse incoming parameters until the status changes...
  while ((http_status = httpUpdate(client->http)) == HTTP_STATUS_CONTINUE);

  if (http_status != HTTP_STATUS_OK)
  {
    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }

  // Validate the host header...
  if (!httpGetField(client->http, HTTP_FIELD_HOST)[0] && httpGetVersion(client->http) >= HTTP_VERSION_1_1)
  {
    // HTTP/1.1 and higher require the "Host:" field...
    fprintf(stderr, "%s Missing Host: header.\n", client->hostname);
    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }

  cupsCopyString(client->host_field, httpGetField(client->http, HTTP_FIELD_HOST), sizeof(client->host_field));
  if ((ptr = strrchr(client->host_field, ':')) != NULL)
  {
    // Grab port number from Host: header...
    *ptr++ = '\0';
    client->host_port = atoi(ptr);
  }
  else
  {
    // Use the default port number...
    client->host_port = client->printer->port;
  }

  if ((ptr = strstr(client->host_field, ".local")) == NULL)
    ptr = strrchr(client->host_field, '.');

  if (!isdigit(client->host_field[0] & 255) && client->host_field[0] != '[' && strcmp(client->host_field, client->printer->hostname) && strcmp(client->host_field, "localhost") &&
      (!ptr || (strcmp(ptr, ".local") && strcmp(ptr, ".local."))))
  {
    fprintf(stderr, "%s Bad Host: header '%s'.\n", client->hostname, client->host_field);
    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }

  // Handle HTTP Upgrade...
  if (!strcasecmp(httpGetField(client->http, HTTP_FIELD_CONNECTION), "Upgrade"))
  {
    if (strstr(httpGetField(client->http, HTTP_FIELD_UPGRADE), "TLS/") != NULL && !httpIsEncrypted(client->http))
    {
      char	security[256];		// Security description

      if (!respond_http(client, HTTP_STATUS_SWITCHING_PROTOCOLS, NULL, NULL, 0))
        return (0);

      fprintf(stderr, "%s Upgrading to encrypted connection.\n", client->hostname);

      if (!httpSetEncryption(client->http, HTTP_ENCRYPTION_REQUIRED))
      {
        fprintf(stderr, "%s Unable to encrypt connection: %s\n", client->hostname, cupsGetErrorString());
	return (0);
      }

      fprintf(stderr, "%s Connection now encrypted (%s).\n", client->hostname, httpGetSecurity(client->http, security, sizeof(security)));
    }
    else if (!respond_http(client, HTTP_STATUS_NOT_IMPLEMENTED, NULL, NULL, 0))
      return (0);
  }

  // Handle new transfers...
  switch (client->operation)
  {
    case HTTP_STATE_OPTIONS :
        // Do OPTIONS command...
	return (respond_http(client, HTTP_STATUS_OK, NULL, NULL, 0));

    case HTTP_STATE_HEAD :
        if (!strcmp(client->uri, "/en.strings"))
	  return (respond_http(client, HTTP_STATUS_OK, NULL, "text/strings", 0));
        else if (!strcmp(client->uri, "/icon.png") || !strcmp(client->uri, "/icon-lg.png") || !strcmp(client->uri, "/icon-sm.png"))
	  return (respond_http(client, HTTP_STATUS_OK, NULL, "image/png", 0));
	else if (!strcmp(client->uri, "/") || !strcmp(client->uri, "/media") || !strcmp(client->uri, "/supplies"))
	  return (respond_http(client, HTTP_STATUS_OK, NULL, "text/html", 0));
	else
	  return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));

    case HTTP_STATE_GET :
        if (!strcmp(client->uri, "/en.strings"))
	{
	  // Send strings file.
          if (client->printer->strings)
          {
	    int		fd;		// Icon file
	    struct stat	fileinfo;	// Icon file information
	    char	buffer[4096];	// Copy buffer
	    ssize_t	bytes;		// Bytes

	    if ((fd = open(client->printer->strings, O_RDONLY | O_BINARY)) >= 0)
	    {
	      if (fstat(fd, &fileinfo))
	      {
		close(fd);
		return (0);
	      }
	      else if (!respond_http(client, HTTP_STATUS_OK, NULL, "text/strings", (size_t)fileinfo.st_size))
	      {
		close(fd);
		return (0);
	      }

	      while ((bytes = read(fd, buffer, sizeof(buffer))) > 0)
		httpWrite(client->http, buffer, (size_t)bytes);

	      httpFlushWrite(client->http);

	      close(fd);
	    }
	    else
	      return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
	  }
	  else
	    return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
        }
        else if (!strcmp(client->uri, "/icon.png"))
	{
	  // Send medium PNG icon file.
          if (client->printer->icons[1])
          {
	    int		fd;		// Icon file
	    struct stat	fileinfo;	// Icon file information
	    char	buffer[4096];	// Copy buffer
	    ssize_t	bytes;		// Bytes

	    if ((fd = open(client->printer->icons[1], O_RDONLY | O_BINARY)) >= 0)
	    {
	      if (fstat(fd, &fileinfo))
	      {
		close(fd);
		return (0);
	      }
	      else if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png", (size_t)fileinfo.st_size))
	      {
		close(fd);
		return (0);
	      }

	      while ((bytes = read(fd, buffer, sizeof(buffer))) > 0)
		httpWrite(client->http, buffer, (size_t)bytes);

	      httpFlushWrite(client->http);

	      close(fd);
	    }
	    else
	      return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
	  }
	  else
	  {
	    fputs("Icon file is internal printer.png.\n", stderr);

	    if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png", sizeof(printer_png)))
	      return (0);

            httpWrite(client->http, (const char *)printer_png, sizeof(printer_png));
	    httpFlushWrite(client->http);
	  }
	}
        else if (!strcmp(client->uri, "/icon-lg.png"))
	{
	  // Send large PNG icon file.
          if (client->printer->icons[2])
          {
	    int		fd;		// Icon file
	    struct stat	fileinfo;	// Icon file information
	    char	buffer[4096];	// Copy buffer
	    ssize_t	bytes;		// Bytes

	    if ((fd = open(client->printer->icons[2], O_RDONLY | O_BINARY)) >= 0)
	    {
	      if (fstat(fd, &fileinfo))
	      {
		close(fd);
		return (0);
	      }
	      else if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png", (size_t)fileinfo.st_size))
	      {
		close(fd);
		return (0);
	      }

	      while ((bytes = read(fd, buffer, sizeof(buffer))) > 0)
		httpWrite(client->http, buffer, (size_t)bytes);

	      httpFlushWrite(client->http);

	      close(fd);
	    }
	    else
	      return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
	  }
	  else
	  {
	    fputs("Icon file is internal printer-lg.png.\n", stderr);

	    if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png", sizeof(printer_lg_png)))
	      return (0);

            httpWrite(client->http, (const char *)printer_lg_png, sizeof(printer_lg_png));
	    httpFlushWrite(client->http);
	  }
	}
        else if (!strcmp(client->uri, "/icon-sm.png"))
	{
	  // Send small PNG icon file.
          if (client->printer->icons[0])
          {
	    int		fd;		// Icon file
	    struct stat	fileinfo;	// Icon file information
	    char	buffer[4096];	// Copy buffer
	    ssize_t	bytes;		// Bytes

	    if ((fd = open(client->printer->icons[0], O_RDONLY | O_BINARY)) >= 0)
	    {
	      if (fstat(fd, &fileinfo))
	      {
		close(fd);
		return (0);
	      }
	      else if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png", (size_t)fileinfo.st_size))
	      {
		close(fd);
		return (0);
	      }

	      while ((bytes = read(fd, buffer, sizeof(buffer))) > 0)
		httpWrite(client->http, buffer, (size_t)bytes);

	      httpFlushWrite(client->http);

	      close(fd);
	    }
	    else
	      return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
	  }
	  else
	  {
	    fputs("Icon file is internal printer-sm.png.\n", stderr);

	    if (!respond_http(client, HTTP_STATUS_OK, NULL, "image/png", sizeof(printer_sm_png)))
	      return (0);

            httpWrite(client->http, (const char *)printer_sm_png, sizeof(printer_sm_png));
	    httpFlushWrite(client->http);
	  }
	}
	else
	{
	  // Authenticate if needed...
	  if ((http_status = authenticate_request(client)) != HTTP_STATUS_CONTINUE)
	  {
	    return (respond_http(client, http_status, NULL, NULL, 0));
	  }

	  if (!strcmp(client->uri, "/"))
	  {
	    // Show web status page...
	    return (show_status(client));
	  }
	  else if (!strcmp(client->uri, "/media"))
	  {
	    // Show web media page...
	    return (show_media(client));
	  }
	  else if (!strcmp(client->uri, "/supplies"))
	  {
	    // Show web supplies page...
	    return (show_supplies(client));
	  }
	  else
	    return (respond_http(client, HTTP_STATUS_NOT_FOUND, NULL, NULL, 0));
	}
	break;

    case HTTP_STATE_POST :
	if (strcmp(httpGetField(client->http, HTTP_FIELD_CONTENT_TYPE),
	           "application/ipp"))
        {
	  // Not an IPP request...
	  return (respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0));
	}

        // Read the IPP request...
	client->request = ippNew();

        while ((ipp_state = ippRead(client->http, client->request)) != IPP_STATE_DATA)
	{
	  if (ipp_state == IPP_STATE_ERROR)
	  {
            fprintf(stderr, "%s IPP read error (%s).\n", client->hostname, cupsGetErrorString());
	    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
	    return (0);
	  }
	}

        // Now that we have the IPP request, process the request...
        return (process_ipp(client));

    default :
        break; // Anti-compiler-warning-code
  }

  return (1);
}


//
// 'process_ipp()' - Process an IPP request.
//

static int				// O - 1 on success, 0 on error
process_ipp(ippeve_client_t *client)	// I - Client
{
  ipp_tag_t		group;		// Current group tag
  ipp_attribute_t	*attr;		// Current attribute
  ipp_attribute_t	*charset;	// Character set attribute
  ipp_attribute_t	*language;	// Language attribute
  ipp_attribute_t	*uri;		// Printer URI attribute
  int			major, minor;	// Version number
  const char		*name;		// Name of attribute
  http_status_t		status;		// Authentication status


  debug_attributes("Request", client->request, 1);

  // First build an empty response message for this request...
  client->operation_id = ippGetOperation(client->request);
  client->response     = ippNewResponse(client->request);

  // Then validate the request header and required attributes...
  major = ippGetVersion(client->request, &minor);

  if (major < 1 || major > 2)
  {
    // Return an error, since we only support IPP 1.x and 2.x.
    respond_ipp(client, IPP_STATUS_ERROR_VERSION_NOT_SUPPORTED, "Bad request version number %d.%d.", major, minor);
  }
  else if ((major * 10 + minor) > MaxVersion)
  {
    if (httpGetState(client->http) != HTTP_STATE_POST_SEND)
      httpFlush(client->http);		// Flush trailing (junk) data

    respond_http(client, HTTP_STATUS_BAD_REQUEST, NULL, NULL, 0);
    return (0);
  }
  else if (ippGetRequestId(client->request) <= 0)
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Bad request-id %d.", ippGetRequestId(client->request));
  }
  else if (!ippGetFirstAttribute(client->request))
  {
    respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "No attributes in request.");
  }
  else
  {
    // Make sure that the attributes are provided in the correct order and  don't repeat groups...
    for (attr = ippGetFirstAttribute(client->request), group = ippGetGroupTag(attr); attr; attr = ippGetNextAttribute(client->request))
    {
      if (ippGetGroupTag(attr) < group && ippGetGroupTag(attr) != IPP_TAG_ZERO)
      {
        // Out of order; return an error...
	respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Attribute groups are out of order (%x < %x).", ippGetGroupTag(attr), group);
	break;
      }
      else
      {
	group = ippGetGroupTag(attr);
      }
    }

    if (!attr)
    {
      // Then make sure that the first three attributes are:
      //
      //   attributes-charset
      //   attributes-natural-language
      //   printer-uri/job-uri
      attr = ippGetFirstAttribute(client->request);
      name = ippGetName(attr);
      if (attr && name && !strcmp(name, "attributes-charset") && ippGetValueTag(attr) == IPP_TAG_CHARSET)
	charset = attr;
      else
	charset = NULL;

      attr = ippGetNextAttribute(client->request);
      name = ippGetName(attr);

      if (attr && name && !strcmp(name, "attributes-natural-language") && ippGetValueTag(attr) == IPP_TAG_LANGUAGE)
	language = attr;
      else
	language = NULL;

      if ((attr = ippFindAttribute(client->request, "printer-uri", IPP_TAG_URI)) != NULL)
	uri = attr;
      else if ((attr = ippFindAttribute(client->request, "job-uri", IPP_TAG_URI)) != NULL)
	uri = attr;
      else
	uri = NULL;

      if (charset && strcasecmp(ippGetString(charset, 0, NULL), "us-ascii") && strcasecmp(ippGetString(charset, 0, NULL), "utf-8"))
      {
        // Bad character set...
	respond_ipp(client, IPP_STATUS_ERROR_CHARSET, "Unsupported character set '%s'.", ippGetString(charset, 0, NULL));
	attr = ippCopyAttribute(client->response, charset, 0);
        ippSetGroupTag(client->response, &attr, IPP_TAG_UNSUPPORTED_GROUP);
      }
      else if (!charset || !language || !uri)
      {
        // Return an error, since attributes-charset,
	// attributes-natural-language, and printer-uri/job-uri are required
	// for all operations.
	respond_ipp(client, IPP_STATUS_ERROR_BAD_REQUEST, "Missing required attributes.");
      }
      else
      {
        char		scheme[32],	// URI scheme
			userpass[32],	// Username/password in URI
			host[256],	// Host name in URI
			resource[256];	// Resource path in URI
	int		port;		// Port number in URI

        name = ippGetName(uri);

        if (httpSeparateURI(HTTP_URI_CODING_ALL, ippGetString(uri, 0, NULL), scheme, sizeof(scheme), userpass, sizeof(userpass), host, sizeof(host), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK)
	{
	  respond_ipp(client, IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES, "Bad %s value '%s'.", name, ippGetString(uri, 0, NULL));
        }
        else if ((!strcmp(name, "job-uri") && strncmp(resource, "/ipp/print/", 11)) || (!strcmp(name, "printer-uri") && strcmp(resource, "/ipp/print") && (strcmp(resource, "/") || ippGetOperation(client->request) != IPP_OP_GET_PRINTER_ATTRIBUTES)))
	{
	  respond_ipp(client, IPP_STATUS_ERROR_NOT_FOUND, "%s %s not found.", name, ippGetString(uri, 0, NULL));
	}
	else if (client->operation_id != IPP_OP_GET_PRINTER_ATTRIBUTES && (status = authenticate_request(client)) != HTTP_STATUS_CONTINUE)
        {
          flush_document_data(client);

          return (respond_http(client, status, NULL, NULL, 0));
        }
        else
	{
	  // Handle HTTP Expect...
	  if (httpGetExpect(client->http))
	  {
	    if (httpGetExpect(client->http) == HTTP_STATUS_CONTINUE)
	    {
	      // Send 100-continue header...
	      if (!respond_http(client, HTTP_STATUS_CONTINUE, NULL, NULL, 0))
		return (0);
	    }
	    else
	    {
	      // Send 417-expectation-failed header...
	      if (!respond_http(client, HTTP_STATUS_EXPECTATION_FAILED, NULL, NULL, 0))
		return (0);

	      flush_document_data(client);
	      return (1);
	    }
	  }

	  // Try processing the operation...
	  switch (client->operation_id)
	  {
	    case IPP_OP_PRINT_JOB :
		ipp_print_job(client);
		break;

	    case IPP_OP_PRINT_URI :
		ipp_print_uri(client);
		break;

	    case IPP_OP_VALIDATE_JOB :
		ipp_validate_job(client);
		break;

	    case IPP_OP_CREATE_JOB :
		ipp_create_job(client);
		break;

	    case IPP_OP_SEND_DOCUMENT :
		ipp_send_document(client);
		break;

	    case IPP_OP_SEND_URI :
		ipp_send_uri(client);
		break;

	    case IPP_OP_CANCEL_JOB :
		ipp_cancel_job(client);
		break;

	    case IPP_OP_CANCEL_JOBS :
	    case IPP_OP_CANCEL_MY_JOBS :
		ipp_cancel_jobs(client);
		break;

	    case IPP_OP_GET_JOB_ATTRIBUTES :
		ipp_get_job_attributes(client);
		break;

	    case IPP_OP_GET_JOBS :
		ipp_get_jobs(client);
		break;

	    case IPP_OP_GET_PRINTER_ATTRIBUTES :
		ipp_get_printer_attributes(client);
		break;

	    case IPP_OP_CLOSE_JOB :
	        ipp_close_job(client);
		break;

	    case IPP_OP_IDENTIFY_PRINTER :
	        ipp_identify_printer(client);
		break;

	    default :
		respond_ipp(client, IPP_STATUS_ERROR_OPERATION_NOT_SUPPORTED, "Operation not supported.");
		break;
	  }
	}
      }
    }
  }

  // Send the HTTP header and return...
  if (httpGetState(client->http) != HTTP_STATE_POST_SEND)
    httpFlush(client->http);		// Flush trailing (junk) data

  return (respond_http(client, HTTP_STATUS_OK, NULL, "application/ipp", ippGetLength(client->response)));
}


//
// 'process_job()' - Process a print job.
//

static void *				// O - Thread exit status
process_job(ippeve_job_t *job)		// I - Job
{
  job->state          = IPP_JSTATE_PROCESSING;
  job->printer->state = IPP_PSTATE_PROCESSING;
  job->processing     = time(NULL);

  while (job->printer->state_reasons & IPPEVE_PREASON_MEDIA_EMPTY)
  {
    job->printer->state_reasons |= IPPEVE_PREASON_MEDIA_NEEDED;

    sleep(1);
  }

  job->printer->state_reasons &= (ippeve_preason_t)~IPPEVE_PREASON_MEDIA_NEEDED;

  if (job->printer->command)
  {
    // Execute a command with the job spool file and wait for it to complete...
    int 		pid,		// Process ID
			status;		// Exit status
    struct timeval	start,		// Start time
			end;		// End time
    char		*myargv[3],	// Command-line arguments
			*myenvp[400];	// Environment variables
    int			myenvc;		// Number of environment variables
    ipp_attribute_t	*attr;		// Job attribute
    char		val[1280],	// IPP_NAME=value
			*valptr;	// Pointer into string
#ifndef _WIN32
    int			mystdout = -1;	// File for stdout
    int			mypipe[2];	// Pipe for stderr
    char		line[2048],	// Line from stderr
			*ptr,		// Pointer into line
			*endptr;	// End of line
    ssize_t		bytes;		// Bytes read
#endif // !_WIN32

    fprintf(stderr, "[Job %d] Running command \"%s %s\".\n", job->id, job->printer->command, job->filename);
    gettimeofday(&start, NULL);

    // Setup the command-line arguments...
    myargv[0] = job->printer->command;
    myargv[1] = job->filename;
    myargv[2] = NULL;

    // Copy the current environment, then add environment variables for every
    // Job attribute and Printer -default attributes...
    for (myenvc = 0; environ[myenvc] && myenvc < (int)(sizeof(myenvp) / sizeof(myenvp[0]) - 1); myenvc ++)
      myenvp[myenvc] = strdup(environ[myenvc]);

    if (myenvc > (int)(sizeof(myenvp) / sizeof(myenvp[0]) - 32))
    {
      fprintf(stderr, "[Job %d] Too many environment variables to process job.\n", job->id);
      job->state = IPP_JSTATE_ABORTED;
      goto error;
    }

    snprintf(val, sizeof(val), "CONTENT_TYPE=%s", job->format);
    myenvp[myenvc ++] = strdup(val);

    if (job->printer->device_uri)
    {
      snprintf(val, sizeof(val), "DEVICE_URI=%s", job->printer->device_uri);
      myenvp[myenvc ++] = strdup(val);
    }

    if (job->printer->output_format)
    {
      snprintf(val, sizeof(val), "OUTPUT_TYPE=%s", job->printer->output_format);
      myenvp[myenvc ++] = strdup(val);
    }

    if (job->password[0])
    {
      snprintf(val, sizeof(val), "IPP_DOCUMENT_PASSWORD=%s", job->password);
      myenvp[myenvc ++] = strdup(val);
    }

    for (attr = ippGetFirstAttribute(job->printer->attrs); attr && myenvc < (int)(sizeof(myenvp) / sizeof(myenvp[0]) - 1); attr = ippGetNextAttribute(job->printer->attrs))
    {
      // Convert "attribute-name-default" to "IPP_ATTRIBUTE_NAME_DEFAULT=",
      // "attribute-name-ready" to "IPP_ATTRIBUTE_NAME_READY",
      // "attribute-name-supported" to "IPP_ATTRIBUTE_NAME_SUPPORTED",
      // "pclm-xxx" to "IPP_PCLM_XXX", and "pwg-xxx" to "IPP_PWG_XXX",
      // then add the value(s) from the attribute.
      const char	*name = ippGetName(attr),
					// Attribute name
			*default_suffix = strstr(name, "-default"),
					// Suffix on attribute name
			*ready_suffix = strstr(name, "-ready"),
					// Suffix on attribute name
			*supported_suffix = strstr(name, "-supported");
					// Suffix on attribute name

      if (strncmp(name, "pclm-", 5) && strncmp(name, "pwg-", 4) && (!default_suffix || default_suffix[8]) && (!ready_suffix || ready_suffix[6]) && (!supported_suffix || supported_suffix[10]))
        continue;

      valptr = val;
      *valptr++ = 'I';
      *valptr++ = 'P';
      *valptr++ = 'P';
      *valptr++ = '_';
      while (*name && valptr < (val + sizeof(val) - 2))
      {
        if (*name == '-')
	  *valptr++ = '_';
	else
	  *valptr++ = (char)toupper(*name & 255);

	name ++;
      }
      *valptr++ = '=';
      ippAttributeString(attr, valptr, sizeof(val) - (size_t)(valptr - val));

      myenvp[myenvc++] = strdup(val);
    }

    for (attr = ippGetFirstAttribute(job->attrs); attr && myenvc < (int)(sizeof(myenvp) / sizeof(myenvp[0]) - 1); attr = ippGetNextAttribute(job->attrs))
    {
      // Convert "attribute-name" to "IPP_ATTRIBUTE_NAME=" and then add the
      // value(s) from the attribute.
      const char *name = ippGetName(attr);
					// Attribute name

      if (!name)
        continue;

      valptr = val;
      *valptr++ = 'I';
      *valptr++ = 'P';
      *valptr++ = 'P';
      *valptr++ = '_';
      while (*name && valptr < (val + sizeof(val) - 2))
      {
        if (*name == '-')
	  *valptr++ = '_';
	else
	  *valptr++ = (char)toupper(*name & 255);

	name ++;
      }
      *valptr++ = '=';
      ippAttributeString(attr, valptr, sizeof(val) - (size_t)(valptr - val));

      myenvp[myenvc++] = strdup(val);
    }

    if (attr)
    {
      fprintf(stderr, "[Job %d] Too many environment variables to process job.\n", job->id);
      job->state = IPP_JSTATE_ABORTED;
      goto error;
    }

    myenvp[myenvc] = NULL;

    // Now run the program...
#ifdef _WIN32
    status = _spawnvpe(_P_WAIT, job->printer->command, myargv, myenvp);

#else
    if (job->printer->device_uri)
    {
      char	scheme[32],		// URI scheme
		userpass[256],		// username:password (unused)
		host[256],		// Hostname or IP address
		resource[256];		// Resource path
      int	port;			// Port number


      if (httpSeparateURI(HTTP_URI_CODING_ALL, job->printer->device_uri, scheme, sizeof(scheme), userpass, sizeof(userpass), host, sizeof(host), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK)
      {
        fprintf(stderr, "[Job %d] Bad device URI '%s'.\n", job->id, job->printer->device_uri);
      }
      else if (!strcmp(scheme, "file"))
      {
        struct stat	fileinfo;	// See if this is a file or directory...

        if (stat(resource, &fileinfo))
        {
          if (errno == ENOENT)
          {
            if ((mystdout = open(resource, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666)) >= 0)
	      fprintf(stderr, "[Job %d] Saving print command output to '%s'.\n", job->id, resource);
	    else
	      fprintf(stderr, "[Job %d] Unable to create '%s': %s\n", job->id, resource, strerror(errno));
          }
          else
          {
            fprintf(stderr, "[Job %d] Unable to access '%s': %s\n", job->id, resource, strerror(errno));
          }
        }
        else if (S_ISDIR(fileinfo.st_mode))
        {
          if ((mystdout = create_job_file(job, line, sizeof(line), resource, "prn")) >= 0)
	    fprintf(stderr, "[Job %d] Saving print command output to '%s'.\n", job->id, line);
          else
            fprintf(stderr, "[Job %d] Unable to create '%s': %s\n", job->id, line, strerror(errno));
        }
	else if (!S_ISREG(fileinfo.st_mode))
	{
	  if ((mystdout = open(resource, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666)) >= 0)
	    fprintf(stderr, "[Job %d] Saving print command output to '%s'.\n", job->id, resource);
	  else
            fprintf(stderr, "[Job %d] Unable to create '%s': %s\n", job->id, resource, strerror(errno));
	}
        else if ((mystdout = open(resource, O_WRONLY | O_BINARY)) >= 0)
        {
	  fprintf(stderr, "[Job %d] Saving print command output to '%s'.\n", job->id, resource);
	}
	else
	{
	  fprintf(stderr, "[Job %d] Unable to open '%s': %s\n", job->id, resource, strerror(errno));
	}
      }
      else if (!strcmp(scheme, "socket"))
      {
        http_addrlist_t	*addrlist;	// List of addresses
        char		service[32];	// Service number

        snprintf(service, sizeof(service), "%d", port);

        if ((addrlist = httpAddrGetList(host, AF_UNSPEC, service)) == NULL)
          fprintf(stderr, "[Job %d] Unable to find '%s': %s\n", job->id, host, cupsGetErrorString());
        else if (!httpAddrConnect(addrlist, &mystdout, 30000, &(job->cancel)))
          fprintf(stderr, "[Job %d] Unable to connect to '%s' on port %d: %s\n", job->id, host, port, cupsGetErrorString());

        httpAddrFreeList(addrlist);
      }
      else
      {
        fprintf(stderr, "[Job %d] Unsupported device URI scheme '%s'.\n", job->id, scheme);
      }
    }
    else if ((mystdout = create_job_file(job, line, sizeof(line), job->printer->directory, "prn")) >= 0)
    {
      fprintf(stderr, "[Job %d] Saving print command output to '%s'.\n", job->id, line);
    }

    if (mystdout < 0)
    {
      if ((mystdout = open("/dev/null", O_WRONLY | O_BINARY)) < 0)
        fprintf(stderr, "[Job %d] Unable to redirect command output to /dev/null: %s", job->id, strerror(errno));
    }

    if (pipe(mypipe))
    {
      fprintf(stderr, "[Job %d] Unable to create pipe for stderr: %s\n", job->id, strerror(errno));
      mypipe[0] = mypipe[1] = -1;
    }

    if ((pid = fork()) == 0)
    {
      // Child comes here...
      if (mystdout >= 0)
      {
        close(1);
        dup2(mystdout, 1);
        close(mystdout);
      }

      if (mypipe[1] >= 0)
      {
        close(2);
	dup2(mypipe[1], 2);
	close(mypipe[0]);
	close(mypipe[1]);
      }

      execve(job->printer->command, myargv, myenvp);
      exit(errno);
    }
    else if (pid < 0)
    {
      // Unable to fork process...
      fprintf(stderr, "[Job %d] Unable to start job processing command: %s\n", job->id, strerror(errno));
      status = -1;

      if (mystdout >= 0)
	close(mystdout);

      if (mypipe[0] >= 0)
      {
	close(mypipe[0]);
	close(mypipe[1]);
      }

      // Free memory used for environment...
      while (myenvc > 0)
	free(myenvp[-- myenvc]);
    }
    else
    {
      // Free memory used for environment...
      while (myenvc > 0)
	free(myenvp[-- myenvc]);

      // Close the output file in the parent process...
      if (mystdout >= 0)
	close(mystdout);

      // If the pipe exists, read from it until EOF...
      if (mypipe[0] >= 0)
      {
	close(mypipe[1]);

	endptr = line;
	while ((bytes = read(mypipe[0], endptr, sizeof(line) - (size_t)(endptr - line) - 1)) > 0)
	{
	  endptr += bytes;
	  *endptr = '\0';

          while ((ptr = strchr(line, '\n')) != NULL)
	  {
	    int level = 3;		// Message log level

	    *ptr++ = '\0';

	    if (!strncmp(line, "ATTR:", 5))
	    {
	      // Process job/printer attribute updates.
	      process_attr_message(job, line);
	    }
	    else if (!strncmp(line, "DEBUG:", 6))
	    {
	      // Debug message...
              level = 2;
	    }
	    else if (!strncmp(line, "ERROR:", 6))
	    {
	      // Error message...
              level         = 0;
              job->message  = strdup(line + 6);
              job->msglevel = 0;
	    }
	    else if (!strncmp(line, "INFO:", 5))
	    {
	      // Informational/progress message...
              level = 1;
              if (job->msglevel)
              {
                job->message  = strdup(line + 5);
                job->msglevel = 1;
	      }
	    }
	    else if (!strncmp(line, "STATE:", 6))
	    {
	      // Process printer-state-reasons keywords.
	      process_state_message(job->printer, line);
	    }

	    if (Verbosity >= level)
	      fprintf(stderr, "[Job %d] Command - %s\n", job->id, line);

	    bytes = ptr - line;
            if (ptr < endptr)
	      memmove(line, ptr, (size_t)(endptr - ptr));
	    endptr -= bytes;
	    *endptr = '\0';
	  }
	}

	close(mypipe[0]);
      }

      // Wait for child to complete...
#  ifdef HAVE_WAITPID
      while (waitpid(pid, &status, 0) < 0);
#  else
      while (wait(&status) < 0);
#  endif // HAVE_WAITPID
    }
#endif // _WIN32

    if (status)
    {
#ifndef _WIN32
      if (WIFEXITED(status))
#endif // !_WIN32
	fprintf(stderr, "[Job %d] Command \"%s\" exited with status %d.\n", job->id,  job->printer->command, WEXITSTATUS(status));
#ifndef _WIN32
      else
	fprintf(stderr, "[Job %d] Command \"%s\" terminated with signal %d.\n", job->id, job->printer->command, WTERMSIG(status));
#endif // !_WIN32
      job->state = IPP_JSTATE_ABORTED;
    }
    else
    {
      fprintf(stderr, "[Job %d] Command \"%s\" completed successfully.\n", job->id, job->printer->command);
    }

    // Report the total processing time...
    gettimeofday(&end, NULL);

    fprintf(stderr, "[Job %d] Processing time was %.3f seconds.\n", job->id, end.tv_sec - start.tv_sec + 0.000001 * (end.tv_usec - start.tv_usec));
  }
  else
  {
    // Sleep for a random amount of time to simulate job processing.
    sleep((unsigned)(5 + (cupsGetRand() % 11)));
  }

  if (job->cancel)
    job->state = IPP_JSTATE_CANCELED;
  else if (job->state == IPP_JSTATE_PROCESSING)
    job->state = IPP_JSTATE_COMPLETED;

  error:

  job->completed           = time(NULL);
  job->printer->state      = IPP_PSTATE_IDLE;
  job->printer->active_job = NULL;

  return (NULL);
}


//
// 'process_state_message()' - Process a STATE: message from a command.
//

static void
process_state_message(
    ippeve_printer_t *printer,		// I - Printer
    const char       *message)		// I - Message
{
  int		i;			// Looping var
  ippeve_preason_t state_reasons,	// printer-state-reasons values
		bit;			// Current reason bit
  char		keyword[128],		// Current keyword
		*keyptr;		// Pointer into keyword
  int		remove;			// Non-zero if we are removing keywords


  // Skip leading "STATE:" and any whitespace...
  if (!strncmp(message, "STATE:", 6))
  {
    for (message += 6; *message; message ++)
    {
      if (*message != ' ' && *message != '\t')
	break;
    }
  }

  // Support the following forms of message:
  //
  // "keyword[,keyword,...]" to set the printer-state-reasons value(s).
  //
  // "-keyword[,keyword,...]" to remove keywords.
  //
  // "+keyword[,keyword,...]" or "~keyword[,keyword,...]" to add keywords.
  //
  // Keywords may or may not have a suffix (-report, -warning, -error) per
  // RFC 8011.
  if (*message == '-')
  {
    remove        = 1;
    state_reasons = printer->state_reasons;
    message ++;
  }
  else if (*message == '+' || *message == '~')
  {
    remove        = 0;
    state_reasons = printer->state_reasons;
    message ++;
  }
  else
  {
    remove        = 0;
    state_reasons = IPPEVE_PREASON_NONE;

    memset(printer->state_keywords, 0, sizeof(printer->state_keywords));
  }

  while (*message)
  {
    for (keyptr = keyword; *message && *message != ','; message ++)
    {
      if (keyptr < (keyword + sizeof(keyword) - 1))
        *keyptr++ = *message;
    }
    *keyptr = '\0';

    if (*message == ',')
      message ++;

    if ((keyptr = strstr(keyword, "-error")) != NULL)
      *keyptr = '\0';
    else if ((keyptr = strstr(keyword, "-report")) != NULL)
      *keyptr = '\0';
    else if ((keyptr = strstr(keyword, "-warning")) != NULL)
      *keyptr = '\0';

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

    // Lookup standard keywords...
    for (i = 0, bit = 1; i < (int)(sizeof(ippeve_preason_strings) / sizeof(ippeve_preason_strings[0])); i ++, bit *= 2)
    {
      if (!strcmp(keyword, ippeve_preason_strings[i]))
      {
	if (remove)
	  state_reasons &= ~bit;
	else
	  state_reasons |= bit;

	break;
      }
    }

    if (i >= (int)(sizeof(ippeve_preason_strings) / sizeof(ippeve_preason_strings[0])))
    {
      // Look at custom state keywords...
      for (i = 0; i < (int)(sizeof(printer->state_keywords) / sizeof(printer->state_keywords[0])); i ++)
      {
	if (!strcmp(keyword, printer->state_keywords[i]))
	{
	  if (remove)
	  {
	    // Remove custom keyword...
	    printer->state_keywords[i][0] = '\0';
	  }

	  break;
	}
      }

      if (i >= (int)(sizeof(printer->state_keywords) / sizeof(printer->state_keywords[0])) && !remove)
      {
	for (i = 0; i < (int)(sizeof(printer->state_keywords) / sizeof(printer->state_keywords[0])); i ++)
	{
	  if (!printer->state_keywords[i][0])
	  {
	    // Add this keyword to the empty slot...
	    cupsCopyString(printer->state_keywords[i], keyword, sizeof(printer->state_keywords[i]));
	    break;
	  }
	}
      }
    }
  }

  printer->state_reasons = state_reasons;
}


//
// 'register_printer()' - Register a printer object via DNS-SD.
//

static bool				// O - `true` on success, `false` on error
register_printer(
    ippeve_printer_t *printer)		// I - Printer
{
  size_t		num_txt;	// Number of DNS-SD IPP TXT key/value pairs
  cups_option_t		*txt;		// DNS-SD IPP TXT key/value pairs
  uint32_t		if_index;	// Interface index
  size_t		i,		// Looping var
			count;		// Number of values
  ipp_attribute_t	*color_supported,
			*document_format_supported,
			*printer_location,
			*printer_make_and_model,
			*printer_uuid,
			*sides_supported,
			*urf_supported;	// Printer attributes
  const char		*value;		// Value string
  char			regtype[256],	// Registration types
			adminurl[247],	// adminurl value
			formats[252],	// List of supported formats
			urf[252],	// List of supported URF values
			*ptr;		// Pointer into string


  if (printer->dnssd_subtypes && !strcmp(printer->dnssd_subtypes, "off"))
    return (true);

  color_supported           = ippFindAttribute(printer->attrs, "color-supported", IPP_TAG_BOOLEAN);
  document_format_supported = ippFindAttribute(printer->attrs, "document-format-supported", IPP_TAG_MIMETYPE);
  printer_location          = ippFindAttribute(printer->attrs, "printer-location", IPP_TAG_TEXT);
  printer_make_and_model    = ippFindAttribute(printer->attrs, "printer-make-and-model", IPP_TAG_TEXT);
  printer_uuid              = ippFindAttribute(printer->attrs, "printer-uuid", IPP_TAG_URI);
  sides_supported           = ippFindAttribute(printer->attrs, "sides-supported", IPP_TAG_KEYWORD);
  urf_supported             = ippFindAttribute(printer->attrs, "urf-supported", IPP_TAG_KEYWORD);

  httpAssembleURI(HTTP_URI_CODING_ALL, adminurl, sizeof(adminurl), WEB_SCHEME, NULL, printer->hostname, printer->port, "/");

  for (i = 0, count = ippGetCount(document_format_supported), ptr = formats; i < count; i ++)
  {
    value = ippGetString(document_format_supported, i, NULL);

    if (!strcasecmp(value, "application/octet-stream"))
      continue;

    if (ptr > formats && ptr < (formats + sizeof(formats) - 1))
      *ptr++ = ',';

    cupsCopyString(ptr, value, sizeof(formats) - (size_t)(ptr - formats));
    ptr += strlen(ptr);

    if (ptr >= (formats + sizeof(formats) - 1))
      break;
  }

  urf[0] = '\0';
  for (i = 0, count = ippGetCount(urf_supported), ptr = urf; i < count; i ++)
  {
    value = ippGetString(urf_supported, i, NULL);

    if (ptr > urf && ptr < (urf + sizeof(urf) - 1))
      *ptr++ = ',';

    cupsCopyString(ptr, value, sizeof(urf) - (size_t)(ptr - urf));
    ptr += strlen(ptr);

    if (ptr >= (urf + sizeof(urf) - 1))
      break;
  }

  // Rename the service as needed...
  if (printer->dnssd_collision)
  {
    char	new_dnssd_name[256];	// New DNS-SD name
    const char	*uuid = ippGetString(printer_uuid, 0, NULL);
					// "printer-uuid" value

    cupsRWLockWrite(&printer->rwlock);

    snprintf(new_dnssd_name, sizeof(new_dnssd_name), "%s (%c%c%c%c%c%c)", printer->dnssd_name, toupper(uuid[39]), toupper(uuid[40]), toupper(uuid[41]), toupper(uuid[42]), toupper(uuid[43]), toupper(uuid[44]));

    free(printer->dnssd_name);
    printer->dnssd_name = strdup(new_dnssd_name);

    fprintf(stderr, "DNS-SD name collision, trying new DNS-SD service name '%s'.\n", printer->dnssd_name);

    cupsRWUnlock(&printer->rwlock);

    printer->dnssd_collision = 0;
  }

  // Build the TXT record for IPP...
  num_txt = cupsAddOption("rp", "ipp/print", 0, &txt);
  if ((value = ippGetString(printer_make_and_model, 0, NULL)) != NULL)
    num_txt = cupsAddOption("ty", value, num_txt, &txt);
  num_txt = cupsAddOption("adminurl", adminurl, num_txt, &txt);
  if ((value = ippGetString(printer_location, 0, NULL)) != NULL)
    num_txt = cupsAddOption("note", value, num_txt, &txt);
  num_txt = cupsAddOption("pdl", formats, num_txt, &txt);
  num_txt = cupsAddOption("Color", ippGetBoolean(color_supported, 0) ? "T" : "F", num_txt, &txt);
  num_txt = cupsAddOption("Duplex", ippGetCount(sides_supported) > 1 ? "T" : "F", num_txt, &txt);
  if ((value = ippGetString(printer_uuid, 0, NULL)) != NULL)
    num_txt = cupsAddOption("UUID", value + 9, num_txt, &txt);
  num_txt = cupsAddOption("TLS", "1.3", num_txt, &txt);
  if (urf[0])
    num_txt = cupsAddOption("URF", urf, num_txt, &txt);
  num_txt = cupsAddOption("txtvers", "1", num_txt, &txt);
  num_txt = cupsAddOption("qtotal", "1", num_txt, &txt);

  // Register the _printer._tcp (LPD) service type with a port number of 0 to
  // defend our service name but not actually support LPD...
  if_index = !strcmp(printer->hostname, "localhost") ? CUPS_DNSSD_IF_INDEX_LOCAL : CUPS_DNSSD_IF_INDEX_ANY;

  cupsDNSSDServiceDelete(printer->services);
  if ((printer->services = cupsDNSSDServiceNew(printer->dnssd, if_index, printer->dnssd_name, (cups_dnssd_service_cb_t)dnssd_callback, printer)) == NULL)
    goto error;

  if (!cupsDNSSDServiceAdd(printer->services, "_printer._tcp", /*domain*/NULL, printer->hostname, /*port*/0, /*num_txt*/0, /*txt*/NULL))
    goto error;

  // Then register the _ipp._tcp (IPP) service type with the real port number to
  // advertise our IPP printer...
  if (printer->dnssd_subtypes && *(printer->dnssd_subtypes))
    snprintf(regtype, sizeof(regtype), "_ipp._tcp,%s", printer->dnssd_subtypes);
  else
    cupsCopyString(regtype, "_ipp._tcp", sizeof(regtype));

  if (!cupsDNSSDServiceAdd(printer->services, regtype, /*domain*/NULL, printer->hostname, (uint16_t)printer->port, num_txt, txt))
    goto error;

  // Then register the _ipps._tcp (IPP) service type with the real port number
  // to advertise our IPPS printer...
  if (printer->dnssd_subtypes && *(printer->dnssd_subtypes))
    snprintf(regtype, sizeof(regtype), "_ipps._tcp,%s", printer->dnssd_subtypes);
  else
    cupsCopyString(regtype, "_ipps._tcp", sizeof(regtype));

  if (!cupsDNSSDServiceAdd(printer->services, regtype, /*domain*/NULL, printer->hostname, (uint16_t)printer->port, num_txt, txt))
    goto error;

  // Similarly, register the _http._tcp,_printer (HTTP) service type with the
  // real port number to advertise our IPP printer's web interface...
  if (!cupsDNSSDServiceAdd(printer->services, "_http._tcp,_printer", /*domain*/NULL, printer->hostname, (uint16_t)printer->port, num_txt, txt))
    goto error;

  // Commit it...
  if (!cupsDNSSDServicePublish(printer->services))
    goto error;

  fprintf(stderr, "Registered printer '%s' for discovery using DNS-SD.\n", printer->dnssd_name);

  cupsFreeOptions(num_txt, txt);

  return (true);

  // If we get here there was a problem...
  error:

  fprintf(stderr, "Unable to register printer '%s' for discovery using DNS-SD: %s\n", printer->dnssd_name, cupsGetErrorString());

  cupsFreeOptions(num_txt, txt);

  return (false);
}


//
// 'respond_http()' - Send a HTTP response.
//

bool					// O - `true` on success, `false` on failure
respond_http(
    ippeve_client_t *client,		// I - Client
    http_status_t code,			// I - HTTP status of response
    const char    *content_encoding,	// I - Content-Encoding of response
    const char    *type,		// I - MIME media type of response
    size_t        length)		// I - Length of response
{
  char	message[1024];			// Text message


  fprintf(stderr, "%s %s\n", client->hostname, httpStatusString(code));

  if (code == HTTP_STATUS_CONTINUE)
  {
    // 100-continue doesn't send any headers...
    return (httpWriteResponse(client->http, HTTP_STATUS_CONTINUE));
  }

  // Format an error message...
  if (!type && !length && code != HTTP_STATUS_OK && code != HTTP_STATUS_SWITCHING_PROTOCOLS)
  {
    snprintf(message, sizeof(message), "%d - %s\n", code, httpStatusString(code));

    type   = "text/plain";
    length = strlen(message);
  }
  else
    message[0] = '\0';

  // Send the HTTP response header...
  httpClearFields(client->http);

  if (code == HTTP_STATUS_METHOD_NOT_ALLOWED || client->operation == HTTP_STATE_OPTIONS)
    httpSetField(client->http, HTTP_FIELD_ALLOW, "GET, HEAD, OPTIONS, POST");

  if (code == HTTP_STATUS_UNAUTHORIZED)
  {
    char value[256];			// WWW-Authenticate value

    snprintf(value, sizeof(value), "Basic realm=\"%s\"", PAMService);
    httpSetField(client->http, HTTP_FIELD_WWW_AUTHENTICATE, value);
  }

  if (type)
  {
    if (!strcmp(type, "text/html"))
      httpSetField(client->http, HTTP_FIELD_CONTENT_TYPE, "text/html; charset=utf-8");
    else
      httpSetField(client->http, HTTP_FIELD_CONTENT_TYPE, type);

    if (content_encoding)
      httpSetField(client->http, HTTP_FIELD_CONTENT_ENCODING, content_encoding);
  }

  httpSetLength(client->http, length);

  if (!httpWriteResponse(client->http, code))
    return (false);

  // Send the response data...
  if (message[0])
  {
    // Send a plain text message.
    if (httpPrintf(client->http, "%s", message) < 0)
      return (false);

    if (httpWrite(client->http, "", 0) < 0)
      return (false);
  }
  else if (client->response)
  {
    // Send an IPP response...
    debug_attributes("Response", client->response, 2);

    ippSetState(client->response, IPP_STATE_IDLE);

    if (ippWrite(client->http, client->response) != IPP_STATE_DATA)
      return (false);
  }

  return (true);
}


//
// 'respond_ignored()' - Respond with an ignored attribute.
//

static void
respond_ignored(
    ippeve_client_t *client,		// I - Client
    ipp_attribute_t *attr)		// I - Attribute
{
  ipp_attribute_t	*temp;		// Copy of attribute


  if (!ippGetStatusCode(client->response))
    respond_ipp(client, IPP_STATUS_OK_IGNORED_OR_SUBSTITUTED, "Unsupported %s %s%s value.", ippGetName(attr), ippGetCount(attr) > 1 ? "1setOf " : "", ippTagString(ippGetValueTag(attr)));

  temp = ippCopyAttribute(client->response, attr, 0);
  ippSetGroupTag(client->response, &temp, IPP_TAG_UNSUPPORTED_GROUP);
}


//
// 'respond_ipp()' - Send an IPP response.
//

static void
respond_ipp(ippeve_client_t *client,	// I - Client
            ipp_status_t  status,	// I - status-code
	    const char    *message,	// I - printf-style status-message
	    ...)			// I - Additional args as needed
{
  const char	*formatted = NULL;	// Formatted message


  if (ippGetStatusCode(client->response) == IPP_STATUS_OK)
    ippSetStatusCode(client->response, status);

  if (message)
  {
    va_list		ap;		// Pointer to additional args
    ipp_attribute_t	*attr;		// New status-message attribute

    va_start(ap, message);
    if ((attr = ippFindAttribute(client->response, "status-message", IPP_TAG_TEXT)) != NULL)
      ippSetStringfv(client->response, &attr, 0, message, ap);
    else
      attr = ippAddStringfv(client->response, IPP_TAG_OPERATION, IPP_TAG_TEXT, "status-message", NULL, message, ap);
    va_end(ap);

    formatted = ippGetString(attr, 0, NULL);
  }

  if (formatted)
    fprintf(stderr, "%s %s %s (%s)\n", client->hostname, ippOpString(client->operation_id), ippErrorString(status), formatted);
  else
    fprintf(stderr, "%s %s %s\n", client->hostname, ippOpString(client->operation_id), ippErrorString(status));
}


//
// 'respond_unsupported()' - Respond with an unsupported attribute.
//

static void
respond_unsupported(
    ippeve_client_t *client,		// I - Client
    ipp_attribute_t *attr)		// I - Atribute
{
  ipp_attribute_t	*temp;		// Copy of attribute


  respond_ipp(client, IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES, "Unsupported %s %s%s value.", ippGetName(attr), ippGetCount(attr) > 1 ? "1setOf " : "", ippTagString(ippGetValueTag(attr)));

  temp = ippCopyAttribute(client->response, attr, 0);
  ippSetGroupTag(client->response, &temp, IPP_TAG_UNSUPPORTED_GROUP);
}


//
// 'run_printer()' - Run the printer service.
//

static void
run_printer(ippeve_printer_t *printer)	// I - Printer
{
  int			num_fds;	// Number of file descriptors
  struct pollfd		polldata[3];	// poll() data
  ippeve_client_t	*client;	// New client


#ifndef _WIN32
  // Set signal handlers for SIGINT and SIGTERM...
  signal(SIGINT, signal_handler);
  signal(SIGTERM, signal_handler);
#endif // !_WIN32

  // Setup poll() data for the DNS-SD service socket and IPv4/6 listeners...
  polldata[0].fd     = printer->ipv4;
  polldata[0].events = POLLIN;

  polldata[1].fd     = printer->ipv6;
  polldata[1].events = POLLIN;

  num_fds = 2;

  // Loop until we are killed or have a hard error...
  for (;;)
  {
    if (poll(polldata, (nfds_t)num_fds, 1000) < 0 && errno != EINTR)
    {
      perror("poll() failed");
      break;
    }

#ifndef _WIN32
    if (StopPrinter)
      break;
#endif // !_WIN32

    if (polldata[0].revents & POLLIN)
    {
      if ((client = create_client(printer, printer->ipv4)) != NULL)
      {
        cups_thread_t t = cupsThreadCreate((cups_thread_func_t)process_client, client);

        if (t)
        {
          cupsThreadDetach(t);
        }
        else
	{
	  perror("Unable to create client thread");
	  delete_client(client);
	}
      }
    }

    if (polldata[1].revents & POLLIN)
    {
      if ((client = create_client(printer, printer->ipv6)) != NULL)
      {
        cups_thread_t t = cupsThreadCreate((cups_thread_func_t)process_client, client);

        if (t)
        {
          cupsThreadDetach(t);
        }
        else
	{
	  perror("Unable to create client thread");
	  delete_client(client);
	}
      }
    }

    if (printer->dnssd_collision)
      register_printer(printer);

    // Clean out old jobs...
    clean_jobs(printer);
  }
}


//
// 'show_media()' - Show media load state.
//

static int				// O - 1 on success, 0 on failure
show_media(ippeve_client_t  *client)	// I - Client connection
{
  ippeve_printer_t *printer = client->printer;
					// Printer
  size_t		i, j,		// Looping vars
                        num_ready,	// Number of ready media
                        num_sizes,	// Number of media sizes
			num_sources,	// Number of media sources
                        num_types;	// Number of media types
  ipp_attribute_t	*media_col_ready,// media-col-ready attribute
                        *media_ready,	// media-ready attribute
                        *media_sizes,	// media-supported attribute
                        *media_sources,	// media-source-supported attribute
                        *media_types,	// media-type-supported attribute
                        *input_tray;	// printer-input-tray attribute
  ipp_t			*media_col;	// media-col value
  const char            *media_size,	// media value
                        *media_source,	// media-source value
                        *media_type,	// media-type value
                        *ready_size,	// media-col-ready media-size[-name] value
                        *ready_source,	// media-col-ready media-source value
                        *ready_tray,	// printer-input-tray value
                        *ready_type;	// media-col-ready media-type value
  char			tray_str[1024],	// printer-input-tray string value
			*tray_ptr;	// Pointer into value
  size_t		tray_len;	// Length of printer-input-tray value
  int			ready_sheets;	// printer-input-tray sheets value
  size_t		num_options = 0;// Number of form options
  cups_option_t		*options = NULL;// Form options
  static const int	sheets[] =	// Number of sheets
  {
    250,
    125,
    50,
    25,
    5,
    0,
    -2
  };


  if (!respond_http(client, HTTP_STATUS_OK, NULL, "text/html", 0))
    return (0);

  html_header(client, printer->name, 0);

  if ((media_col_ready = ippFindAttribute(printer->attrs, "media-col-ready", IPP_TAG_BEGIN_COLLECTION)) == NULL)
  {
    html_printf(client, "<p>Error: No media-col-ready defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  media_ready = ippFindAttribute(printer->attrs, "media-ready", IPP_TAG_ZERO);

  if ((media_sizes = ippFindAttribute(printer->attrs, "media-supported", IPP_TAG_ZERO)) == NULL)
  {
    html_printf(client, "<p>Error: No media-supported defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  if ((media_sources = ippFindAttribute(printer->attrs, "media-source-supported", IPP_TAG_ZERO)) == NULL)
  {
    html_printf(client, "<p>Error: No media-source-supported defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  if ((media_types = ippFindAttribute(printer->attrs, "media-type-supported", IPP_TAG_ZERO)) == NULL)
  {
    html_printf(client, "<p>Error: No media-type-supported defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  if ((input_tray = ippFindAttribute(printer->attrs, "printer-input-tray", IPP_TAG_STRING)) == NULL)
  {
    html_printf(client, "<p>Error: No printer-input-tray defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  num_ready   = ippGetCount(media_col_ready);
  num_sizes   = ippGetCount(media_sizes);
  num_sources = ippGetCount(media_sources);
  num_types   = ippGetCount(media_types);

  if (num_sources != ippGetCount(input_tray))
  {
    html_printf(client, "<p>Error: Different number of trays in media-source-supported and printer-input-tray defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  // Process form data if present...
  if (printer->web_forms)
    num_options = parse_options(client, &options);

  if (num_options > 0)
  {
    // WARNING: A real printer/server implementation MUST NOT implement
    // media updates via a GET request - GET requests are supposed to be
    // idempotent (without side-effects) and we obviously are not
    // authenticating access here.  This form is provided solely to
    // enable testing and development!
    char	name[255];		// Form name
    const char	*val;			// Form value
    pwg_media_t	*media;			// Media info

    cupsRWLockWrite(&printer->rwlock);

    ippDeleteAttribute(printer->attrs, media_col_ready);
    media_col_ready = NULL;

    if (media_ready)
    {
      ippDeleteAttribute(printer->attrs, media_ready);
      media_ready = NULL;
    }

    printer->state_reasons &= (ippeve_preason_t)~(IPPEVE_PREASON_MEDIA_LOW | IPPEVE_PREASON_MEDIA_EMPTY | IPPEVE_PREASON_MEDIA_NEEDED);

    for (i = 0; i < num_sources; i ++)
    {
      media_source = ippGetString(media_sources, i, NULL);

      if (!strcmp(media_source, "auto") || !strcmp(media_source, "manual") || strstr(media_source, "-man") != NULL)
	continue;

      snprintf(name, sizeof(name), "size%u", (unsigned)i);
      if ((media_size = cupsGetOption(name, num_options, options)) != NULL && (media = pwgMediaForPWG(media_size)) != NULL)
      {
        snprintf(name, sizeof(name), "type%u", (unsigned)i);
        if ((media_type = cupsGetOption(name, num_options, options)) != NULL && !*media_type)
          media_type = NULL;

        if (media_ready)
          ippSetString(printer->attrs, &media_ready, ippGetCount(media_ready), media_size);
        else
          media_ready = ippAddString(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "media-ready", NULL, media_size);

        media_col = create_media_col(media_size, media_source, media_type, create_media_size(media->width, media->length), -1, -1, -1, -1);

        if (media_col_ready)
          ippSetCollection(printer->attrs, &media_col_ready, ippGetCount(media_col_ready), media_col);
        else
          media_col_ready = ippAddCollection(printer->attrs, IPP_TAG_PRINTER, "media-col-ready", media_col);
        ippDelete(media_col);
      }
      else
        media = NULL;

      snprintf(name, sizeof(name), "level%u", (unsigned)i);
      if ((val = cupsGetOption(name, num_options, options)) != NULL)
        ready_sheets = atoi(val);
      else
        ready_sheets = 0;

      if (!strcmp(media_source, "roll"))
	snprintf(tray_str, sizeof(tray_str), "type=continuousRoll;mediafeed=%d;mediaxfeed=%d;maxcapacity=%d;level=%d;status=0;name=%s;", media ? media->length : 0, media ? media->width : 0, 100, ready_sheets, media_source);
      else
	snprintf(tray_str, sizeof(tray_str), "type=sheetFeedAuto%sRemovableTray;mediafeed=%d;mediaxfeed=%d;maxcapacity=%d;level=%d;status=0;name=%s;", !strcmp(media_source, "by-pass-tray") ? "Non" : "", media ? media->length : 0, media ? media->width : 0, strcmp(media_source, "by-pass-tray") ? 250 : 25, ready_sheets, media_source);

      ippSetOctetString(printer->attrs, &input_tray, i, tray_str, strlen(tray_str));

      if (ready_sheets == 0)
      {
        printer->state_reasons |= IPPEVE_PREASON_MEDIA_EMPTY;
        if (printer->active_job)
          printer->state_reasons |= IPPEVE_PREASON_MEDIA_NEEDED;
      }
      else if (ready_sheets < 25 && ready_sheets > 0)
        printer->state_reasons |= IPPEVE_PREASON_MEDIA_LOW;
    }

    if (!media_col_ready)
      media_col_ready = ippAddOutOfBand(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_NOVALUE, "media-col-ready");

    if (!media_ready)
      media_ready = ippAddOutOfBand(printer->attrs, IPP_TAG_PRINTER, IPP_TAG_NOVALUE, "media-ready");

    cupsRWUnlock(&printer->rwlock);
  }

  if (printer->web_forms)
    html_printf(client, "<form method=\"GET\" action=\"/media\">\n");

  html_printf(client, "<table class=\"form\" summary=\"Media\">\n");
  for (i = 0; i < num_sources; i ++)
  {
    media_source = ippGetString(media_sources, i, NULL);

    if (!strcmp(media_source, "auto") || !strcmp(media_source, "manual") || strstr(media_source, "-man") != NULL)
      continue;

    for (j = 0, ready_size = NULL, ready_type = NULL; j < num_ready; j ++)
    {
      media_col    = ippGetCollection(media_col_ready, j);
      ready_size   = ippGetString(ippFindAttribute(media_col, "media-size-name", IPP_TAG_ZERO), 0, NULL);
      ready_source = ippGetString(ippFindAttribute(media_col, "media-source", IPP_TAG_ZERO), 0, NULL);
      ready_type   = ippGetString(ippFindAttribute(media_col, "media-type", IPP_TAG_ZERO), 0, NULL);

      if (!ready_size)
        ready_size = ippGetString(media_ready, j, NULL);

      if (ready_source && !strcmp(ready_source, media_source))
        break;

      ready_size = NULL;
      ready_type = NULL;
    }

    html_printf(client, "<tr><th>%s:</th>", media_source);

    // Media size...
    if (printer->web_forms)
    {
      html_printf(client, "<td><select name=\"size%u\"><option value=\"\">None</option>", (unsigned)i);
      if (ready_size)
        html_printf(client, "<option selected>%s</option>", ready_size);
      for (j = 0; j < num_sizes; j ++)
      {
	media_size = ippGetString(media_sizes, j, NULL);

	html_printf(client, "<option>%s</option>", media_size);
      }
      html_printf(client, "</select>");
    }
    else
      html_printf(client, "<td>%s", ready_size);

    // Media type...
    if (printer->web_forms)
    {
      html_printf(client, " <select name=\"type%u\"><option value=\"\">None</option>", (unsigned)i);
      if (ready_type)
        html_printf(client, "<option selected>%s</option>", ready_type);
      for (j = 0; j < num_types; j ++)
      {
	media_type = ippGetString(media_types, j, NULL);

	html_printf(client, "<option%s>%s</option>", (ready_type && !strcmp(ready_type, media_type)) ? " selected" : "", media_type);
      }
      html_printf(client, "</select>");
    }
    else if (ready_type)
      html_printf(client, ", %s", ready_type);

    // Level/sheets loaded...
    if ((ready_tray = ippGetOctetString(input_tray, i, &tray_len)) != NULL)
    {
      if (tray_len > (int)(sizeof(tray_str) - 1))
        tray_len = (int)sizeof(tray_str) - 1;
      memcpy(tray_str, ready_tray, (size_t)tray_len);
      tray_str[tray_len] = '\0';

      if ((tray_ptr = strstr(tray_str, "level=")) != NULL)
        ready_sheets = atoi(tray_ptr + 6);
      else
        ready_sheets = 0;
    }
    else
      ready_sheets = 0;

    if (printer->web_forms)
    {
      html_printf(client, " <select name=\"level%u\">", (unsigned)i);
      for (j = 0; j < (int)(sizeof(sheets) / sizeof(sheets[0])); j ++)
      {
	if (!strcmp(media_source, "by-pass-tray") && sheets[j] > 25)
	  continue;

	if (sheets[j] < 0)
	  html_printf(client, "<option value=\"%d\"%s>Unknown</option>", sheets[j], sheets[j] == ready_sheets ? " selected" : "");
	else
	  html_printf(client, "<option value=\"%d\"%s>%d sheets</option>", sheets[j], sheets[j] == ready_sheets ? " selected" : "", sheets[j]);
      }
      html_printf(client, "</select></td></tr>\n");
    }
    else if (ready_sheets == 1)
      html_printf(client, ", 1 sheet</td></tr>\n");
    else if (ready_sheets > 0)
      html_printf(client, ", %d sheets</td></tr>\n", ready_sheets);
    else
      html_printf(client, "</td></tr>\n");
  }

  if (printer->web_forms)
  {
    html_printf(client, "<tr><td></td><td><input type=\"submit\" value=\"Update Media\">");
    if (num_options > 0)
      html_printf(client, " <span class=\"badge\" id=\"status\">Media updated.</span>\n");
    html_printf(client, "</td></tr></table></form>\n");

    if (num_options > 0)
      html_printf(client, "<script>\n"
			  "setTimeout(hide_status, 3000);\n"
			  "function hide_status() {\n"
			  "  var status = document.getElementById('status');\n"
			  "  status.style.display = 'none';\n"
			  "}\n"
			  "</script>\n");
  }
  else
    html_printf(client, "</table>\n");

  cupsFreeOptions(num_options, options);

  html_footer(client);

  return (1);
}


//
// 'show_status()' - Show printer/system state.
//

static int				// O - 1 on success, 0 on failure
show_status(ippeve_client_t  *client)	// I - Client connection
{
  ippeve_printer_t *printer = client->printer;
					// Printer
  ippeve_job_t		*job;		// Current job
  size_t		i;		// Looping var
  ippeve_preason_t	reason;		// Current reason
  static const char * const reasons[] =	// Reason strings
  {
    "Other",
    "Cover Open",
    "Input Tray Missing",
    "Marker Supply Empty",
    "Marker Supply Low",
    "Marker Waste Almost Full",
    "Marker Waste Full",
    "Media Empty",
    "Media Jam",
    "Media Low",
    "Media Needed",
    "Moving to Paused",
    "Paused",
    "Spool Area Full",
    "Toner Empty",
    "Toner Low"
  };
  static const char * const state_colors[] =
  {					// State colors
    "#0C0",				// Idle
    "#EE0",				// Processing
    "#C00"				// Stopped
  };


  // Process form data if present...
  if (printer->web_forms)
  {
    size_t		num_options = 0;// Number of form options
    cups_option_t	*options = NULL;// Form options
    const char		*oreasons;	// Reasons string

    num_options = parse_options(client, &options);

    if ((oreasons = cupsGetOption("reasons", num_options, options)) != NULL)
    {
      // WARNING: A real printer/server implementation MUST NOT implement
      // state updates via a GET request - GET requests are supposed to be
      // idempotent (without side-effects) and we obviously are not
      // authenticating access here.  This form is provided solely to
      // enable testing and development!

      process_state_message(printer, oreasons);
    }

    cupsFreeOptions(num_options, options);
  }

  if (!respond_http(client, HTTP_STATUS_OK, NULL, "text/html", 0))
    return (0);

  html_header(client, printer->name, printer->state == IPP_PSTATE_PROCESSING ? 5 : 15);
  html_printf(client, "<h1><img style=\"background: %s; border-radius: 10px; float: left; margin-right: 10px; padding: 10px;\" src=\"/icon.png\" width=\"64\" height=\"64\">%s Jobs</h1>\n", state_colors[printer->state - IPP_PSTATE_IDLE], printer->name);
  html_printf(client, "<p>%s, %u job(s).", printer->state == IPP_PSTATE_IDLE ? "Idle" : printer->state == IPP_PSTATE_PROCESSING ? "Printing" : "Stopped", (unsigned)cupsArrayGetCount(printer->jobs));
  for (i = 0, reason = 1; i < (sizeof(reasons) / sizeof(reasons[0])); i ++, reason <<= 1)
  {
    if (printer->state_reasons & reason)
      html_printf(client, "\n<br>&nbsp;&nbsp;&nbsp;&nbsp;%s", reasons[i]);
  }
  for (i = 0; i < (sizeof(printer->state_keywords) / sizeof(printer->state_keywords[0])); i ++)
  {
    if (printer->state_keywords[i][0])
      html_printf(client, "\n<br>&nbsp;&nbsp;&nbsp;&nbsp;%s", printer->state_keywords[i]);
  }
  html_printf(client, "</p>\n");

  if (cupsArrayGetCount(printer->jobs) > 0)
  {
    cupsRWLockRead(&(printer->rwlock));

    html_printf(client, "<table class=\"striped\" summary=\"Jobs\"><thead><tr><th>Job #</th><th>Name</th><th>Owner</th><th>Status</th></tr></thead><tbody>\n");
    for (job = (ippeve_job_t *)cupsArrayGetFirst(printer->jobs); job; job = (ippeve_job_t *)cupsArrayGetNext(printer->jobs))
    {
      char	when[256],		// When job queued/started/finished
		hhmmss[64];		// Time HH:MM:SS

      switch (job->state)
      {
	case IPP_JSTATE_PENDING :
	case IPP_JSTATE_HELD :
	    snprintf(when, sizeof(when), "Queued at %s", time_string(job->created, hhmmss, sizeof(hhmmss)));
	    break;
	case IPP_JSTATE_PROCESSING :
	case IPP_JSTATE_STOPPED :
	    snprintf(when, sizeof(when), "Started at %s", time_string(job->processing, hhmmss, sizeof(hhmmss)));
	    break;
	case IPP_JSTATE_ABORTED :
	    snprintf(when, sizeof(when), "Aborted at %s", time_string(job->completed, hhmmss, sizeof(hhmmss)));
	    break;
	case IPP_JSTATE_CANCELED :
	    snprintf(when, sizeof(when), "Canceled at %s", time_string(job->completed, hhmmss, sizeof(hhmmss)));
	    break;
	case IPP_JSTATE_COMPLETED :
	    snprintf(when, sizeof(when), "Completed at %s", time_string(job->completed, hhmmss, sizeof(hhmmss)));
	    break;
      }

      html_printf(client, "<tr><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr>\n", job->id, job->name, job->username, when);
    }
    html_printf(client, "</tbody></table>\n");

    cupsRWUnlock(&(printer->rwlock));
  }

  html_footer(client);

  return (1);
}


//
// 'show_supplies()' - Show printer supplies.
//

static int				// O - 1 on success, 0 on failure
show_supplies(
    ippeve_client_t  *client)		// I - Client connection
{
  ippeve_printer_t *printer = client->printer;
					// Printer
  size_t	i,			// Looping var
		num_supply;		// Number of supplies
  ipp_attribute_t *supply,		// printer-supply attribute
		*supply_desc;		// printer-supply-description attribute
  size_t	num_options = 0;	// Number of form options
  cups_option_t	*options = NULL;	// Form options
  size_t	supply_len;		// Length of supply value
  int		level;			// Supply level
  const char	*supply_value;		// Supply value
  char		supply_text[1024],	// Supply string
		*supply_ptr;		// Pointer into supply string
  static const char * const printer_supply[] =
  {					// printer-supply values
    "index=1;class=receptacleThatIsFilled;type=wasteToner;unit=percent;"
        "maxcapacity=100;level=%d;colorantname=unknown;",
    "index=2;class=supplyThatIsConsumed;type=toner;unit=percent;"
        "maxcapacity=100;level=%d;colorantname=black;",
    "index=3;class=supplyThatIsConsumed;type=toner;unit=percent;"
        "maxcapacity=100;level=%d;colorantname=cyan;",
    "index=4;class=supplyThatIsConsumed;type=toner;unit=percent;"
        "maxcapacity=100;level=%d;colorantname=magenta;",
    "index=5;class=supplyThatIsConsumed;type=toner;unit=percent;"
        "maxcapacity=100;level=%d;colorantname=yellow;"
  };
  static const char * const backgrounds[] =
  {					// Background colors for the supply-level bars
    "#777 linear-gradient(#333,#777)",
    "#000 linear-gradient(#666,#000)",
    "#0FF linear-gradient(#6FF,#0FF)",
    "#F0F linear-gradient(#F6F,#F0F)",
    "#CC0 linear-gradient(#EE6,#EE0)"
  };
  static const char * const colors[] =	// Text colors for the supply-level bars
  {
    "#fff",
    "#fff",
    "#000",
    "#000",
    "#000"
  };


  if (!respond_http(client, HTTP_STATUS_OK, NULL, "text/html", 0))
    return (0);

  html_header(client, printer->name, 0);

  if ((supply = ippFindAttribute(printer->attrs, "printer-supply", IPP_TAG_STRING)) == NULL)
  {
    html_printf(client, "<p>Error: No printer-supply defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  num_supply = ippGetCount(supply);

  if ((supply_desc = ippFindAttribute(printer->attrs, "printer-supply-description", IPP_TAG_TEXT)) == NULL)
  {
    html_printf(client, "<p>Error: No printer-supply-description defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  if (num_supply != ippGetCount(supply_desc))
  {
    html_printf(client, "<p>Error: Different number of values for printer-supply and printer-supply-description defined for printer.</p>\n");
    html_footer(client);
    return (1);
  }

  if (printer->web_forms)
    num_options = parse_options(client, &options);

  if (num_options > 0)
  {
    // WARNING: A real printer/server implementation MUST NOT implement
    // supply updates via a GET request - GET requests are supposed to be
    // idempotent (without side-effects) and we obviously are not
    // authenticating access here.  This form is provided solely to
    // enable testing and development!
    char	name[64];		// Form field
    const char	*val;			// Form value

    cupsRWLockWrite(&printer->rwlock);

    ippDeleteAttribute(printer->attrs, supply);
    supply = NULL;

    printer->state_reasons &= (ippeve_preason_t)~(IPPEVE_PREASON_MARKER_SUPPLY_EMPTY | IPPEVE_PREASON_MARKER_SUPPLY_LOW | IPPEVE_PREASON_MARKER_WASTE_ALMOST_FULL | IPPEVE_PREASON_MARKER_WASTE_FULL | IPPEVE_PREASON_TONER_EMPTY | IPPEVE_PREASON_TONER_LOW);

    for (i = 0; i < num_supply; i ++)
    {
      snprintf(name, sizeof(name), "supply%u", (unsigned)i);
      if ((val = cupsGetOption(name, num_options, options)) != NULL)
      {
        level = atoi(val);      // New level

        snprintf(supply_text, sizeof(supply_text), printer_supply[i], level);
        if (supply)
          ippSetOctetString(printer->attrs, &supply, ippGetCount(supply), supply_text, strlen(supply_text));
        else
          supply = ippAddOctetString(printer->attrs, IPP_TAG_PRINTER, "printer-supply", supply_text, strlen(supply_text));

        if (i == 0)
        {
          if (level == 100)
            printer->state_reasons |= IPPEVE_PREASON_MARKER_WASTE_FULL;
          else if (level > 90)
            printer->state_reasons |= IPPEVE_PREASON_MARKER_WASTE_ALMOST_FULL;
        }
        else
        {
          if (level == 0)
            printer->state_reasons |= IPPEVE_PREASON_TONER_EMPTY;
          else if (level < 10)
            printer->state_reasons |= IPPEVE_PREASON_TONER_LOW;
        }
      }
    }

    cupsRWUnlock(&printer->rwlock);
  }

  if (printer->web_forms)
    html_printf(client, "<form method=\"GET\" action=\"/supplies\">\n");

  html_printf(client, "<table class=\"form\" summary=\"Supplies\">\n");
  for (i = 0; i < num_supply; i ++)
  {
    supply_value = ippGetOctetString(supply, i, &supply_len);
    if (supply_len > (int)(sizeof(supply_text) - 1))
      supply_len = (int)sizeof(supply_text) - 1;

    memcpy(supply_text, supply_value, (size_t)supply_len);
    supply_text[supply_len] = '\0';

    if ((supply_ptr = strstr(supply_text, "level=")) != NULL)
      level = atoi(supply_ptr + 6);
    else
      level = 50;

    if (printer->web_forms)
      html_printf(client, "<tr><th>%s:</th><td><input name=\"supply%u\" size=\"3\" value=\"%d\"></td>", ippGetString(supply_desc, i, NULL), (unsigned)i, level);
    else
      html_printf(client, "<tr><th>%s:</th>", ippGetString(supply_desc, i, NULL));

    if (level < 10)
      html_printf(client, "<td class=\"meter\"><span class=\"bar\" style=\"background: %s; padding: 5px %dpx;\"></span>&nbsp;%d%%</td></tr>\n", backgrounds[i], level * 2, level);
    else
      html_printf(client, "<td class=\"meter\"><span class=\"bar\" style=\"background: %s; color: %s; padding: 5px %dpx;\">%d%%</span></td></tr>\n", backgrounds[i], colors[i], level * 2, level);
  }

  if (printer->web_forms)
  {
    html_printf(client, "<tr><td></td><td colspan=\"2\"><input type=\"submit\" value=\"Update Supplies\">");
    if (num_options > 0)
      html_printf(client, " <span class=\"badge\" id=\"status\">Supplies updated.</span>\n");
    html_printf(client, "</td></tr>\n</table>\n</form>\n");

    if (num_options > 0)
      html_printf(client, "<script>\n"
			  "setTimeout(hide_status, 3000);\n"
			  "function hide_status() {\n"
			  "  var status = document.getElementById('status');\n"
			  "  status.style.display = 'none';\n"
			  "}\n"
			  "</script>\n");
  }
  else
    html_printf(client, "</table>\n");

  cupsFreeOptions(num_options, options);

  html_footer(client);

  return (1);
}


#ifndef _WIN32
//
// 'signal_handler()' - Handle termination signals.
//

static void
signal_handler(int signum)		// I - Signal number (not used)
{
  (void)signum;

  StopPrinter = true;
}
#endif // !_WIN32


//
// 'time_string()' - Return the local time in hours, minutes, and seconds.
//

static char *
time_string(time_t tv,			// I - Time value
            char   *buffer,		// I - Buffer
	    size_t bufsize)		// I - Size of buffer
{
  struct tm	date;			// Local time and date

  localtime_r(&tv, &date);

  strftime(buffer, bufsize, "%X", &date);

  return (buffer);
}


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

static int				// O - Exit status
usage(FILE *out)			// I - Output file
{
  cupsLangPuts(out, _("Usage: ippeveprinter [OPTIONS] \"NAME\""));
  cupsLangPuts(out, _("Options:"));
  cupsLangPuts(out, _("--help                         Show this help"));
  cupsLangPuts(out, _("--no-web-forms                 Disable web forms for media and supplies"));
  cupsLangPuts(out, _("--pam-service SERVICE          Use the named PAM service"));
  cupsLangPuts(out, _("--version                      Show the program version"));
  cupsLangPuts(out, _("-2                             Set 2-sided printing support (default=1-sided)"));
  cupsLangPuts(out, _("-a FILENAME                    Load printer attributes from IPP file"));
  cupsLangPuts(out, _("-A                             Enable authentication"));
  cupsLangPuts(out, _("-c COMMAND                     Set print command"));
  cupsLangPuts(out, _("-d SPOOL-DIRECTORY             Set spool directory"));
  cupsLangPuts(out, _("-D DEVICE-URI                  Set the device URI for the printer"));
  cupsLangPuts(out, _("-f TYPE/SUBTYPE[,...]          Set supported file types"));
  cupsLangPuts(out, _("-F TYPE/SUBTYPE                Set the output format for the printer"));
  cupsLangPuts(out, _("-i ICONFILE.png[,...]          Set icon file(s)"));
  cupsLangPuts(out, _("-k                             Keep job spool files"));
  cupsLangPuts(out, _("-K KEYSTORE                    Set location of server X.509 certificates and keys."));
  cupsLangPuts(out, _("-l LOCATION                    Set location of printer"));
  cupsLangPuts(out, _("-L LOGFILE                     Set log file (start with '+' to append)"));
  cupsLangPuts(out, _("-m MODEL                       Set model name (default=Printer)"));
  cupsLangPuts(out, _("-M MANUFACTURER                Set manufacturer name (default=Test)"));
  cupsLangPuts(out, _("-n HOSTNAME                    Set hostname for printer"));
  cupsLangPuts(out, _("-p PORT                        Set port number for printer"));
  cupsLangPuts(out, _("-r SUBTYPE[,...]               Set DNS-SD service subtype"));
  cupsLangPuts(out, _("-s SPEED[,COLOR-SPEED]         Set speed in pages per minute"));
  cupsLangPuts(out, _("-S FILENAME.strings            Set strings file"));
  cupsLangPuts(out, _("-v                             Be verbose"));
  cupsLangPuts(out, _("-V VERSION                     Set default IPP version"));

  return (out == stdout ? 0 : 1);
}


//
// 'valid_doc_attributes()' - Determine whether the document attributes are
//                            valid.
//
// When one or more document attributes are invalid, this function adds a
// suitable response and attributes to the unsupported group.
//

static bool				// O - `true` if valid, `false` if not
valid_doc_attributes(
    ippeve_client_t *client)		// I - Client
{
  bool			valid = true;	// Valid attributes?
  ipp_op_t		op = ippGetOperation(client->request);
					// IPP operation
  const char		*op_name = ippOpString(op);
					// IPP operation name
  ipp_attribute_t	*attr,		// Current attribute
			*supported;	// xxx-supported attribute
  const char		*compression = NULL,
					// compression value
			*format = NULL;	// document-format value


  // Check operation attributes...
  if ((attr = ippFindAttribute(client->request, "compression", IPP_TAG_ZERO)) != NULL)
  {
    // If compression is specified, only accept a supported value in a Print-Job
    // or Send-Document request...
    compression = ippGetString(attr, 0, NULL);
    supported   = ippFindAttribute(client->printer->attrs,
                                   "compression-supported", IPP_TAG_KEYWORD);

    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_KEYWORD ||
        ippGetGroupTag(attr) != IPP_TAG_OPERATION ||
        (op != IPP_OP_PRINT_JOB && op != IPP_OP_SEND_DOCUMENT &&
         op != IPP_OP_VALIDATE_JOB) ||
        !ippContainsString(supported, compression))
    {
      respond_unsupported(client, attr);
      valid = false;
    }
    else
    {
      fprintf(stderr, "%s %s compression=\"%s\"\n", client->hostname, op_name, compression);

      ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_KEYWORD, "compression-supplied", NULL, compression);

      if (strcmp(compression, "none"))
      {
	if (Verbosity)
	  fprintf(stderr, "Receiving job file with \"%s\" compression.\n", compression);
        httpSetField(client->http, HTTP_FIELD_CONTENT_ENCODING, compression);
      }
    }
  }

  // Is it a format we support?
  if ((attr = ippFindAttribute(client->request, "document-format", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_MIMETYPE ||
        ippGetGroupTag(attr) != IPP_TAG_OPERATION)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
    else
    {
      format = ippGetString(attr, 0, NULL);

      fprintf(stderr, "%s %s document-format=\"%s\"\n", client->hostname, op_name, format);

      ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_MIMETYPE, "document-format-supplied", NULL, format);
    }
  }
  else
  {
    format = ippGetString(ippFindAttribute(client->printer->attrs, "document-format-default", IPP_TAG_MIMETYPE), 0, NULL);
    if (!format)
      format = "application/octet-stream"; // Should never happen

    attr = ippAddString(client->request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, format);
  }

  if (format && !strcmp(format, "application/octet-stream") && (ippGetOperation(client->request) == IPP_OP_PRINT_JOB || ippGetOperation(client->request) == IPP_OP_SEND_DOCUMENT))
  {
    // Auto-type the file using the first 8 bytes of the file...
    unsigned char	header[8];	// First 8 bytes of file

    memset(header, 0, sizeof(header));
    httpPeek(client->http, (char *)header, sizeof(header));

    fprintf(stderr, "%s %s Auto-type header: %02X%02X%02X%02X%02X%02X%02X%02X\n", client->hostname, op_name, header[0], header[1], header[2], header[3], header[4], header[5], header[6], header[7]);
    if (!memcmp(header, "%PDF", 4))
      format = "application/pdf";
    else if (!memcmp(header, "%!", 2))
      format = "application/postscript";
    else if (!memcmp(header, "\377\330\377", 3) && header[3] >= 0xe0 && header[3] <= 0xef)
      format = "image/jpeg";
    else if (!memcmp(header, "\211PNG", 4))
      format = "image/png";
    else if (!memcmp(header, "RaS2PwgR", 8))
      format = "image/pwg-raster";
    else if (!memcmp(header, "UNIRAST", 8))
      format = "image/urf";
    else
      format = NULL;

    if (format)
    {
      fprintf(stderr, "%s %s Auto-typed document-format=\"%s\"\n", client->hostname, op_name, format);

      ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_MIMETYPE, "document-format-detected", NULL, format);
    }
  }

  if (op != IPP_OP_CREATE_JOB && (supported = ippFindAttribute(client->printer->attrs, "document-format-supported", IPP_TAG_MIMETYPE)) != NULL && !ippContainsString(supported, format))
  {
    respond_unsupported(client, attr);
    valid = false;
  }

  // document-name
  if ((attr = ippFindAttribute(client->request, "document-name", IPP_TAG_NAME)) != NULL)
    ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_NAME, "document-name-supplied", NULL, ippGetString(attr, 0, NULL));

  return (valid);
}


//
// 'valid_job_attributes()' - Determine whether the job attributes are valid.
//
// When one or more job attributes are invalid, this function adds a suitable
// response and attributes to the unsupported group.
//

static bool				// O - `true` if valid, `false` if not
valid_job_attributes(
    ippeve_client_t *client)		// I - Client
{
  size_t		i,		// Looping var
			count;		// Number of values
  bool			fidelity,	// "ipp-attribute-fidelity" value
			valid = true;	// Valid attributes?
  ipp_attribute_t	*attr,		// Current attribute
			*attr_col,	// xxx-col attribute
			*supported;	// xxx-supported attribute


  // Check operation attributes...
  valid = valid_doc_attributes(client);

  // Check the various job template attributes...
  if ((attr = ippFindAttribute(client->request, "ipp-attribute-fidelity", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_BOOLEAN)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  fidelity = ippGetBoolean(attr, 0);

  if ((attr = ippFindAttribute(client->request, "copies", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER ||
        ippGetInteger(attr, 0) < 1 || ippGetInteger(attr, 0) > 999)
    {
      if (fidelity)
      {
        respond_unsupported(client, attr);
        valid = false;
      }
      else
      {
        respond_ignored(client, attr);
        ippDeleteAttribute(client->request, attr);
      }
    }
  }

  attr_col = ippFindAttribute(client->request, "finishings-col", IPP_TAG_ZERO);

  if ((attr = ippFindAttribute(client->request, "finishings", IPP_TAG_ZERO)) != NULL)
  {
    if (attr_col)
    {
      // Cannot specify both finishings and finishings-col...
      respond_unsupported(client, attr);
      respond_unsupported(client, attr_col);
      ippSetStatusCode(client->response, IPP_STATUS_ERROR_BAD_REQUEST);
      valid = false;
    }
    else if (ippGetValueTag(attr) != IPP_TAG_ENUM)
    {
      if (fidelity)
      {
        respond_unsupported(client, attr);
        valid = false;
      }
      else
      {
        respond_ignored(client, attr);
        ippDeleteAttribute(client->request, attr);
      }
    }
    else
    {
      supported = ippFindAttribute(client->printer->attrs, "finishings-supported", IPP_TAG_ENUM);

      for (i = 0, count = ippGetCount(attr); i < count; i ++)
      {
        if (!ippContainsInteger(supported, ippGetInteger(attr, i)))
          break;
      }

      if (i < count)
      {
	if (fidelity)
	{
	  respond_unsupported(client, attr);
	  valid = false;
	}
	else
	{
	  respond_ignored(client, attr);
	  ippDeleteAttribute(client->request, attr);
	}
      }
    }
  }
  else if (attr_col)
  {
    if (ippGetValueTag(attr_col) != IPP_TAG_BEGIN_COLLECTION)
    {
      if (fidelity)
      {
        respond_unsupported(client, attr_col);
        valid = false;
      }
      else
      {
        respond_ignored(client, attr_col);
        ippDeleteAttribute(client->request, attr_col);
      }
    }
    else
    {
      supported = ippFindAttribute(client->printer->attrs, "finishing-template-supported", IPP_TAG_ZERO);

      for (i = 0, count = ippGetCount(attr_col); i < count; i ++)
      {
        if (!ippContainsString(supported, ippGetString(ippFindAttribute(ippGetCollection(attr_col, i), "finishing-template", IPP_TAG_ZERO), 0, NULL)))
          break;
      }

      if (i < count)
      {
	if (fidelity)
	{
	  respond_unsupported(client, attr_col);
	  valid = false;
	}
	else
	{
	  respond_ignored(client, attr_col);
	  ippDeleteAttribute(client->request, attr_col);
	}
      }
    }
  }

  if ((attr = ippFindAttribute(client->request, "job-hold-until", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 ||
        (ippGetValueTag(attr) != IPP_TAG_NAME &&
	 ippGetValueTag(attr) != IPP_TAG_NAMELANG &&
	 ippGetValueTag(attr) != IPP_TAG_KEYWORD) ||
	strcmp(ippGetString(attr, 0, NULL), "no-hold"))
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "job-impressions", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER || ippGetInteger(attr, 0) < 0)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "job-name", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 ||
        (ippGetValueTag(attr) != IPP_TAG_NAME &&
	 ippGetValueTag(attr) != IPP_TAG_NAMELANG))
    {
      respond_unsupported(client, attr);
      valid = false;
    }

    ippSetGroupTag(client->request, &attr, IPP_TAG_JOB);
  }
  else
  {
    ippAddString(client->request, IPP_TAG_JOB, IPP_TAG_NAME, "job-name", NULL, "Untitled");
  }

  if ((attr = ippFindAttribute(client->request, "job-priority", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER ||
        ippGetInteger(attr, 0) < 1 || ippGetInteger(attr, 0) > 100)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "job-sheets", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 ||
        (ippGetValueTag(attr) != IPP_TAG_NAME &&
	 ippGetValueTag(attr) != IPP_TAG_NAMELANG &&
	 ippGetValueTag(attr) != IPP_TAG_KEYWORD) ||
	strcmp(ippGetString(attr, 0, NULL), "none"))
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  attr_col = ippFindAttribute(client->request, "media-col", IPP_TAG_ZERO);

  if ((attr = ippFindAttribute(client->request, "media", IPP_TAG_ZERO)) != NULL)
  {
    if (attr_col)
    {
      // Cannot send both media and media-col in the same request, this is a
      // bad request...
      respond_unsupported(client, attr);
      respond_unsupported(client, attr_col);
      ippSetStatusCode(client->response, IPP_STATUS_ERROR_BAD_REQUEST);
      valid = false;
    }
    else if (ippGetCount(attr) != 1 || (ippGetValueTag(attr) != IPP_TAG_NAME && ippGetValueTag(attr) != IPP_TAG_NAMELANG && ippGetValueTag(attr) != IPP_TAG_KEYWORD))
    {
      respond_unsupported(client, attr);
      valid = false;
    }
    else
    {
      supported = ippFindAttribute(client->printer->attrs, "media-supported", IPP_TAG_KEYWORD);

      if (!ippContainsString(supported, ippGetString(attr, 0, NULL)))
      {
        if (fidelity)
        {
	  respond_unsupported(client, attr);
	  valid = false;
	}
	else
	{
	  respond_ignored(client, attr);
	  ippDeleteAttribute(client->request, attr);
	}
      }
    }
  }
  else if (attr_col)
  {
    ipp_t		*col,		// media-col collection
			*size;		// media-size collection
    ipp_attribute_t	*member,	// Member attribute
			*size_name,	// "media-size-name" member attribute
			*x_dim,		// x-dimension
			*y_dim;		// y-dimension
    int			x_value,	// y-dimension value
			y_value;	// x-dimension value

    if (ippGetCount(attr_col) != 1 || ippGetValueTag(attr_col) != IPP_TAG_BEGIN_COLLECTION)
    {
      respond_unsupported(client, attr_col);
      valid = false;
    }

    col = ippGetCollection(attr_col, 0);

    member = ippFindAttribute(col, "media-size", IPP_TAG_BEGIN_COLLECTION);
    if ((size_name = ippFindAttribute(col, "media-size-name", IPP_TAG_ZERO)) != NULL)
    {
      if (member)
      {
        // Cannot specify both media-size and media-size-name...
	respond_unsupported(client, attr_col);
	ippSetStatusCode(client->response, IPP_STATUS_ERROR_BAD_REQUEST);
	valid = false;
      }
      else if (ippGetCount(size_name) != 1 || (ippGetValueTag(size_name) != IPP_TAG_NAME && ippGetValueTag(size_name) != IPP_TAG_NAMELANG && ippGetValueTag(size_name) != IPP_TAG_KEYWORD))
      {
	respond_unsupported(client, attr_col);
	valid = false;
      }
      else
      {
	supported = ippFindAttribute(client->printer->attrs, "media-supported", IPP_TAG_KEYWORD);

	if (!ippContainsString(supported, ippGetString(size_name, 0, NULL)))
	{
	  if (fidelity)
	  {
	    respond_unsupported(client, attr_col);
	    valid = false;
	  }
	  else
	  {
	    respond_ignored(client, attr_col);
	    ippDeleteAttribute(client->request, attr_col);
	  }
	}
      }
    }
    else if (member)
    {
      if (ippGetCount(member) != 1)
      {
	respond_unsupported(client, attr_col);
	valid = false;
      }
      else
      {
	size = ippGetCollection(member, 0);

	if ((x_dim = ippFindAttribute(size, "x-dimension", IPP_TAG_INTEGER)) == NULL || ippGetCount(x_dim) != 1 ||
	    (y_dim = ippFindAttribute(size, "y-dimension", IPP_TAG_INTEGER)) == NULL || ippGetCount(y_dim) != 1)
	{
	  respond_unsupported(client, attr_col);
	  valid = false;
	}
	else
	{
	  x_value   = ippGetInteger(x_dim, 0);
	  y_value   = ippGetInteger(y_dim, 0);
	  supported = ippFindAttribute(client->printer->attrs, "media-size-supported", IPP_TAG_BEGIN_COLLECTION);
	  count     = ippGetCount(supported);

	  for (i = 0; i < count ; i ++)
	  {
	    int	x_min, x_max;		// Min/max width
	    int y_min, y_max;		// Min/max length

	    size  = ippGetCollection(supported, i);
	    x_dim = ippFindAttribute(size, "x-dimension", IPP_TAG_ZERO);
	    y_dim = ippFindAttribute(size, "y-dimension", IPP_TAG_ZERO);

            if (ippGetValueTag(x_dim) == IPP_TAG_INTEGER)
            {
              x_min = ippGetInteger(x_dim, 0) - 100;
              x_max = ippGetInteger(x_dim, 0) + 100;
            }
            else
            {
              x_min = ippGetRange(x_dim, 0, &x_max);
            }

            if (ippGetValueTag(y_dim) == IPP_TAG_INTEGER)
            {
              y_min = ippGetInteger(y_dim, 0) - 100;
              y_max = ippGetInteger(y_dim, 0) + 100;
            }
            else
            {
              y_min = ippGetRange(y_dim, 0, &y_max);
            }

	    if ((x_value < x_min || x_value > x_max) && (y_value < y_min || y_value > y_max))
	      break;
	  }

	  if (i >= count)
	  {
	    if (fidelity)
	    {
	      respond_unsupported(client, attr_col);
	      valid = false;
	    }
	    else
	    {
	      respond_ignored(client, attr_col);
	      ippDeleteAttribute(client->request, attr_col);
	    }
	  }
	}
      }
    }
  }

  if ((attr = ippFindAttribute(client->request, "multiple-document-handling", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_KEYWORD ||
        (strcmp(ippGetString(attr, 0, NULL),
		"separate-documents-uncollated-copies") &&
	 strcmp(ippGetString(attr, 0, NULL),
		"separate-documents-collated-copies")))
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "orientation-requested", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_ENUM ||
        ippGetInteger(attr, 0) < IPP_ORIENT_PORTRAIT ||
        ippGetInteger(attr, 0) > IPP_ORIENT_REVERSE_PORTRAIT)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "page-ranges", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetValueTag(attr) != IPP_TAG_RANGE)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "print-quality", IPP_TAG_ZERO)) != NULL)
  {
    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_ENUM ||
        ippGetInteger(attr, 0) < IPP_QUALITY_DRAFT ||
        ippGetInteger(attr, 0) > IPP_QUALITY_HIGH)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
  }

  if ((attr = ippFindAttribute(client->request, "printer-resolution", IPP_TAG_ZERO)) != NULL)
  {
    supported = ippFindAttribute(client->printer->attrs, "printer-resolution-supported", IPP_TAG_RESOLUTION);

    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_RESOLUTION ||
        !supported)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
    else
    {
      int	xdpi,			// Horizontal resolution for job template attribute
		ydpi,			// Vertical resolution for job template attribute
		sydpi;			// Vertical resolution for supported value
      ipp_res_t	units,			// Units for job template attribute
		sunits;			// Units for supported value

      xdpi  = ippGetResolution(attr, 0, &ydpi, &units);
      count = ippGetCount(supported);

      for (i = 0; i < count; i ++)
      {
        if (xdpi == ippGetResolution(supported, i, &sydpi, &sunits) && ydpi == sydpi && units == sunits)
          break;
      }

      if (i >= count)
      {
        if (fidelity)
        {
	  respond_unsupported(client, attr);
	  valid = false;
	}
	else
	{
	  respond_ignored(client, attr);
	  ippDeleteAttribute(client->request, attr);
	}
      }
    }
  }

  if ((attr = ippFindAttribute(client->request, "sides", IPP_TAG_ZERO)) != NULL)
  {
    const char *sides = ippGetString(attr, 0, NULL);
					// "sides" value...

    if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_KEYWORD)
    {
      respond_unsupported(client, attr);
      valid = false;
    }
    else if ((supported = ippFindAttribute(client->printer->attrs, "sides-supported", IPP_TAG_KEYWORD)) != NULL)
    {
      if (!ippContainsString(supported, sides))
      {
        if (fidelity)
        {
	  respond_unsupported(client, attr);
	  valid = false;
	}
	else
	{
	  respond_ignored(client, attr);
	  ippDeleteAttribute(client->request, attr);
	}
      }
    }
    else if (strcmp(sides, "one-sided"))
    {
      if (fidelity)
      {
	respond_unsupported(client, attr);
	valid = false;
      }
      else
      {
        respond_ignored(client, attr);
        ippDeleteAttribute(client->request, attr);
      }
    }
  }

  return (valid);
}
