xref: /openbsd-src/usr.sbin/tcpdump/smbutil.c (revision 6fb12b7054efc6b436584db6cef9c2f85c0d7e27)
1 /*	$OpenBSD: smbutil.c,v 1.8 2013/04/16 18:10:24 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   memset(&t, 0, sizeof t);
64   interpret_dos_date(dos_date,&t.tm_year,&t.tm_mon,
65 		     &t.tm_mday,&t.tm_hour,&t.tm_min,&t.tm_sec);
66   t.tm_wday = 1;
67   t.tm_yday = 1;
68   t.tm_isdst = 0;
69 
70   return (mktime(&t));
71 }
72 
73 /*******************************************************************
74   create a unix date from a dos date
75 ********************************************************************/
76 static time_t make_unix_date2(const void *date_ptr)
77 {
78   uint32 x,x2;
79 
80   x = IVAL(date_ptr,0);
81   x2 = ((x&0xFFFF)<<16) | ((x&0xFFFF0000)>>16);
82   SIVAL(&x,0,x2);
83 
84   return(make_unix_date((void *)&x));
85 }
86 
87 /****************************************************************************
88 interpret an 8 byte "filetime" structure to a time_t
89 It's originally in "100ns units since jan 1st 1601"
90 ****************************************************************************/
91 static time_t interpret_long_date(const char *p)
92 {
93   double d;
94   time_t ret;
95 
96   /* this gives us seconds since jan 1st 1601 (approx) */
97   d = (IVAL(p,4)*256.0 + CVAL(p,3)) * (1.0e-7 * (1<<24));
98 
99   /* now adjust by 369 years to make the secs since 1970 */
100   d -= 369.0*365.25*24*60*60;
101 
102   /* and a fudge factor as we got it wrong by a few days */
103   d += (3*24*60*60 + 6*60*60 + 2);
104 
105   if (d<0)
106     return(0);
107 
108   ret = (time_t)d;
109 
110   return(ret);
111 }
112 
113 
114 /****************************************************************************
115 interpret the weird netbios "name". Return the name type, or -1 if
116 we run past the end of the buffer
117 ****************************************************************************/
118 static int name_interpret(const uchar *in,const uchar *maxbuf,char *out)
119 {
120   char *ob = out;
121   int ret;
122   int len;
123 
124   if (in >= maxbuf)
125     return(-1);	/* name goes past the end of the buffer */
126   TCHECK2(*in, 1);
127   len = (*in++) / 2;
128 
129   *out=0;
130 
131   if (len > 30 || len<1) return(0);
132 
133   while (len--)
134     {
135       if (in + 1 >= maxbuf)
136 	return(-1);	/* name goes past the end of the buffer */
137       TCHECK2(*in, 2);
138       if (in[0] < 'A' || in[0] > 'P' || in[1] < 'A' || in[1] > 'P') {
139 	*out++ = 0;
140 	break;
141       }
142       *out = ((in[0]-'A')<<4) + (in[1]-'A');
143       in += 2;
144       out++;
145     }
146   ret = out[-1];
147   out--;
148   while (out[-1] == ' ')
149     out--;
150   *out = '\0';
151   for (; *ob; ob++)
152     if (!isprint(*ob))
153       *ob = 'X';
154 
155   return(ret);
156 
157 trunc:
158   return(-1);
159 }
160 
161 /****************************************************************************
162 find a pointer to a netbios name
163 ****************************************************************************/
164 static const uchar *name_ptr(const uchar *buf,int ofs,const uchar *maxbuf)
165 {
166   const uchar *p;
167   uchar c;
168 
169   p = buf+ofs;
170   if (p >= maxbuf)
171     return(NULL);	/* name goes past the end of the buffer */
172   TCHECK2(*p, 1);
173 
174   c = *p;
175 
176   /* XXX - this should use the same code that the DNS dissector does */
177   if ((c & 0xC0) == 0xC0)
178     {
179       uint16 l = RSVAL(buf, ofs) & 0x3FFF;
180       if (l == 0)
181 	{
182 	  /* We have a pointer that points to itself. */
183 	  return(NULL);
184 	}
185       p = buf + l;
186       if (p >= maxbuf)
187 	return(NULL);	/* name goes past the end of the buffer */
188       TCHECK2(*p, 1);
189       return(buf + l);
190     }
191   else
192     return(buf+ofs);
193 
194 trunc:
195   return(NULL);	/* name goes past the end of the buffer */
196 }
197 
198 /****************************************************************************
199 extract a netbios name from a buf
200 ****************************************************************************/
201 static int name_extract(const uchar *buf,int ofs,const uchar *maxbuf,char *name)
202 {
203   const uchar *p = name_ptr(buf,ofs,maxbuf);
204   if (p == NULL)
205     return(-1);	/* error (probably name going past end of buffer) */
206   *name = '\0';
207   return(name_interpret(p,maxbuf,name));
208 }
209 
210 
211 /****************************************************************************
212 return the total storage length of a mangled name
213 ****************************************************************************/
214 static int name_len(const unsigned char *s, const unsigned char *maxbuf)
215 {
216   const unsigned char *s0 = s;
217   unsigned char c;
218 
219   if (s >= maxbuf)
220     return(-1);	/* name goes past the end of the buffer */
221   TCHECK2(*s, 1);
222   c = *s;
223   if ((c & 0xC0) == 0xC0)
224     return(2);
225   while (*s)
226     {
227       if (s >= maxbuf)
228 	return(-1);	/* name goes past the end of the buffer */
229       TCHECK2(*s, 1);
230       s += (*s)+1;
231     }
232   return(PTR_DIFF(s,s0)+1);
233 
234 trunc:
235   return(-1);	/* name goes past the end of the buffer */
236 }
237 
238 static char *name_type_str(int name_type)
239 {
240   static char *f = NULL;
241   switch (name_type) {
242   case 0:    f = "Workstation"; break;
243   case 0x03: f = "Client?"; break;
244   case 0x20: f = "Server"; break;
245   case 0x1d: f = "Master Browser"; break;
246   case 0x1b: f = "Domain Controller"; break;
247   case 0x1e: f = "Browser Server"; break;
248   default:   f = "Unknown"; break;
249   }
250   return(f);
251 }
252 
253 static void write_bits(unsigned int val,char *fmt)
254 {
255   char *p = fmt;
256   int i=0;
257 
258   while ((p=strchr(fmt,'|'))) {
259     int l = PTR_DIFF(p,fmt);
260     if (l && (val & (1<<i)))
261       printf("%.*s ",l,fmt);
262     fmt = p+1;
263     i++;
264   }
265 }
266 
267 /* convert a unicode string */
268 static const char *unistr(const char *s, int *len)
269 {
270 	static char buf[1000];
271 	int l=0;
272 	static int use_unicode = -1;
273 
274 	if (use_unicode == -1) {
275 		char *p = getenv("USE_UNICODE");
276 		if (p && (atoi(p) == 1))
277 			use_unicode = 1;
278 		else
279 			use_unicode = 0;
280 	}
281 
282 	/* maybe it isn't unicode - a cheap trick */
283 	if (!use_unicode || (s[0] && s[1])) {
284 		*len = strlen(s)+1;
285 		return s;
286 	}
287 
288 	*len = 0;
289 
290 	if (s[0] == 0 && s[1] != 0) {
291 		s++;
292 		*len = 1;
293 	}
294 
295 	while (l < (sizeof(buf)-1) && s[0] && s[1] == 0) {
296 		buf[l] = s[0];
297 		s += 2; l++;
298 		*len += 2;
299 	}
300 	buf[l] = 0;
301 	*len += 2;
302 	return buf;
303 }
304 
305 static const uchar *fdata1(const uchar *buf, const char *fmt, const uchar *maxbuf)
306 {
307   int reverse=0;
308   char *attrib_fmt = "READONLY|HIDDEN|SYSTEM|VOLUME|DIR|ARCHIVE|";
309   int len;
310 
311   while (*fmt && buf<maxbuf) {
312     switch (*fmt) {
313     case 'a':
314       write_bits(CVAL(buf,0),attrib_fmt);
315       buf++; fmt++;
316       break;
317 
318     case 'A':
319       write_bits(SVAL(buf,0),attrib_fmt);
320       buf+=2; fmt++;
321       break;
322 
323     case '{':
324       {
325 	char bitfmt[128];
326 	char *p = strchr(++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