1/*
2 * "$Id: ippdiscover.c 11093 2013-07-03 20:48:42Z msweet $"
3 *
4 *   ippdiscover command for CUPS.
5 *
6 *   Copyright 2007-2013 by Apple Inc.
7 *   Copyright 1997-2007 by Easy Software Products.
8 *
9 *   These coded instructions, statements, and computer programs are the
10 *   property of Apple Inc. and are protected by Federal copyright
11 *   law.  Distribution and use rights are outlined in the file "LICENSE.txt"
12 *   which should have been included with this file.  If this file is
13 *   file is missing or damaged, see the license at "http://www.cups.org/".
14 *
15 *   This file is subject to the Apple OS-Developed Software exception.
16 *
17 * Contents:
18 *
19 */
20
21
22/*
23 * Include necessary headers.
24 */
25
26#include <cups/cups-private.h>
27#ifdef HAVE_DNSSD
28#  include <dns_sd.h>
29#  ifdef WIN32
30#    pragma comment(lib, "dnssd.lib")
31#  endif /* WIN32 */
32#endif /* HAVE_DNSSD */
33#ifdef HAVE_AVAHI
34#  include <avahi-client/client.h>
35#  include <avahi-client/lookup.h>
36#  include <avahi-common/simple-watch.h>
37#  include <avahi-common/domain.h>
38#  include <avahi-common/error.h>
39#  include <avahi-common/malloc.h>
40#define kDNSServiceMaxDomainName AVAHI_DOMAIN_NAME_MAX
41#endif /* HAVE_AVAHI */
42
43
44/*
45 * Local globals...
46 */
47
48#ifdef HAVE_AVAHI
49static int		got_data = 0;	/* Got data from poll? */
50static AvahiSimplePoll	*simple_poll = NULL;
51					/* Poll information */
52#endif /* HAVE_AVAHI */
53static const char	*program = NULL;/* Program to run */
54
55
56/*
57 * Local functions...
58 */
59
60#ifdef HAVE_DNSSD
61static void DNSSD_API	browse_callback(DNSServiceRef sdRef,
62			                DNSServiceFlags flags,
63				        uint32_t interfaceIndex,
64				        DNSServiceErrorType errorCode,
65				        const char *serviceName,
66				        const char *regtype,
67				        const char *replyDomain, void *context)
68					__attribute__((nonnull(1,5,6,7,8)));
69static void DNSSD_API	resolve_cb(DNSServiceRef sdRef,
70				   DNSServiceFlags flags,
71				   uint32_t interfaceIndex,
72				   DNSServiceErrorType errorCode,
73				   const char *fullName,
74				   const char *hostTarget,
75				   uint16_t port, uint16_t txtLen,
76				   const unsigned char *txtRecord,
77				   void *context);
78#endif /* HAVE_DNSSD */
79
80#ifdef HAVE_AVAHI
81static void		browse_callback(AvahiServiceBrowser *browser,
82					AvahiIfIndex interface,
83					AvahiProtocol protocol,
84					AvahiBrowserEvent event,
85					const char *serviceName,
86					const char *regtype,
87					const char *replyDomain,
88					AvahiLookupResultFlags flags,
89					void *context);
90static void		client_cb(AvahiClient *client, AvahiClientState state,
91				  void *simple_poll);
92static int		poll_cb(struct pollfd *pollfds, unsigned int num_pollfds,
93				int timeout, void *context);
94static void		resolve_cb(AvahiServiceResolver *resolver,
95				   AvahiIfIndex interface,
96				   AvahiProtocol protocol,
97				   AvahiResolverEvent event,
98				   const char *name, const char *type,
99				   const char *domain, const char *host_name,
100				   const AvahiAddress *address, uint16_t port,
101				   AvahiStringList *txt,
102				   AvahiLookupResultFlags flags, void *context);
103#endif /* HAVE_AVAHI */
104
105static void		resolve_and_run(const char *name, const char *type,
106			                const char *domain);
107static void		unquote(char *dst, const char *src, size_t dstsize);
108static void		usage(void) __attribute__((noreturn));
109
110
111/*
112 * 'main()' - Browse for printers and run the specified command.
113 */
114
115int					/* O - Exit status */
116main(int  argc,				/* I - Number of command-line args */
117     char *argv[])			/* I - Command-line arguments */
118{
119  int		i;			/* Looping var */
120  const char	*opt,			/* Current option character */
121		*name = NULL,		/* Service name */
122		*type = "_ipp._tcp",	/* Service type */
123		*domain = "local.";	/* Service domain */
124#ifdef HAVE_DNSSD
125  DNSServiceRef	ref;			/* Browsing service reference */
126#endif /* HAVE_DNSSD */
127#ifdef HAVE_AVAHI
128  AvahiClient	*client;		/* Client information */
129  int		error;			/* Error code, if any */
130#endif /* HAVE_AVAHI */
131
132
133  for (i = 1; i < argc; i ++)
134    if (!strcmp(argv[i], "snmp"))
135      snmponly = 1;
136    else if (!strcmp(argv[i], "ipp"))
137      ipponly = 1;
138    else
139    {
140      puts("Usage: ./ipp-printers [{ipp | snmp}]");
141      return (1);
142    }
143
144 /*
145  * Create an array to track devices...
146  */
147
148  devices = cupsArrayNew((cups_array_func_t)compare_devices, NULL);
149
150 /*
151  * Browse for different kinds of printers...
152  */
153
154  if (DNSServiceCreateConnection(&main_ref) != kDNSServiceErr_NoError)
155  {
156    perror("ERROR: Unable to create service connection");
157    return (1);
158  }
159
160  fd = DNSServiceRefSockFD(main_ref);
161
162  ipp_ref = main_ref;
163  DNSServiceBrowse(&ipp_ref, kDNSServiceFlagsShareConnection, 0,
164                   "_ipp._tcp", NULL, browse_callback, devices);
165
166 /*
167  * Loop until we are killed...
168  */
169
170  progress();
171
172  for (;;)
173  {
174    FD_ZERO(&input);
175    FD_SET(fd, &input);
176
177    timeout.tv_sec  = 2;
178    timeout.tv_usec = 500000;
179
180    if (select(fd + 1, &input, NULL, NULL, &timeout) <= 0)
181    {
182      time_t curtime = time(NULL);
183
184      for (device = (cups_device_t *)cupsArrayFirst(devices);
185           device;
186	   device = (cups_device_t *)cupsArrayNext(devices))
187        if (!device->got_resolve)
188        {
189          if (!device->ref)
190            break;
191
192          if ((curtime - device->resolve_time) > 10)
193          {
194            device->got_resolve = -1;
195	    fprintf(stderr, "\rUnable to resolve \"%s\": timeout\n",
196		    device->name);
197	    progress();
198	  }
199          else
200            break;
201        }
202
203      if (!device)
204        break;
205    }
206
207    if (FD_ISSET(fd, &input))
208    {
209     /*
210      * Process results of our browsing...
211      */
212
213      progress();
214      DNSServiceProcessResult(main_ref);
215    }
216    else
217    {
218     /*
219      * Query any devices we've found...
220      */
221
222      DNSServiceErrorType	status;	/* DNS query status */
223      int			count;	/* Number of queries */
224
225
226      for (device = (cups_device_t *)cupsArrayFirst(devices), count = 0;
227           device;
228	   device = (cups_device_t *)cupsArrayNext(devices))
229      {
230        if (!device->ref && !device->sent)
231	{
232	 /*
233	  * Found the device, now get the TXT record(s) for it...
234	  */
235
236          if (count < 50)
237	  {
238	    device->resolve_time = time(NULL);
239	    device->ref          = main_ref;
240
241	    status = DNSServiceResolve(&(device->ref),
242				       kDNSServiceFlagsShareConnection,
243				       0, device->name, device->regtype,
244				       device->domain, resolve_callback,
245				       device);
246            if (status != kDNSServiceErr_NoError)
247            {
248	      fprintf(stderr, "\rUnable to resolve \"%s\": %d\n",
249	              device->name, status);
250	      progress();
251	    }
252	    else
253	      count ++;
254          }
255	}
256	else if (!device->sent && device->got_resolve)
257	{
258	 /*
259	  * Got the TXT records, now report the device...
260	  */
261
262	  DNSServiceRefDeallocate(device->ref);
263	  device->ref  = 0;
264	  device->sent = 1;
265        }
266      }
267    }
268  }
269
270#ifndef DEBUG
271  fprintf(stderr, "\rFound %d printers. Now querying for capabilities...\n",
272          cupsArrayCount(devices));
273#endif /* !DEBUG */
274
275  puts("#!/bin/sh -x");
276  puts("test -d results && rm -rf results");
277  puts("mkdir results");
278  puts("CUPS_DEBUG_LEVEL=6; export CUPS_DEBUG_LEVEL");
279  puts("CUPS_DEBUG_FILTER='^(ipp|http|_ipp|_http|cupsGetResponse|cupsSend|"
280       "cupsWrite|cupsDo).*'; export CUPS_DEBUG_FILTER");
281
282  for (device = (cups_device_t *)cupsArrayFirst(devices);
283       device;
284       device = (cups_device_t *)cupsArrayNext(devices))
285  {
286    if (device->got_resolve <= 0 || device->cups_shared)
287      continue;
288
289#ifdef DEBUG
290    fprintf(stderr, "Checking \"%s\" (got_resolve=%d, cups_shared=%d, uri=%s)\n",
291            device->name, device->got_resolve, device->cups_shared, device->uri);
292#else
293    fprintf(stderr, "Checking \"%s\"...\n", device->name);
294#endif /* DEBUG */
295
296    if ((http = httpConnect(device->host, device->port)) == NULL)
297    {
298      fprintf(stderr, "Failed to connect to \"%s\": %s\n", device->name,
299              cupsLastErrorString());
300      continue;
301    }
302
303    request = ippNewRequest(IPP_GET_PRINTER_ATTRIBUTES);
304    ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL,
305                 device->uri);
306
307    response = cupsDoRequest(http, request, device->rp);
308
309    if (cupsLastError() > IPP_OK_SUBST)
310      fprintf(stderr, "Failed to query \"%s\": %s\n", device->name,
311              cupsLastErrorString());
312    else
313    {
314      if ((attr = ippFindAttribute(response, "ipp-versions-supported",
315				   IPP_TAG_KEYWORD)) != NULL)
316      {
317	version = attr->values[0].string.text;
318
319	for (i = 1; i < attr->num_values; i ++)
320	  if (strcmp(attr->values[i].string.text, version) > 0)
321	    version = attr->values[i].string.text;
322      }
323      else
324	version = "1.0";
325
326      testfile = NULL;
327
328      if ((attr = ippFindAttribute(response, "document-format-supported",
329                                   IPP_TAG_MIMETYPE)) != NULL)
330      {
331       /*
332        * Figure out the test file for printing, preferring PDF and PostScript
333        * over JPEG and plain text...
334        */
335
336        for (i = 0; i < attr->num_values; i ++)
337        {
338          if (!strcasecmp(attr->values[i].string.text, "application/pdf"))
339          {
340            testfile = "testfile.pdf";
341            break;
342          }
343          else if (!strcasecmp(attr->values[i].string.text,
344                               "application/postscript"))
345            testfile = "testfile.ps";
346          else if (!strcasecmp(attr->values[i].string.text, "image/jpeg") &&
347                   !testfile)
348            testfile = "testfile.jpg";
349          else if (!strcasecmp(attr->values[i].string.text, "text/plain") &&
350                   !testfile)
351            testfile = "testfile.txt";
352          else if (!strcasecmp(attr->values[i].string.text,
353                               "application/vnd.hp-PCL") && !testfile)
354            testfile = "testfile.pcl";
355        }
356
357        if (!testfile)
358        {
359          fprintf(stderr,
360                  "Printer \"%s\" reports the following IPP file formats:\n",
361                  device->name);
362          for (i = 0; i < attr->num_values; i ++)
363            fprintf(stderr, "    \"%s\"\n", attr->values[i].string.text);
364        }
365      }
366
367      if (!testfile && device->pdl)
368      {
369	char	*pdl,			/* Copy of pdl string */
370		*start, *end;		/* Pointers into pdl string */
371
372
373        pdl = strdup(device->pdl);
374	for (start = device->pdl; start && *start; start = end)
375	{
376	  if ((end = strchr(start, ',')) != NULL)
377	    *end++ = '\0';
378
379	  if (!strcasecmp(start, "application/pdf"))
380	  {
381	    testfile = "testfile.pdf";
382	    break;
383	  }
384	  else if (!strcasecmp(start, "application/postscript"))
385	    testfile = "testfile.ps";
386	  else if (!strcasecmp(start, "image/jpeg") && !testfile)
387	    testfile = "testfile.jpg";
388	  else if (!strcasecmp(start, "text/plain") && !testfile)
389	    testfile = "testfile.txt";
390	  else if (!strcasecmp(start, "application/vnd.hp-PCL") && !testfile)
391	    testfile = "testfile.pcl";
392	}
393	free(pdl);
394
395        if (testfile)
396        {
397	  fprintf(stderr,
398		  "Using \"%s\" for printer \"%s\" based on TXT record pdl "
399		  "info.\n", testfile, device->name);
400        }
401        else
402        {
403	  fprintf(stderr,
404		  "Printer \"%s\" reports the following TXT file formats:\n",
405		  device->name);
406	  fprintf(stderr, "    \"%s\"\n", device->pdl);
407	}
408      }
409
410      if (!device->ty &&
411	  (attr = ippFindAttribute(response, "printer-make-and-model",
412				   IPP_TAG_TEXT)) != NULL)
413	device->ty = strdup(attr->values[0].string.text);
414
415      if (strcmp(version, "1.0") && testfile && device->ty)
416      {
417	char		filename[1024],	/* Filename */
418			*fileptr;	/* Pointer into filename */
419	const char	*typtr;		/* Pointer into ty */
420
421        if (!strncasecmp(device->ty, "DeskJet", 7) ||
422            !strncasecmp(device->ty, "DesignJet", 9) ||
423            !strncasecmp(device->ty, "OfficeJet", 9) ||
424            !strncasecmp(device->ty, "Photosmart", 10))
425          strlcpy(filename, "HP_", sizeof(filename));
426        else
427          filename[0] = '\0';
428
429	fileptr = filename + strlen(filename);
430
431        if (!strncasecmp(device->ty, "Lexmark International Lexmark", 29))
432          typtr = device->ty + 22;
433        else
434          typtr = device->ty;
435
436	while (*typtr && fileptr < (filename + sizeof(filename) - 1))
437	{
438	  if (isalnum(*typtr & 255) || *typtr == '-')
439	    *fileptr++ = *typtr++;
440	  else
441	  {
442	    *fileptr++ = '_';
443	    typtr++;
444	  }
445	}
446
447	*fileptr = '\0';
448
449        printf("# %s\n", device->name);
450        printf("echo \"Testing %s...\"\n", device->name);
451
452        if (!ipponly)
453        {
454	  printf("echo \"snmpwalk -c public -v 1 -Cc %s 1.3.6.1.2.1.25 "
455	         "1.3.6.1.2.1.43 1.3.6.1.4.1.2699.1\" > results/%s.snmpwalk\n",
456	         device->host, filename);
457	  printf("snmpwalk -c public -v 1 -Cc %s 1.3.6.1.2.1.25 "
458	         "1.3.6.1.2.1.43 1.3.6.1.4.1.2699.1 | "
459	         "tee -a results/%s.snmpwalk\n",
460	         device->host, filename);
461        }
462
463        if (!snmponly)
464        {
465	  printf("echo \"./ipptool-static -tIf %s -T 30 -d NOPRINT=1 -V %s %s "
466	         "ipp-%s.test\" > results/%s.log\n", testfile, version,
467	         device->uri, version, filename);
468	  printf("CUPS_DEBUG_LOG=results/%s.debug_log "
469	         "./ipptool-static -tIf %s -T 30 -d NOPRINT=1 -V %s %s "
470	         "ipp-%s.test | tee -a results/%s.log\n", filename,
471	         testfile, version, device->uri,
472	         version, filename);
473        }
474
475	puts("");
476      }
477      else if (!device->ty)
478	fprintf(stderr,
479		"Ignoring \"%s\" since it doesn't provide a make and model.\n",
480		device->name);
481      else if (!testfile)
482	fprintf(stderr,
483	        "Ignoring \"%s\" since it does not support a common format.\n",
484		device->name);
485      else
486	fprintf(stderr, "Ignoring \"%s\" since it only supports IPP/1.0.\n",
487		device->name);
488    }
489
490    ippDelete(response);
491    httpClose(http);
492  }
493
494  return (0);
495}
496
497
498/*
499 * 'browse_callback()' - Browse devices.
500 */
501
502static void
503browse_callback(
504    DNSServiceRef       sdRef,		/* I - Service reference */
505    DNSServiceFlags     flags,		/* I - Option flags */
506    uint32_t            interfaceIndex,	/* I - Interface number */
507    DNSServiceErrorType errorCode,	/* I - Error, if any */
508    const char          *serviceName,	/* I - Name of service/device */
509    const char          *regtype,	/* I - Type of service */
510    const char          *replyDomain,	/* I - Service domain */
511    void                *context)	/* I - Devices array */
512{
513#ifdef DEBUG
514  fprintf(stderr, "browse_callback(sdRef=%p, flags=%x, "
515                  "interfaceIndex=%d, errorCode=%d, serviceName=\"%s\", "
516		  "regtype=\"%s\", replyDomain=\"%s\", context=%p)\n",
517          sdRef, flags, interfaceIndex, errorCode,
518	  serviceName ? serviceName : "(null)",
519	  regtype ? regtype : "(null)",
520	  replyDomain ? replyDomain : "(null)",
521	  context);
522#endif /* DEBUG */
523
524 /*
525  * Only process "add" data...
526  */
527
528  if (errorCode != kDNSServiceErr_NoError || !(flags & kDNSServiceFlagsAdd))
529    return;
530
531 /*
532  * Get the device...
533  */
534
535  get_device((cups_array_t *)context, serviceName, regtype, replyDomain);
536}
537
538
539/*
540 * 'compare_devices()' - Compare two devices.
541 */
542
543static int				/* O - Result of comparison */
544compare_devices(cups_device_t *a,	/* I - First device */
545                cups_device_t *b)	/* I - Second device */
546{
547  int retval = strcmp(a->name, b->name);
548
549  if (retval)
550    return (retval);
551  else
552    return (-strcmp(a->regtype, b->regtype));
553}
554
555
556/*
557 * 'get_device()' - Create or update a device.
558 */
559
560static cups_device_t *			/* O - Device */
561get_device(cups_array_t *devices,	/* I - Device array */
562           const char   *serviceName,	/* I - Name of service/device */
563           const char   *regtype,	/* I - Type of service */
564           const char   *replyDomain)	/* I - Service domain */
565{
566  cups_device_t	key,			/* Search key */
567		*device;		/* Device */
568  char		fullName[kDNSServiceMaxDomainName];
569					/* Full name for query */
570
571
572 /*
573  * See if this is a new device...
574  */
575
576  key.name    = (char *)serviceName;
577  key.regtype = (char *)regtype;
578
579  for (device = cupsArrayFind(devices, &key);
580       device;
581       device = cupsArrayNext(devices))
582    if (strcasecmp(device->name, key.name))
583      break;
584    else
585    {
586      if (!strcasecmp(device->domain, "local.") &&
587          strcasecmp(device->domain, replyDomain))
588      {
589       /*
590        * Update the .local listing to use the "global" domain name instead.
591	* The backend will try local lookups first, then the global domain name.
592	*/
593
594        free(device->domain);
595	device->domain = strdup(replyDomain);
596
597	DNSServiceConstructFullName(fullName, device->name, regtype,
598	                            replyDomain);
599	free(device->fullName);
600	device->fullName = strdup(fullName);
601      }
602
603      return (device);
604    }
605
606 /*
607  * Yes, add the device...
608  */
609
610  device          = calloc(sizeof(cups_device_t), 1);
611  device->name    = strdup(serviceName);
612  device->domain  = strdup(replyDomain);
613  device->regtype = strdup(regtype);
614
615  cupsArrayAdd(devices, device);
616
617 /*
618  * Set the "full name" of this service, which is used for queries...
619  */
620
621  DNSServiceConstructFullName(fullName, serviceName, regtype, replyDomain);
622  device->fullName = strdup(fullName);
623
624#ifdef DEBUG
625  fprintf(stderr, "get_device: fullName=\"%s\"...\n", fullName);
626#endif /* DEBUG */
627
628  return (device);
629}
630
631
632/*
633 * 'progress()' - Show query progress.
634 */
635
636static void
637progress(void)
638{
639#ifndef DEBUG
640  const char	*chars = "|/-\\";
641  static int	count = 0;
642
643
644  fprintf(stderr, "\rLooking for printers %c", chars[count]);
645  fflush(stderr);
646  count = (count + 1) & 3;
647#endif /* !DEBUG */
648}
649
650
651/*
652 * 'resolve_callback()' - Process resolve data.
653 */
654
655static void
656resolve_callback(
657    DNSServiceRef       sdRef,		/* I - Service reference */
658    DNSServiceFlags     flags,		/* I - Data flags */
659    uint32_t            interfaceIndex,	/* I - Interface */
660    DNSServiceErrorType errorCode,	/* I - Error, if any */
661    const char          *fullName,	/* I - Full service name */
662    const char          *hostTarget,	/* I - Hostname */
663    uint16_t            port,		/* I - Port number (network byte order) */
664    uint16_t            txtLen,		/* I - Length of TXT record data */
665    const unsigned char *txtRecord,	/* I - TXT record data */
666    void                *context)	/* I - Device */
667{
668  char		temp[257],		/* TXT key value */
669		uri[1024];		/* Printer URI */
670  const void	*value;			/* Value from TXT record */
671  uint8_t	valueLen;		/* Length of value */
672  cups_device_t	*device = (cups_device_t *)context;
673					/* Device */
674
675
676#ifdef DEBUG
677  fprintf(stderr, "\rresolve_callback(sdRef=%p, flags=%x, "
678                  "interfaceIndex=%d, errorCode=%d, fullName=\"%s\", "
679		  "hostTarget=\"%s\", port=%d, txtLen=%u, txtRecord=%p, "
680		  "context=%p)\n",
681          sdRef, flags, interfaceIndex, errorCode,
682	  fullName ? fullName : "(null)", hostTarget ? hostTarget : "(null)",
683	  ntohs(port), txtLen, txtRecord, context);
684#endif /* DEBUG */
685
686 /*
687  * Only process "add" data...
688  */
689
690  if (errorCode != kDNSServiceErr_NoError)
691    return;
692
693  device->got_resolve = 1;
694  device->host        = strdup(hostTarget);
695  device->port        = ntohs(port);
696
697 /*
698  * Extract the "remote printer" key from the TXT record and save the URI...
699  */
700
701  if ((value = TXTRecordGetValuePtr(txtLen, txtRecord, "rp",
702                                    &valueLen)) != NULL)
703  {
704    if (((char *)value)[0] == '/')
705    {
706     /*
707      * "rp" value (incorrectly) has a leading slash already...
708      */
709
710      memcpy(temp, value, valueLen);
711      temp[valueLen] = '\0';
712    }
713    else
714    {
715     /*
716      * Convert to resource by concatenating with a leading "/"...
717      */
718
719      temp[0] = '/';
720      memcpy(temp + 1, value, valueLen);
721      temp[valueLen + 1] = '\0';
722    }
723  }
724  else
725  {
726   /*
727    * Default "rp" value is blank, mapping to a path of "/"...
728    */
729
730    temp[0] = '/';
731    temp[1] = '\0';
732  }
733
734  if (!strncmp(temp, "/printers/", 10))
735    device->cups_shared = -1;
736
737  httpAssembleURI(HTTP_URI_CODING_ALL, uri, sizeof(uri), "ipp",
738                  NULL, hostTarget, ntohs(port), temp);
739  device->uri = strdup(uri);
740  device->rp  = strdup(temp);
741
742  if ((value = TXTRecordGetValuePtr(txtLen, txtRecord, "ty",
743                                    &valueLen)) != NULL)
744  {
745    memcpy(temp, value, valueLen);
746    temp[valueLen] = '\0';
747
748    device->ty = strdup(temp);
749  }
750
751  if ((value = TXTRecordGetValuePtr(txtLen, txtRecord, "pdl",
752                                    &valueLen)) != NULL)
753  {
754    memcpy(temp, value, valueLen);
755    temp[valueLen] = '\0';
756
757    device->pdl = strdup(temp);
758  }
759
760  if ((value = TXTRecordGetValuePtr(txtLen, txtRecord, "printer-type",
761                                    &valueLen)) != NULL)
762    device->cups_shared = 1;
763
764  if (device->cups_shared)
765    fprintf(stderr, "\rIgnoring CUPS printer %s\n", uri);
766  else
767    fprintf(stderr, "\rFound IPP printer %s\n", uri);
768
769  progress();
770}
771
772
773/*
774 * 'unquote()' - Unquote a name string.
775 */
776
777static void
778unquote(char       *dst,		/* I - Destination buffer */
779        const char *src,		/* I - Source string */
780	size_t     dstsize)		/* I - Size of destination buffer */
781{
782  char	*dstend = dst + dstsize - 1;	/* End of destination buffer */
783
784
785  while (*src && dst < dstend)
786  {
787    if (*src == '\\')
788    {
789      src ++;
790      if (isdigit(src[0] & 255) && isdigit(src[1] & 255) &&
791          isdigit(src[2] & 255))
792      {
793        *dst++ = ((((src[0] - '0') * 10) + src[1] - '0') * 10) + src[2] - '0';
794	src += 3;
795      }
796      else
797        *dst++ = *src++;
798    }
799    else
800      *dst++ = *src ++;
801  }
802
803  *dst = '\0';
804}
805
806
807/*
808 * 'usage()' - Show program usage and exit.
809 */
810
811static void
812usage(void)
813{
814  _cupsLangPuts(stdout, _("Usage: ippdiscover [options] -a\n"
815                          "       ippdiscover [options] \"service name\"\n"
816                          "\n"
817                          "Options:"));
818  _cupsLangPuts(stdout, _("  -a                      Browse for all services."));
819  _cupsLangPuts(stdout, _("  -d domain               Browse/resolve in specified domain."));
820  _cupsLangPuts(stdout, _("  -p program              Run specified program for each service."));
821  _cupsLangPuts(stdout, _("  -t type                 Browse/resolve with specified type."));
822
823  exit(0);
824}
825
826
827/*
828 * End of "$Id: ippdiscover.c 11093 2013-07-03 20:48:42Z msweet $".
829 */
830