xref: /openbsd-src/usr.sbin/tcpdump/smbutil.c (revision 898184e3e61f9129feb5978fad5a8c6865f00b92)
1 /*	$OpenBSD: smbutil.c,v 1.7 2009/10/27 23:59:57 deraadt Exp $	*/
2 
3 /*
4    Copyright (C) Andrew Tridgell 1995-1999
5 
6    This software may be distributed either under the terms of the
7    BSD-style license that accompanies tcpdump or the GNU GPL version 2
8    or later */
9 
10 #ifdef HAVE_CONFIG_H
11 #include "config.h"
12 #endif
13 
14 #include <sys/param.h>
15 #include <sys/time.h>
16 #include <sys/types.h>
17 #include <sys/socket.h>
18 
19 
20 #include <netinet/in.h>
21 
22 #include <ctype.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <time.h>
27 
28 #include "interface.h"
29 #include "smb.h"
30 
31 extern const uchar *startbuf;
32 
33 /*******************************************************************
34   interpret a 32 bit dos packed date/time to some parameters
35 ********************************************************************/
36 static void interpret_dos_date(uint32 date,int *year,int *month,int *day,int *hour,int *minute,int *second)
37 {
38   uint32 p0,p1,p2,p3;
39 
40   p0=date&0xFF; p1=((date&0xFF00)>>8)&0xFF;
41   p2=((date&0xFF0000)>>16)&0xFF; p3=((date&0xFF000000)>>24)&0xFF;
42 
43   *second = 2*(p0 & 0x1F);
44   *minute = ((p0>>5)&0xFF) + ((p1&0x7)<<3);
45   *hour = (p1>>3)&0xFF;
46   *day = (p2&0x1F);
47   *month = ((p2>>5)&0xFF) + ((p3&0x1)<<3) - 1;
48   *year = ((p3>>1)&0xFF) + 80;
49 }
50 
51 /*******************************************************************
52   create a unix date from a dos date
53 ********************************************************************/
54 static time_t make_unix_date(const void *date_ptr)
55 {
56   uint32 dos_date=0;
57   struct tm t;
58 
59   dos_date = IVAL(date_ptr,0);
60 
61   if (dos_date == 0) return(0);
62 
63   interpret_dos_date(dos_date,&t.tm_year,&t.tm_mon,
64 		     &t.tm_mday,&t.tm_hour,&t.tm_min,&t.tm_sec);
65   t.tm_wday = 1;
66   t.tm_yday = 1;
67   t.tm_isdst = 0;
68 
69   return (mktime(&t));
70 }
71 
72 /*******************************************************************
73   create a unix date from a dos date
74 ********************************************************************/
75 static time_t make_unix_date2(const void *date_ptr)
76 {
77   uint32 x,x2;
78 
79   x = IVAL(date_ptr,0);
80   x2 = ((x&0xFFFF)<<16) | ((x&0xFFFF0000)>>16);
81   SIVAL(&x,0,x2);
82 
83   return(make_unix_date((void *)&x));
84 }
85 
86 /****************************************************************************
87 interpret an 8 byte "filetime" structure to a time_t
88 It's originally in "100ns units since jan 1st 1601"
89 ****************************************************************************/
90 static time_t interpret_long_date(const char *p)
91 {
92   double d;
93   time_t ret;
94 
95   /* this gives us seconds since jan 1st 1601 (approx) */
96   d = (IVAL(p,4)*256.0 + CVAL(p,3)) * (1.0e-7 * (1<<24));
97 
98   /* now adjust by 369 years to make the secs since 1970 */
99   d -= 369.0*365.25*24*60*60;
100 
101   /* and a fudge factor as we got it wrong by a few days */
102   d += (3*24*60*60 + 6*60*60 + 2);
103 
104   if (d<0)
105     return(0);
106 
107   ret = (time_t)d;
108 
109   return(ret);
110 }
111 
112 
113 /****************************************************************************
114 interpret the weird netbios "name". Return the name type, or -1 if
115 we run past the end of the buffer
116 ****************************************************************************/
117 static int name_interpret(const uchar *in,const uchar *maxbuf,char *out)
118 {
119   char *ob = out;
120   int ret;
121   int len;
122 
123   if (in >= maxbuf)
124     return(-1);	/* name goes past the end of the buffer */
125   TCHECK2(*in, 1);
126   len = (*in++) / 2;
127 
128   *out=0;
129 
130   if (len > 30 || len<1) return(0);
131 
132   while (len--)
133     {
134       if (in + 1 >= maxbuf)
135 	return(-1);	/* name goes past the end of the buffer */
136       TCHECK2(*in, 2);
137       if (in[0] < 'A' || in[0] > 'P' || in[1] < 'A' || in[1] > 'P') {
138 	*out++ = 0;
139 	break;
140       }
141       *out = ((in[0]-'A')<<4) + (in[1]-'A');
142       in += 2;
143       out++;
144     }
145   ret = out[-1];
146   out--;
147   while (out[-1] == ' ')
148     out--;
149   *out = '\0';
150   for (; *ob; ob++)
151     if (!isprint(*ob))
152       *ob = 'X';
153 
154   return(ret);
155 
156 trunc:
157   return(-1);
158 }
159 
160 /****************************************************************************
161 find a pointer to a netbios name
162 ****************************************************************************/
163 static const uchar *name_ptr(const uchar *buf,int ofs,const uchar *maxbuf)
164 {
165   const uchar *p;
166   uchar c;
167 
168   p = buf+ofs;
169   if (p >= maxbuf)
170     return(NULL);	/* name goes past the end of the buffer */
171   TCHECK2(*p, 1);
172 
173   c = *p;
174 
175   /* XXX - this should use the same code that the DNS dissector does */
176   if ((c & 0xC0) == 0xC0)
177     {
178       uint16 l = RSVAL(buf, ofs) & 0x3FFF;
179       if (l == 0)
180 	{
181 	  /* We have a pointer that points to itself. */
182 	  return(NULL);
183 	}
184       p = buf + l;
185       if (p >= maxbuf)
186 	return(NULL);	/* name goes past the end of the buffer */
187       TCHECK2(*p, 1);
188       return(buf + l);
189     }
190   else
191     return(buf+ofs);
192 
193 trunc:
194   return(NULL);	/* name goes past the end of the buffer */
195 }
196 
197 /****************************************************************************
198 extract a netbios name from a buf
199 ****************************************************************************/
200 static int name_extract(const uchar *buf,int ofs,const uchar *maxbuf,char *name)
201 {
202   const uchar *p = name_ptr(buf,ofs,maxbuf);
203   if (p == NULL)
204     return(-1);	/* error (probably name going past end of buffer) */
205   *name = '\0';
206   return(name_interpret(p,maxbuf,name));
207 }
208 
209 
210 /****************************************************************************
211 return the total storage length of a mangled name
212 ****************************************************************************/
213 static int name_len(const unsigned char *s, const unsigned char *maxbuf)
214 {
215   const unsigned char *s0 = s;
216   unsigned char c;
217 
218   if (s >= maxbuf)
219     return(-1);	/* name goes past the end of the buffer */
220   TCHECK2(*s, 1);
221   c = *s;
222   if ((c & 0xC0) == 0xC0)
223     return(2);
224   while (*s)
225     {
226       if (s >= maxbuf)
227 	return(-1);	/* name goes past the end of the buffer */
228       TCHECK2(*s, 1);
229       s += (*s)+1;
230     }
231   return(PTR_DIFF(s,s0)+1);
232 
233 trunc:
234   return(-1);	/* name goes past the end of the buffer */
235 }
236 
237 static char *name_type_str(int name_type)
238 {
239   static char *f = NULL;
240   switch (name_type) {
241   case 0:    f = "Workstation"; break;
242   case 0x03: f = "Client?"; break;
243   case 0x20: f = "Server"; break;
244   case 0x1d: f = "Master Browser"; break;
245   case 0x1b: f = "Domain Controller"; break;
246   case 0x1e: f = "Browser Server"; break;
247   default:   f = "Unknown"; break;
248   }
249   return(f);
250 }
251 
252 static void write_bits(unsigned int val,char *fmt)
253 {
254   char *p = fmt;
255   int i=0;
256 
257   while ((p=strchr(fmt,'|'))) {
258     int l = PTR_DIFF(p,fmt);
259     if (l && (val & (1<<i)))
260       printf("%.*s ",l,fmt);
261     fmt = p+1;
262     i++;
263   }
264 }
265 
266 /* convert a unicode string */
267 static const char *unistr(const char *s, int *len)
268 {
269 	static char buf[1000];
270 	int l=0;
271 	static int use_unicode = -1;
272 
273 	if (use_unicode == -1) {
274 		char *p = getenv("USE_UNICODE");
275 		if (p && (atoi(p) == 1))
276 			use_unicode = 1;
277 		else
278 			use_unicode = 0;
279 	}
280 
281 	/* maybe it isn't unicode - a cheap trick */
282 	if (!use_unicode || (s[0] && s[1])) {
283 		*len = strlen(s)+1;
284 		return s;
285 	}
286 
287 	*len = 0;
288 
289 	if (s[0] == 0 && s[1] != 0) {
290 		s++;
291 		*len = 1;
292 	}
293 
294 	while (l < (sizeof(buf)-1) && s[0] && s[1] == 0) {
295 		buf[l] = s[0];
296 		s += 2; l++;
297 		*len += 2;
298 	}
299 	buf[l] = 0;
300 	*len += 2;
301 	return buf;
302 }
303 
304 static const uchar *fdata1(const uchar *buf, const char *fmt, const uchar *maxbuf)
305 {
306   int reverse=0;
307   char *attrib_fmt = "READONLY|HIDDEN|SYSTEM|VOLUME|DIR|ARCHIVE|";
308   int len;
309 
310   while (*fmt && buf<maxbuf) {
311     switch (*fmt) {
312     case 'a':
313       write_bits(CVAL(buf,0),attrib_fmt);
314       buf++; fmt++;
315       break;
316 
317     case 'A':
318       write_bits(SVAL(buf,0),attrib_fmt);
319       buf+=2; fmt++;
320       break;
321 
322     case '{':
323       {
324 	char bitfmt[128];
325 	char *p = strchr(++fmt,'}');
326 	int l = PTR_DIFF(p,fmt);
327 	strlcpy(bitfmt,fmt,sizeof(bitfmt));
328 	fmt = p+1;
329 	write_bits(CVAL(buf,0),bitfmt);
330 	buf++;
331 	break;
332       }
333 
334     case 'P':
335       {
336 	int l = atoi(fmt+1);
337 	buf += l;
338 	fmt++;
339 	while (isdigit(*fmt)) fmt++;
340 	break;
341       }
342     case 'r':
343       reverse = !reverse;
344       fmt++;
345       break;
346     case 'D':
347       {
348 	unsigned int x = reverse?RIVAL(buf,0):IVAL(buf,0);
349 	printf("%d (0x%x)",x, x);
350 	buf += 4;
351 	fmt++;
352 	break;
353       }
354     case 'L':
355       {
356 	unsigned int x1 = reverse?RIVAL(buf,0):IVAL(buf,0);
357 	unsigned int x2 = reverse?RIVAL(buf,4):IVAL(buf,4);
358 	if (x2) {
359 		printf("0x%08x:%08x",x2, x1);
360 	} else {
361 		printf("%d (0x%08x%08x)",x1, x2, x1);
362 	}
363 	buf += 8;
364 	fmt++;
365 	break;
366       }
367     case 'd':
368       {
369 	unsigned int x = reverse?RSVAL(buf,0):SVAL(buf,0);
370 	printf("%d",x);
371 	buf += 2;
372 	fmt++;
373 	break;
374       }
375     case 'W':
376       {
377 	unsigned int x = reverse?RIVAL(buf,0):IVAL(buf,0);
378 	printf("0x%X",x);
379 	buf += 4;
380 	fmt++;
381 	break;
382       }
383     case 'w':
384       {
385 	unsigned int x = reverse?RSVAL(buf,0):SVAL(buf,0);
386 	printf("0x%X",x);
387 	buf += 2;
388 	fmt++;
389 	break;
390       }
391     case 'B':
392       {
393 	unsigned int x = CVAL(buf,0);
394 	printf("0x%X",x);
395 	buf += 1;
396 	fmt++;
397 	break;
398       }
399     case 'b':
400       {
401 	unsigned int x = CVAL(buf,0);
402 	printf("%d",x); 			/* EMF - jesus, use B if you want hex */
403 	buf += 1;
404 	fmt++;
405 	break;
406       }
407     case 'S':
408       {
409 	      printf("%.*s",(int)PTR_DIFF(maxbuf,buf),unistr(buf, &len));
410 	      buf += len;
411 	      fmt++;
412 	      break;
413       }
414     case 'Z':
415       {
416 	if (*buf != 4 && *buf != 2)
417 	  printf("Error! ASCIIZ buffer of type %d (safety=%d) ",
418 		 *buf,(int)PTR_DIFF(maxbuf,buf));
419 	printf("%.*s",(int)PTR_DIFF(maxbuf,buf+1),unistr(buf+1, &len));
420 	buf += len+1;
421 	fmt++;
422 	break;
423       }
424     case 's':
425       {
426 	int l = atoi(fmt+1);
427 	printf("%-*.*s",l,l,buf);
428 	buf += l;
429 	fmt++; while (isdigit(*fmt)) fmt++;
430 	break;
431       }
432     case 'h':
433       {
434 	int l = atoi(fmt+1);
435 	while (l--) printf("%02x",*buf++);
436 	fmt++; while (isdigit(*fmt)) fmt++;
437 	break;
438       }
439     case 'n':
440       {
441 	int t = atoi(fmt+1);
442 	char nbuf[255];
443 	int name_type;
444 	int len;
445 	switch (t) {
446 	case 1:
447 	  name_type = name_extract(startbuf,PTR_DIFF(buf,startbuf),maxbuf,
448 		nbuf);
449 	  if (name_type < 0)
450 	    goto trunc;
451 	  len = name_len(buf,maxbuf);
452 	  if (len < 0)
453 	    goto trunc;
454 	  buf += len;
455 	  printf("%.15s type 0x%02X (%s)",
456 		 nbuf,name_type,name_type_str(name_type));
457 	  break;
458 	case 2:
459 	  name_type = buf[15];
460 	  printf("%.15s type 0x%02X (%s)",
461 		 buf,name_type,name_type_str(name_type));
462 	  buf += 16;
463 	  break;
464 	}
465 	fmt++; while (isdigit(*fmt)) fmt++;
466 	break;
467       }
468     case 'T':
469       {
470 	time_t t;
471 	int x = IVAL(buf,0);
472 	switch (atoi(fmt+1)) {
473 	case 1:
474 	  if (x==0 || x==-1 || x==0xFFFFFFFF)
475 	    t = 0;
476 	  else
477 	    t = make_unix_date(buf);
478 	  buf+=4;
479 	  break;
480 	case 2:
481 	  if (x==0 || x==-1 || x==0xFFFFFFFF)
482 	    t = 0;
483 	  else
484 	    t = make_unix_date2(buf);
485 	  buf+=4;
486 	  break;
487 	case 3:
488 	  t = interpret_long_date(buf);
489 	  buf+=8;
490 	  break;
491 	default:
492 	  error("fdata1: invalid fmt: %s", fmt);
493 	}
494 	printf("%s",t?asctime(localtime(&t)):"NULL ");
495 	fmt++; while (isdigit(*fmt)) fmt++;
496 	break;
497       }
498     default:
499       putchar(*fmt);
500       fmt++;
501       break;
502     }
503   }
504 
505   if (buf>=maxbuf && *fmt)
506     printf("END OF BUFFER ");
507 
508   return(buf);
509 
510 trunc:
511   printf("WARNING: Short packet. Try increasing the snap length ");
512   return(NULL);
513 }
514 
515 const uchar *fdata(const uchar *buf, const char *fmt, const uchar *maxbuf)
516 {
517   static int depth=0;
518   char s[128];
519   char *p;
520 
521   while (*fmt) {
522     switch (*fmt) {
523     case '*':
524       fmt++;
525       while (buf < maxbuf) {
526 	const uchar *buf2;
527 	depth++;
528 	buf2 = fdata(buf,fmt,maxbuf);
529 	depth--;
530 	if (buf2 == buf) return(buf);
531 	buf = buf2;
532       }
533       break;
534 
535     case '|':
536       fmt++;
537       if (buf>=maxbuf) return(buf);
538       break;
539 
540     case '%':
541       fmt++;
542       buf=maxbuf;
543       break;
544 
545     case '#':
546       fmt++;
547       return(buf);
548       break;
549 
550     case '[':
551       fmt++;
552       if (buf>=maxbuf) return(buf);
553       memset(s, 0, sizeof(s));
554       p = strchr(fmt,']');
555       strncpy(s,fmt,p-fmt);	/* XXX? */
556       fmt = p+1;
557       buf = fdata1(buf,s,maxbuf);
558       if (buf == NULL)
559 	return(NULL);
560       break;
561 
562     default:
563       putchar(*fmt); fmt++;
564       fflush(stdout);
565       break;
566     }
567   }
568   if (!depth && buf<maxbuf) {
569     int len = PTR_DIFF(maxbuf,buf);
570     printf("(%d data bytes)",len);
571     /* EMF -  use -X flag if you want this verbosity
572      * print_data(buf,len);
573      */
574     return(buf+len);
575   }
576   return(buf);
577 }
578 
579 typedef struct
580 {
581   char *name;
582   int code;
583   char *message;
584 } err_code_struct;
585 
586 /* Dos Error Messages */
587 static err_code_struct dos_msgs[] = {
588   {"ERRbadfunc",1,"Invalid function."},
589   {"ERRbadfile",2,"File not found."},
590   {"ERRbadpath",3,"Directory invalid."},
591   {"ERRnofids",4,"No file descriptors available"},
592   {"ERRnoaccess",5,"Access denied."},
593   {"ERRbadfid",6,"Invalid file handle."},
594   {"ERRbadmcb",7,"Memory control blocks destroyed."},
595   {"ERRnomem",8,"Insufficient server memory to perform the requested function."},
596   {"ERRbadmem",9,"Invalid memory block address."},
597   {"ERRbadenv",10,"Invalid environment."},
598   {"ERRbadformat",11,"Invalid format."},
599   {"ERRbadaccess",12,"Invalid open mode."},
600   {"ERRbaddata",13,"Invalid data."},
601   {"ERR",14,"reserved."},
602   {"ERRbaddrive",15,"Invalid drive specified."},
603   {"ERRremcd",16,"A Delete Directory request attempted  to  remove  the  server's  current directory."},
604   {"ERRdiffdevice",17,"Not same device."},
605   {"ERRnofiles",18,"A File Search command can find no more files matching the specified criteria."},
606   {"ERRbadshare",32,"The sharing mode specified for an Open conflicts with existing  FIDs  on the file."},
607   {"ERRlock",33,"A Lock request conflicted with an existing lock or specified an  invalid mode,  or an Unlock requested attempted to remove a lock held by another process."},
608   {"ERRfilexists",80,"The file named in a Create Directory, Make  New  File  or  Link  request already exists."},
609   {"ERRbadpipe",230,"Pipe invalid."},
610   {"ERRpipebusy",231,"All instances of the requested pipe are busy."},
611   {"ERRpipeclosing",232,"Pipe close in progress."},
612   {"ERRnotconnected",233,"No process on other end of pipe."},
613   {"ERRmoredata",234,"There is more data to be returned."},
614   {NULL,-1,NULL}};
615 
616 /* Server Error Messages */
617 err_code_struct server_msgs[] = {
618   {"ERRerror",1,"Non-specific error code."},
619   {"ERRbadpw",2,"Bad password - name/password pair in a Tree Connect or Session Setup are invalid."},
620   {"ERRbadtype",3,"reserved."},
621   {"ERRaccess",4,"The requester does not have  the  necessary  access  rights  within  the specified  context for the requested function. The context is defined by the TID or the UID."},
622   {"ERRinvnid",5,"The tree ID (TID) specified in a command was invalid."},
623   {"ERRinvnetname",6,"Invalid network name in tree connect."},
624   {"ERRinvdevice",7,"Invalid device - printer request made to non-printer connection or  non-printer request made to printer connection."},
625   {"ERRqfull",49,"Print queue full (files) -- returned by open print file."},
626   {"ERRqtoobig",50,"Print queue full -- no space."},
627   {"ERRqeof",51,"EOF on print queue dump."},
628   {"ERRinvpfid",52,"Invalid print file FID."},
629   {"ERRsmbcmd",64,"The server did not recognize the command received."},
630   {"ERRsrverror",65,"The server encountered an internal error, e.g., system file unavailable."},
631   {"ERRfilespecs",67,"The file handle (FID) and pathname parameters contained an invalid  combination of values."},
632   {"ERRreserved",68,"reserved."},
633   {"ERRbadpermits",69,"The access permissions specified for a file or directory are not a valid combination.  The server cannot set the requested attribute."},
634   {"ERRreserved",70,"reserved."},
635   {"ERRsetattrmode",71,"The attribute mode in the Set File Attribute request is invalid."},
636   {"ERRpaused",81,"Server is paused."},
637   {"ERRmsgoff",82,"Not receiving messages."},
638   {"ERRnoroom",83,"No room to buffer message."},
639   {"ERRrmuns",87,"Too many remote user names."},
640   {"ERRtimeout",88,"Operation timed out."},
641   {"ERRnoresource",89,"No resources currently available for request."},
642   {"ERRtoomanyuids",90,"Too many UIDs active on this session."},
643   {"ERRbaduid",91,"The UID is not known as a valid ID on this session."},
644   {"ERRusempx",250,"Temp unable to support Raw, use MPX mode."},
645   {"ERRusestd",251,"Temp unable to support Raw, use standard read/write."},
646   {"ERRcontmpx",252,"Continue in MPX mode."},
647   {"ERRreserved",253,"reserved."},
648   {"ERRreserved",254,"reserved."},
649   {"ERRnosupport",0xFFFF,"Function not supported."},
650   {NULL,-1,NULL}};
651 
652 /* Hard Error Messages */
653 err_code_struct hard_msgs[] = {
654   {"ERRnowrite",19,"Attempt to write on write-protected diskette."},
655   {"ERRbadunit",20,"Unknown unit."},
656   {"ERRnotready",21,"Drive not ready."},
657   {"ERRbadcmd",22,"Unknown command."},
658   {"ERRdata",23,"Data error (CRC)."},
659   {"ERRbadreq",24,"Bad request structure length."},
660   {"ERRseek",25 ,"Seek error."},
661   {"ERRbadmedia",26,"Unknown media type."},
662   {"ERRbadsector",27,"Sector not found."},
663   {"ERRnopaper",28,"Printer out of paper."},
664   {"ERRwrite",29,"Write fault."},
665   {"ERRread",30,"Read fault."},
666   {"ERRgeneral",31,"General failure."},
667   {"ERRbadshare",32,"A open conflicts with an existing open."},
668   {"ERRlock",33,"A Lock request conflicted with an existing lock or specified an invalid mode, or an Unlock requested attempted to remove a lock held by another process."},
669   {"ERRwrongdisk",34,"The wrong disk was found in a drive."},
670   {"ERRFCBUnavail",35,"No FCBs are available to process request."},
671   {"ERRsharebufexc",36,"A sharing buffer has been exceeded."},
672   {NULL,-1,NULL}};
673 
674 
675 static struct
676 {
677   int code;
678   char *class;
679   err_code_struct *err_msgs;
680 } err_classes[] = {
681   {0,"SUCCESS",NULL},
682   {0x01,"ERRDOS",dos_msgs},
683   {0x02,"ERRSRV",server_msgs},
684   {0x03,"ERRHRD",hard_msgs},
685   {0x04,"ERRXOS",NULL},
686   {0xE1,"ERRRMX1",NULL},
687   {0xE2,"ERRRMX2",NULL},
688   {0xE3,"ERRRMX3",NULL},
689   {0xFF,"ERRCMD",NULL},
690   {-1,NULL,NULL}};
691 
692 
693 /****************************************************************************
694 return a SMB error string from a SMB buffer
695 ****************************************************************************/
696 char *smb_errstr(int class,int num)
697 {
698   static char ret[128];
699   int i,j;
700 
701   ret[0]=0;
702 
703   for (i=0;err_classes[i].class;i++)
704     if (err_classes[i].code == class)
705       {
706 	if (err_classes[i].err_msgs)
707 	  {
708 	    err_code_struct *err = err_classes[i].err_msgs;
709 	    for (j=0;err[j].name;j++)
710 	      if (num == err[j].code)
711 		{
712 		  snprintf(ret, sizeof(ret), "%s - %s (%s)",
713 		    err_classes[i].class,
714 		    err[j].name,err[j].message);
715 		  return ret;
716 		}
717 	  }
718 
719 	snprintf(ret,sizeof(ret),"%s - %d",err_classes[i].class,num);
720 	return ret;
721       }
722 
723   snprintf(ret,sizeof(ret),"ERROR: Unknown error (%d,%d)",class,num);
724   return(ret);
725 }
726