Author: Kev <klmitch@mit.edu>
[ircu2.10.12-pk.git] / ircd / ircd_log.c
1 /************************************************************************
2  *   IRC - Internet Relay Chat, src/ircd_log.c
3  *   Copyright (C) 1999 Thomas Helvey (BleepSoft)
4  *   Copyright (C) 2000 Kevin L. Mitchell <klmitch@mit.edu>
5  *                     
6  *   See file AUTHORS in IRC package for additional names of
7  *   the programmers. 
8  *
9  *   This program is free software; you can redistribute it and/or modify
10  *   it under the terms of the GNU General Public License as published by
11  *   the Free Software Foundation; either version 1, or (at your option)
12  *   any later version.
13  *
14  *   This program is distributed in the hope that it will be useful,
15  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
16  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  *   GNU General Public License for more details.
18  *
19  *   You should have received a copy of the GNU General Public License
20  *   along with this program; if not, write to the Free Software
21  *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22  *
23  *   $Id$
24  */
25 #include "ircd_log.h"
26 #include "client.h"
27 #include "config.h"
28 #include "ircd_alloc.h"
29 #include "ircd_snprintf.h"
30 #include "ircd_string.h"
31 #include "ircd.h"
32 #include "s_debug.h"
33 #include "send.h"
34 #include "struct.h"
35
36 #include <assert.h>
37 #include <errno.h>
38 #include <fcntl.h>
39 #include <stdarg.h>
40 #include <stdio.h>
41 #include <sys/stat.h>
42 #include <sys/types.h>
43 #include <sys/uio.h>
44 #include <syslog.h>
45 #include <time.h>
46 #include <unistd.h>
47
48 #define LOG_BUFSIZE 2048 
49
50 /* select default log level cutoff */
51 #ifdef DEBUGMODE
52 # define L_DEFAULT      L_DEBUG
53 #else
54 # define L_DEFAULT      L_INFO
55 #endif
56
57 #define LOG_DOSYSLOG    0x10
58 #define LOG_DOFILELOG   0x20
59 #define LOG_DOSNOTICE   0x40
60
61 #define LOG_DOMASK      (LOG_DOSYSLOG | LOG_DOFILELOG | LOG_DOSNOTICE)
62
63 /* Map severity levels to strings and syslog levels */
64 static struct LevelData {
65   enum LogLevel level;
66   char         *string;
67   int           syslog;
68   unsigned int  snomask; /* 0 means use default in LogDesc */
69 } levelData[] = {
70 #define L(level, syslog, mask) { L_ ## level, #level, (syslog), (mask) }
71   L(CRIT, LOG_CRIT, SNO_OLDSNO),
72   L(ERROR, LOG_ERR, 0),
73   L(WARNING, LOG_WARNING, 0),
74   L(NOTICE, LOG_NOTICE, 0),
75   L(TRACE, LOG_INFO, 0),
76   L(INFO, LOG_INFO, 0),
77   L(DEBUG, LOG_INFO, SNO_DEBUG),
78 #undef L
79   { L_LAST_LEVEL, 0, 0, 0 }
80 };
81
82 /* Just in case some implementation of syslog has them... */
83 #undef LOG_NONE
84 #undef LOG_DEFAULT
85 #undef LOG_NOTFOUND
86
87 #define LOG_NONE     -1 /* don't syslog */
88 #define LOG_DEFAULT   0 /* syslog to logInfo.facility */
89 #define LOG_NOTFOUND -2 /* didn't find a facility corresponding to name */
90
91 /* Map names to syslog facilities--allows syslog configuration from .conf */
92 static struct {
93   char *name;
94   int facility;
95 } facilities[] = {
96 #define F(fac) { #fac, LOG_ ## fac }
97   F(NONE),    F(DEFAULT), F(AUTH),
98 #ifdef LOG_AUTHPRIV
99   F(AUTHPRIV),
100 #endif
101   F(CRON),    F(DAEMON),  F(LOCAL0),  F(LOCAL1),  F(LOCAL2),  F(LOCAL3),
102   F(LOCAL4),  F(LOCAL5),  F(LOCAL6),  F(LOCAL7),  F(LPR),     F(MAIL),
103   F(NEWS),    F(USER),    F(UUCP),
104 #undef F
105   { 0, 0 }
106 };
107
108 #define SNO_NONE     0x00000000 /* don't send server notices */
109 #define SNO_NOTFOUND 0xffffffff /* didn't find a SNO_MASK value for name */
110
111 /* Map names to snomask values--allows configuration from .conf */
112 static struct {
113   char *name;
114   unsigned int snomask;
115 } masks[] = {
116 #define M(mask) { #mask, SNO_ ## mask }
117   M(NONE),       M(OLDSNO),     M(SERVKILL),   M(OPERKILL),   M(HACK2),
118   M(HACK3),      M(UNAUTH),     M(TCPCOMMON),  M(TOOMANY),    M(HACK4),
119   M(GLINE),      M(NETWORK),    M(IPMISMATCH), M(THROTTLE),   M(OLDREALOP),
120   M(CONNEXIT),   M(DEBUG),
121 #undef M
122   { 0, 0 }
123 };
124
125 /* Descriptions of all logging subsystems */
126 static struct LogDesc {
127   enum LogSys     subsys;   /* number for subsystem */
128   char           *name;     /* subsystem name */
129   struct LogFile *file;     /* file descriptor for subsystem */
130   int             def_fac;  /* default facility */
131   unsigned int    def_sno;  /* default snomask */
132   int             facility; /* -1 means don't use syslog */
133   unsigned int    snomask;  /* 0 means no server message */
134   enum LogLevel   level;    /* logging level */
135 } logDesc[] = {
136 #define S(sys, p, sn) { LS_ ## sys, #sys, 0, (p), (sn), (p), (sn), L_DEFAULT }
137   S(SYSTEM, -1, 0),
138   S(CONFIG, 0, SNO_OLDSNO),
139   S(OPERMODE, -1, SNO_HACK4),
140   S(GLINE, -1, SNO_GLINE),
141   S(JUPE, -1, SNO_NETWORK),
142   S(WHO, -1, 0),
143   S(NETWORK, -1, SNO_NETWORK),
144   S(OPERKILL, -1, 0),
145   S(SERVKILL, -1, 0),
146   S(OPER, -1, SNO_OLDREALOP),
147   S(OPERLOG, -1, 0),
148   S(USERLOG, -1, 0),
149   S(RESOLVER, -1, 0),
150   S(SOCKET, -1, 0),
151   S(DEBUG, -1, SNO_DEBUG),
152   S(OLDLOG, -1, 0),
153 #undef S
154   { LS_LAST_SYSTEM, 0, 0, -1, 0, -1, 0 }
155 };
156
157 /* describes a log file */
158 struct LogFile {
159   struct LogFile  *next;   /* next log file descriptor */
160   struct LogFile **prev_p; /* what points to us */
161   int              fd;     /* file's descriptor-- -1 if not open */
162   int              ref;    /* how many things refer to us? */
163   char            *file;   /* file name */
164 };
165
166 /* modifiable static information */
167 static struct {
168   struct LogFile *filelist; /* list of log files */
169   struct LogFile *freelist; /* list of free'd log files */
170   int             facility; /* default facility */
171   const char     *procname; /* process's name */
172   struct LogFile *dbfile;   /* debug file */
173 } logInfo = { 0, 0, LOG_USER, "ircd", 0 };
174
175 void ircd_log(int priority, const char* fmt, ...)
176 {
177   va_list vl;
178
179   va_start(vl, fmt);
180   log_vwrite(LS_OLDLOG, priority, 0, fmt, vl);
181   va_end(vl);
182 }
183
184 /* helper routine to open a log file if needed */
185 static void
186 log_open(struct LogFile *lf)
187 {
188   /* only open the file if we haven't already */
189   if (lf && lf->fd < 0) {
190     alarm(3); /* if NFS hangs, we hang only for 3 seconds */
191     lf->fd = open(lf->file, O_WRONLY | O_CREAT | O_APPEND,
192                   S_IREAD | S_IWRITE);
193     alarm(0);
194   }
195 }
196
197 #ifdef DEBUGMODE
198
199 /* reopen debug log */
200 static void
201 log_debug_reopen(void)
202 {
203   if (!logInfo.dbfile) /* no open debugging file */
204     return;
205
206   if (!logInfo.dbfile->file) { /* using terminal output */
207     logInfo.dbfile->fd = 2;
208     return;
209   }
210
211   /* Ok, it's a real file; close it if necessary and use log_open to open it */
212   if (logInfo.dbfile->fd >= 0)
213     close(logInfo.dbfile->fd);
214
215   log_open(logInfo.dbfile);
216
217   if (logInfo.dbfile->fd < 0) { /* try again with /dev/null */
218     if ((logInfo.dbfile->fd = open("/dev/null", O_WRONLY)) < 0)
219       exit(-1);
220   }
221
222   /* massage the file descriptor to be stderr */
223   if (logInfo.dbfile->fd != 2) {
224     dup2(logInfo.dbfile->fd, 2);
225     close(logInfo.dbfile->fd);
226     logInfo.dbfile->fd = 2;
227   }
228 }
229
230 /* initialize debugging log */
231 void
232 log_debug_init(char *file)
233 {
234   logInfo.dbfile = MyMalloc(sizeof(struct LogFile));
235
236   logInfo.dbfile->next = 0; /* initialize debugging filename */
237   logInfo.dbfile->prev_p = 0;
238   logInfo.dbfile->fd = -1;
239   logInfo.dbfile->ref = 1;
240
241   if (file) /* store pathname to use */
242     DupString(logInfo.dbfile->file, file);
243   else
244     logInfo.dbfile->file = 0;
245
246   log_debug_reopen(); /* open the debug log */
247
248   logDesc[LS_DEBUG].file = logInfo.dbfile; /* remember where it went */
249 }
250
251 /* set the debug log file */
252 static void
253 log_debug_file(char *file)
254 {
255   assert(0 != file);
256
257   /* If we weren't started with debugging enabled, or if we're using
258    * the terminal, don't do anything at all.
259    */
260   if (!logInfo.dbfile || !logInfo.dbfile->file)
261     return;
262
263   MyFree(logInfo.dbfile->file); /* free old pathname */
264   DupString(logInfo.dbfile->file, file); /* store new pathname */
265
266   log_debug_reopen(); /* reopen the debug log */
267 }
268
269 #endif /* DEBUGMODE */
270
271 /* called in place of open_log(), this stores the process name and prepares
272  * for syslogging
273  */
274 void
275 log_init(const char *process_name)
276 {
277   /* store the process name; probably belongs in ircd.c, but oh well... */
278   if (!EmptyString(process_name))
279     logInfo.procname = process_name;
280
281   /* ok, open syslog; default facility: LOG_USER */
282   openlog(logInfo.procname, LOG_PID | LOG_NDELAY, logInfo.facility);
283 }
284
285 /* Files are persistently open; this closes and reopens them to allow
286  * log file rotation
287  */
288 void
289 log_reopen(void)
290 {
291   log_close(); /* close everything...we reopen on demand */
292
293 #ifdef DEBUGMODE
294   log_debug_reopen(); /* reopen debugging log if necessary */
295 #endif /* DEBUGMODE */
296
297   /* reopen syslog, if needed; default facility: LOG_USER */
298   openlog(logInfo.procname, LOG_PID | LOG_NDELAY, logInfo.facility);
299 }
300
301 /* close the log files */
302 void
303 log_close(void)
304 {
305   struct LogFile *ptr;
306
307   closelog(); /* close syslog */
308
309   for (ptr = logInfo.filelist; ptr; ptr = ptr->next) {
310     if (ptr->fd >= 0)
311       close(ptr->fd); /* close all the files... */
312
313     ptr->fd = -1;
314   }
315
316   if (logInfo.dbfile && logInfo.dbfile->file) {
317     if (logInfo.dbfile->fd >= 0)
318       close(logInfo.dbfile->fd); /* close the debug log file */
319
320     logInfo.dbfile->fd = -1;
321   }
322 }
323
324 /* These write entries to a log file */
325 void
326 log_write(enum LogSys subsys, enum LogLevel severity, unsigned int flags,
327           const char *fmt, ...)
328 {
329   va_list vl;
330
331   va_start(vl, fmt);
332   log_vwrite(subsys, severity, flags, fmt, vl);
333   va_end(vl);
334 }
335
336 void
337 log_vwrite(enum LogSys subsys, enum LogLevel severity, unsigned int flags,
338            const char *fmt, va_list vl)
339 {
340   struct VarData vd;
341   struct LogDesc *desc;
342   struct LevelData *ldata;
343   struct tm *tstamp;
344   struct iovec vector[3];
345   time_t curtime;
346   char buf[LOG_BUFSIZE];
347   /* 1234567890123456789012 3 */
348   /* [2000-11-28 16:11:20] \0 */
349   char timebuf[23];
350
351   /* check basic assumptions */
352   assert(-1 < (int)subsys);
353   assert((int)subsys < LS_LAST_SYSTEM);
354   assert(-1 < (int)severity);
355   assert((int)severity < L_LAST_LEVEL);
356   assert(0 == (flags & ~LOG_NOMASK));
357   assert(0 != fmt);
358
359   /* find the log data and the severity data */
360   desc = &logDesc[subsys];
361   ldata = &levelData[severity];
362
363   /* check the set of ordering assumptions */
364   assert(desc->subsys == subsys);
365   assert(ldata->level == severity);
366
367   /* check severity... */
368   if (severity > desc->level)
369     return;
370
371   /* figure out where all we need to log */
372   if (!(flags & LOG_NOFILELOG) && desc->file) {
373     log_open(desc->file);
374     if (desc->file->fd >= 0) /* don't log to file if we can't open the file */
375       flags |= LOG_DOFILELOG;
376   }
377
378   if (!(flags & LOG_NOSYSLOG) && desc->facility >= 0)
379     flags |= LOG_DOSYSLOG; /* will syslog */
380
381   if (!(flags & LOG_NOSNOTICE) && (desc->snomask != 0 || ldata->snomask != 0))
382     flags |= LOG_DOSNOTICE; /* will send a server notice */
383
384   /* short-circuit if there's nothing to do... */
385   if (!(flags & LOG_DOMASK))
386     return;
387
388   /* Build the basic log string */
389   vd.vd_format = fmt;
390   vd.vd_args = vl;
391
392   /* save the length for writev */
393   /* Log format: "SYSTEM [SEVERITY]: log message" */
394   vector[1].iov_len =
395     ircd_snprintf(0, buf, sizeof(buf), "%s [%s]: %v", desc->name,
396                   ldata->string, &vd);
397
398   /* if we have something to write to... */
399   if (flags & LOG_DOFILELOG) {
400     curtime = TStime();
401     tstamp = localtime(&curtime); /* build the timestamp */
402
403     vector[0].iov_len =
404       ircd_snprintf(0, timebuf, sizeof(timebuf), "[%d-%d-%d %d:%02d:%02d] ",
405                     tstamp->tm_year + 1900, tstamp->tm_mon + 1,
406                     tstamp->tm_mday, tstamp->tm_hour, tstamp->tm_min,
407                     tstamp->tm_sec);
408
409     /* set up the remaining parts of the writev vector... */
410     vector[0].iov_base = timebuf;
411     vector[1].iov_base = buf;
412
413     vector[2].iov_base = "\n"; /* terminate lines with a \n */
414     vector[2].iov_len = 1;
415
416     /* write it out to the log file */
417     writev(desc->file->fd, vector, 3);
418   }
419
420   /* oh yeah, syslog it too... */
421   if (flags & LOG_DOSYSLOG)
422     syslog(ldata->syslog | desc->facility, "%s", buf);
423
424   /* can't forget server notices... */
425   if (flags & LOG_DOSNOTICE)
426     sendto_opmask_butone(0, ldata->snomask ? ldata->snomask : desc->snomask,
427                          "%s", buf);
428 }
429
430 /* log kills for fun and profit */
431 void
432 log_write_kill(const struct Client *victim, const struct Client *killer,
433                const char *inpath, const char *path)
434 {
435   if (MyUser(victim))
436     log_write(IsServer(killer) ? LS_SERVKILL : LS_OPERKILL, L_TRACE, 0,
437               "A local client %#C KILLED by %#C Path: %s!%s",
438               victim, killer, inpath, path);
439   else
440     log_write(IsServer(killer) ? LS_SERVKILL : LS_OPERKILL, L_TRACE, 0,
441               "KILL from %C For %C Path: %s!%s", killer, victim, inpath, path);
442 }
443
444 /* return a struct LogFile for a specific filename--reference counted */
445 static struct LogFile *
446 log_file_create(const char *file)
447 {
448   struct LogFile *tmp;
449
450   assert(0 != file);
451
452   /* if one already exists for that file, return it */
453   for (tmp = logInfo.filelist; tmp; tmp = tmp->next)
454     if (!strcmp(tmp->file, file)) {
455       tmp->ref++;
456       return tmp;
457     }
458
459   if (logInfo.freelist) { /* pop one off the free list */
460     tmp = logInfo.freelist;
461     logInfo.freelist = tmp->next;
462   } else /* allocate a new one */
463     tmp = MyMalloc(sizeof(struct LogFile));
464
465   tmp->fd = -1; /* initialize the structure */
466   tmp->ref = 1;
467   DupString(tmp->file, file);
468
469   tmp->next = logInfo.filelist; /* link it into the list... */
470   tmp->prev_p = &logInfo.filelist;
471   logInfo.filelist->prev_p = &tmp->next;
472   logInfo.filelist = tmp;
473
474   return tmp;
475 }
476
477 /* destroy a log file descriptor, under the control of the reference count */
478 static void
479 log_file_destroy(struct LogFile *lf)
480 {
481   assert(0 != lf);
482
483   if (--lf->ref == 0) {
484     if (lf->next) /* clip it out of the list */
485       lf->next->prev_p = lf->prev_p;
486     *lf->prev_p = lf->next;
487
488     lf->prev_p = 0; /* we won't use it for the free list */
489     if (lf->fd >= 0)
490       close(lf->fd);
491     lf->fd = -1;
492     MyFree(lf->file); /* free the file name */
493
494     lf->next = logInfo.freelist; /* stack it onto the free list */
495     logInfo.freelist = lf;
496   }
497 }
498
499 /* finds a subsystem given its name */
500 static struct LogDesc *
501 log_find(char *subsys)
502 {
503   int i;
504
505   assert(0 != subsys);
506
507   /* find the named subsystem */
508   for (i = 0; i < LS_LAST_SYSTEM; i++)
509     if (!ircd_strcmp(subsys, logDesc[i].name))
510       return &logDesc[i];
511
512   return 0; /* not found */
513 }
514
515 /* find a level given its name */
516 static enum LogLevel
517 log_lev_find(char *level)
518 {
519   int i;
520
521   assert(0 != level);
522
523   /* find the named level */
524   for (i = 0; levelData[i].string; i++)
525     if (!ircd_strcmp(level, levelData[i].string))
526       return levelData[i].level;
527
528   return L_LAST_LEVEL; /* not found */
529 }
530
531 /* find a facility given its name */
532 static int
533 log_fac_find(char *facility)
534 {
535   int i;
536
537   assert(0 != facility);
538
539   /* find the named facility */
540   for (i = 0; facilities[i].name; i++)
541     if (!ircd_strcmp(facility, facilities[i].name))
542       return facilities[i].facility;
543
544   return LOG_NOTFOUND; /* not found */
545 }
546
547 /* find a snomask value given its name */
548 static unsigned int
549 log_sno_find(char *maskname)
550 {
551   int i;
552
553   assert(0 != maskname);
554
555   /* find the named snomask */
556   for (i = 0; masks[i].name; i++)
557     if (!ircd_strcmp(maskname, masks[i].name))
558       return masks[i].snomask;
559
560   return SNO_NOTFOUND; /* not found */
561 }
562
563 /* set the log file for a subsystem */
564 void
565 log_set_file(char *subsys, char *filename)
566 {
567   struct LogDesc *desc;
568
569   /* find subsystem */
570   if (!(desc = log_find(subsys)))
571     return;
572
573   /* no change, don't go to the trouble of destroying and recreating */
574   if (filename && !strcmp(desc->file->file, filename))
575     return;
576
577   /* debug log is special, since it has to be opened on fd 2 */
578   if (desc->subsys == LS_DEBUG) {
579     log_debug_file(filename);
580     return;
581   }
582
583   if (desc->file) /* destroy previous entry... */
584     log_file_destroy(desc->file);
585
586   /* set the file to use */
587   desc->file = filename ? log_file_create(filename) : 0;
588 }
589
590 /* get the log file for a subsystem */
591 char *
592 log_get_file(char *subsys)
593 {
594   struct LogDesc *desc;
595
596   /* find subsystem */
597   if (!(desc = log_find(subsys)))
598     return 0;
599
600   return desc->file ? desc->file->file : 0;
601 }
602
603 /* set the facility for a subsystem */
604 void
605 log_set_facility(char *subsys, char *facility)
606 {
607   struct LogDesc *desc;
608   int fac;
609
610   /* find subsystem */
611   if (!(desc = log_find(subsys)))
612     return;
613
614   /* set syslog facility */
615   if (EmptyString(facility))
616     desc->facility = desc->def_fac;
617   else if ((fac = log_fac_find(facility)) != LOG_NOTFOUND)
618     desc->facility = fac;
619 }
620
621 /* get the facility for a subsystem */
622 char *
623 log_get_facility(char *subsys)
624 {
625   struct LogDesc *desc;
626   int i;
627
628   /* find subsystem */
629   if (!(desc = log_find(subsys)))
630     return 0;
631
632   /* find the facility's name */
633   for (i = 0; facilities[i].name; i++)
634     if (desc->facility == facilities[i].facility)
635       return facilities[i].name; /* found it... */
636
637   return 0; /* shouldn't ever happen */
638 }
639
640 /* set the snomask value for a subsystem */
641 void
642 log_set_snomask(char *subsys, char *snomask)
643 {
644   struct LogDesc *desc;
645   unsigned int sno;
646
647   /* find subsystem */
648   if (!(desc = log_find(subsys)))
649     return;
650
651   /* set snomask value */
652   if (EmptyString(snomask))
653     desc->snomask = desc->def_sno;
654   else if ((sno = log_sno_find(snomask)) != SNO_NOTFOUND)
655     desc->snomask = sno;
656 }
657
658 /* get the snomask value for a subsystem */
659 char *
660 log_get_snomask(char *subsys)
661 {
662   struct LogDesc *desc;
663   int i;
664
665   /* find subsystem */
666   if (!(desc = log_find(subsys)))
667     return 0;
668
669   /* find the snomask value's name */
670   for (i = 0; masks[i].name; i++)
671     if (desc->snomask == masks[i].snomask)
672       return masks[i].name; /* found it... */
673
674   return 0; /* shouldn't ever happen */
675 }
676
677 /* set the level for a subsystem */
678 void
679 log_set_level(char *subsys, char *level)
680 {
681   struct LogDesc *desc;
682   enum LogLevel lev;
683
684   /* find subsystem */
685   if (!(desc = log_find(subsys)))
686     return;
687
688   /* set logging level */
689   if (EmptyString(level))
690     desc->level = L_DEFAULT;
691   else if ((lev = log_lev_find(level)) != L_LAST_LEVEL)
692     desc->level = lev;
693 }
694
695 /* get the level for a subsystem */
696 char *
697 log_get_level(char *subsys)
698 {
699   struct LogDesc *desc;
700   int i;
701
702   /* find subsystem */
703   if (!(desc = log_find(subsys)))
704     return 0;
705
706   /* find the level's name */
707   for (i = 0; levelData[i].string; i++)
708     if (desc->level == levelData[i].level)
709       return levelData[i].string; /* found it... */
710
711   return 0; /* shouldn't ever happen */
712 }
713
714 /* set the overall default syslog facility */
715 void
716 log_set_default(char *facility)
717 {
718   int fac, oldfac;
719
720   oldfac = logInfo.facility;
721
722   if (EmptyString(facility))
723     logInfo.facility = LOG_USER;
724   else if ((fac = log_fac_find(facility)) != LOG_NOTFOUND &&
725            fac != LOG_NONE && fac != LOG_DEFAULT)
726     logInfo.facility = fac;
727
728   if (logInfo.facility != oldfac) {
729     closelog(); /* reopen syslog with new facility setting */
730     openlog(logInfo.procname, LOG_PID | LOG_NDELAY, logInfo.facility);
731   }
732 }
733
734 /* get the overall default syslog facility */
735 char *
736 log_get_default(void)
737 {
738   int i;
739
740   /* find the facility's name */
741   for (i = 0; facilities[i].name; i++)
742     if (logInfo.facility == facilities[i].facility)
743       return facilities[i].name; /* found it... */
744
745   return 0; /* shouldn't ever happen */
746 }