chiark / gitweb /
manual/Makefile.am: Install the HTML version of the manual correctly.
[become] / src / userdb.c
1 /* -*-c-*-
2  *
3  * $Id: userdb.c,v 1.11 2004/04/08 01:36:20 mdw Exp $
4  *
5  * User database management
6  *
7  * (c) 1998 EBI
8  */
9
10 /*----- Licensing notice --------------------------------------------------*
11  *
12  * This file is part of `become'
13  *
14  * `Become' is free software; you can redistribute it and/or modify
15  * it under the terms of the GNU General Public License as published by
16  * the Free Software Foundation; either version 2 of the License, or
17  * (at your option) any later version.
18  *
19  * `Become' is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with `become'; if not, write to the Free Software Foundation,
26  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
27  */
28
29 /*----- Header files ------------------------------------------------------*/
30
31 /* --- ANSI headers --- */
32
33 #include <ctype.h>
34 #include <errno.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38
39 /* --- Unix headers --- */
40
41 #include "config.h"
42
43 #include <sys/types.h>
44
45 #include <grp.h>
46 #include <pwd.h>
47 #include <unistd.h>
48
49 /* --- mLib headers --- */
50
51 #include <mLib/alloc.h>
52 #include <mLib/sym.h>
53 #include <mLib/trace.h>
54
55 /* --- Local headers --- */
56
57 #include "become.h"
58 #include "userdb.h"
59 #include "ypstuff.h"
60
61 /*----- Type definitions --------------------------------------------------*/
62
63 /* --- A map link --- */
64
65 typedef struct userdb__node {
66   struct userdb__node *next;
67   void *rec;
68 } userdb__node;
69
70 /* --- A reference to a real record --- */
71
72 typedef struct userdb__sym {
73   sym_base _base;
74   void *rec;
75 } userdb__sym;
76
77 /* --- A name- and number-mapping --- */
78
79 typedef struct userdb__map {
80   sym_table nmap;
81   sym_table idmap;
82   userdb__node *list;
83 } userdb__map;
84
85 /*----- Static variables --------------------------------------------------*/
86
87 static userdb__map userdb__users;       /* Map of user info blocks */
88 static sym_iter userdb__useri;          /* Iterator for users */
89 static userdb__map userdb__groups;      /* Map of group info blocks */
90 static sym_iter userdb__groupi;         /* Iterator for groups */
91
92 /*----- Map management functions ------------------------------------------*/
93
94 /* --- @userdb__createMap@ --- *
95  *
96  * Arguments:   @userdb__map *m@ = pointer to a map block
97  *
98  * Returns:     ---
99  *
100  * Use:         Initialises a map table.
101  */
102
103 static void userdb__createMap(userdb__map *m)
104 {
105   sym_create(&m->nmap);
106   sym_create(&m->idmap);
107   m->list = 0;
108 }
109
110 /* --- @userdb__addToMap@ --- *
111  *
112  * Arguments:   @userdb__map *m@ = pointer to the map block
113  *              @const char *name@ = pointer to the item's name
114  *              @uid_t id@ = the item's id number
115  *              @void *rec@ = pointer to the actual record
116  *
117  * Returns:     ---
118  *
119  * Use:         Adds an item to the given map.
120  */
121
122 static void userdb__addToMap(userdb__map *m,
123                              const char *name,
124                              uid_t id, void *rec)
125 {
126   unsigned f;
127   userdb__sym *s;
128   userdb__node *n;
129
130   s = sym_find(&m->nmap, name, -1, sizeof(*s), &f);
131   if (!f)
132     s->rec = rec;
133
134   s = sym_find(&m->idmap, (char *)&id, sizeof(id), sizeof(*s), &f);
135   if (!f)
136     s->rec = rec;
137
138   n = xmalloc(sizeof(*n));
139   n->rec = rec;
140   n->next = m->list;
141   m->list = n;
142 }
143
144 /* --- @userdb__byName@ --- *
145  *
146  * Arguments:   @userdb__map *m@ = pointer to a map block
147  *              @const char *name@ = name to look up
148  *
149  * Returns:     A pointer to the appropriate block, or zero if not found.
150  *
151  * Use:         Looks up a name in a mapping and returns the result.
152  */
153
154 static void *userdb__byName(userdb__map *m, const char *name)
155 {
156   userdb__sym *s = sym_find(&m->nmap, name, -1, 0, 0);
157   return (s ? s->rec : 0);
158 }
159
160 /* --- @userdb__byId@ --- *
161  *
162  * Arguments:   @userdb__map *m@ = pointer to a map block
163  *              @uid_t id@ = id number to find
164  *
165  * Returns:     A pointer to the appropriate block, or zero if not found.
166  *
167  * Use:         Looks up an ID in a mapping, and returns the result.
168  */
169
170 static void *userdb__byId(userdb__map *m, uid_t id)
171 {
172   userdb__sym *s = sym_find(&m->idmap, (char *)&id, sizeof(id), 0, 0);
173   return (s ? s->rec : 0);
174 }
175
176 /* --- @userdb__clearMap@ --- *
177  *
178  * Arguments:   @userdb__map *m@ = pointer to a map block
179  *              @void (*freerec)(void *rec)@ = pointer to a free-record proc
180  *
181  * Returns:     ---
182  *
183  * Use:         Clears a map, emptying it and releasing the memory it
184  *              occupied.
185  */
186
187 static void userdb__clearMap(userdb__map *m, void (*freerec)(void *rec))
188 {
189   userdb__node *n, *t;
190
191   sym_destroy(&m->nmap);
192   sym_destroy(&m->idmap);
193
194   for (n = m->list; n; n = t) {
195     t = n->next;
196     freerec(n->rec);
197     free(n);
198   }
199 }
200
201 /*----- User and group block management -----------------------------------*/
202
203 /* --- @userdb__dumpUser@ --- *
204  *
205  * Arguments:   @const struct passwd *pw@ = pointer to a user block
206  *
207  * Returns:     ---
208  *
209  * Use:         Writes a user's informationt to a stream.
210  */
211
212 #ifndef NTRACE
213
214 static void userdb__dumpUser(const struct passwd *pw)
215 {
216   trace(TRACE_DEBUG,
217         "debug: name `%s' passwd `%s' uid %i gid %i",
218         pw->pw_name, pw->pw_passwd, (int)pw->pw_uid, (int)pw->pw_gid);
219   trace(TRACE_DEBUG,
220         "debug: ... gecos `%s' home `%s' shell `%s'",
221         pw->pw_gecos, pw->pw_dir, pw->pw_shell);
222 }
223
224 #endif
225
226 /* --- @userdb__split@ --- *
227  *
228  * Arguments:   @char *p@ = pointer to string
229  *              @char **v@ = pointer to vector to fill in
230  *              @int sz@ = maximum number of fields to split
231  *
232  * Returns:     Number of fields extracted.
233  *
234  * Use:         Splits a string into fields at colon characters.
235  */
236
237 static int userdb__split(char *p, char **v, int sz)
238 {
239   int count = 0;
240
241   *v++ = p; sz--; count++;
242   if (!sz)
243     goto done;
244   while (*p) {
245     if (*p++ == ':') {
246       p[-1] = 0;
247       *v++ = p; sz--; count++;
248       if (!sz)
249         goto done;
250     }
251   }
252   while (sz--)
253     *v++ = 0;
254
255 done:
256   return (count);
257 }
258
259 /* --- @userdb_copyUser@ --- *
260  *
261  * Arguments:   @struct passwd *pw@ = pointer to block to copy
262  *
263  * Returns:     Pointer to the copy.
264  *
265  * Use:         Copies a user block.  The copy is `deep' so all the strings
266  *              are copied too.  Free the copy with @userdb_freeUser@ when
267  *              you don't want it any more.
268  */
269
270 struct passwd *userdb_copyUser(struct passwd *pw)
271 {
272   struct passwd *npw;
273
274   if (!pw)
275     return (0);
276
277   npw = xmalloc(sizeof(*npw));
278
279   npw->pw_name = xstrdup(pw->pw_name);
280   npw->pw_passwd = xstrdup(pw->pw_passwd);
281   npw->pw_uid = pw->pw_uid;
282   npw->pw_gid = pw->pw_gid;
283   npw->pw_gecos = xstrdup(pw->pw_gecos);
284   npw->pw_dir = xstrdup(pw->pw_dir);
285   npw->pw_shell = xstrdup(pw->pw_shell);
286
287   return (npw);
288 }
289
290 /* --- @userdb__buildUser@ --- *
291  *
292  * Arguments:   @char *s@ = pointer to user string
293  *
294  * Returns:     Pointer to a user block.
295  *
296  * Use:         Converts a line from a user file into a password entry.
297  *              Note that the string is corrupted by @strtok@ while it gets
298  *              parsed.
299  */
300
301 static struct passwd *userdb__buildUser(char *s)
302 {
303   struct passwd *pw = xmalloc(sizeof(*pw));
304   char *v[7];
305
306   if (userdb__split(s, v, 7) < 7) {
307     free(pw);
308     return (0);
309   }
310
311   pw->pw_name = xstrdup(v[0]);
312   pw->pw_passwd = xstrdup(v[1]);
313   pw->pw_uid = (uid_t)atol(v[2]);
314   pw->pw_gid = (gid_t)atol(v[3]);
315   pw->pw_gecos = xstrdup(v[4]);
316   pw->pw_dir = xstrdup(v[5]);
317   pw->pw_shell = xstrdup(v[6]);
318   return (pw);
319 }
320
321 /* --- @userdb_freeUser@ --- *
322  *
323  * Arguments:   @void *rec@ = pointer to a user record
324  *
325  * Returns:     ---
326  *
327  * Use:         Frees a user record.
328  */
329
330 void userdb_freeUser(void *rec)
331 {
332   struct passwd *pw;
333
334   if (!rec)
335     return;
336
337   pw = rec;
338   free(pw->pw_name);
339   free(pw->pw_passwd);
340   free(pw->pw_gecos);
341   free(pw->pw_dir);
342   free(pw->pw_shell);
343   free(pw);
344
345
346 /* --- @userdb__dumpGroup@ --- *
347  *
348  * Arguments:   @const struct group *gr@ = pointer to a group block
349  *              @FILE *fp@ = pointer to stream to write on
350  *
351  * Returns:     ---
352  *
353  * Use:         Writes a group's information to a stream.
354  */
355
356 #ifndef NTRACE
357
358 static void userdb__dumpGroup(const struct group *gr)
359 {
360   char *const *p;
361
362   trace(TRACE_DEBUG,
363          "debug: name `%s' passwd `%s' gid %i",
364          gr->gr_name, gr->gr_passwd, (int)gr->gr_gid);
365   for (p = gr->gr_mem; *p; p++)
366     trace(TRACE_DEBUG,"debug: ... `%s'", *p);
367 }
368
369 #endif
370
371 /* --- @userdb_copyGroup@ --- *
372  *
373  * Arguments:   @struct group *gr@ = pointer to group block
374  *
375  * Returns:     Pointer to copied block
376  *
377  * Use:         Copies a group block.  The copy is `deep' so all the strings
378  *              are copied too.  Free the copy with @userdb_freeGroup@ when
379  *              you don't want it any more.
380  */
381
382 struct group *userdb_copyGroup(struct group *gr)
383 {
384   struct group *ngr;
385   int i, max;
386
387   if (!gr)
388     return (0);
389
390   ngr = xmalloc(sizeof(*ngr));
391
392   ngr->gr_name = xstrdup(gr->gr_name);
393   ngr->gr_passwd = xstrdup(gr->gr_passwd);
394   ngr->gr_gid = gr->gr_gid;
395
396   for (max = 0; gr->gr_mem[max]; max++)
397     ;
398   ngr->gr_mem = xmalloc((max + 1) * sizeof(char *));
399   for (i = 0; i < max; i++)
400     ngr->gr_mem[i] = xstrdup(gr->gr_mem[i]);
401   ngr->gr_mem[max] = 0;
402
403   return (ngr);
404 }
405
406 /* --- @userdb__buildGroup@ --- *
407  *
408  * Arguments:   @char *s@ = pointer to group line string
409  *
410  * Returns:     Pointer to a group block
411  *
412  * Use:         Parses an entry in the groups file.  The string is garbled
413  *              by @strtok@ as we go.
414  */
415
416 static struct group *userdb__buildGroup(char *s)
417 {
418   struct group *gr = xmalloc(sizeof(*gr));
419   char *v[4];
420   int i;
421
422   /* --- Do the easy bits --- */
423
424   if (userdb__split(s, v, 4) < 3) {
425     free(gr);
426     return (0);
427   }
428   gr->gr_name = xstrdup(v[0]);
429   gr->gr_passwd = xstrdup(v[1]);
430   gr->gr_gid = (gid_t)atol(v[2]);
431
432   /* --- Count the number of members --- */
433
434   s = v[3];
435   i = 0;
436   if (s && s[0]) {
437     for (;;) {
438       i++;
439       if ((s = strpbrk(s, ",")) == 0)
440         break;
441       s++;
442     }
443   }
444
445   /* --- Allocate the block and fill it --- */
446
447   gr->gr_mem = xmalloc((i + 1) * sizeof(char *));
448   i = 0;
449   if (v[3]) {
450     s = strtok(v[3], ",");
451     while (s) {
452       gr->gr_mem[i++] = xstrdup(s);
453       s = strtok(0, ",");
454     }
455   }
456   gr->gr_mem[i] = 0;
457
458   return (gr);
459 }
460
461 /* --- @userdb_freeGroup@ --- *
462  *
463  * Arguments:   @void *rec@ = pointer to a group record
464  *
465  * Returns:     ---
466  *
467  * Use:         Frees a group record.
468  */
469
470 void userdb_freeGroup(void *rec)
471 {
472   struct group *gr;
473   char **p;
474
475   if (!rec)
476     return;
477
478   gr = rec;
479   free(gr->gr_name);
480   free(gr->gr_passwd);
481   for (p = gr->gr_mem; *p; p++)
482     free(*p);
483   free(gr->gr_mem);
484   free(gr);
485
486
487 /*----- Answering queries -------------------------------------------------*/
488
489 /* --- @userdb_userByName@, @userdb_userById@ --- *
490  *
491  * Arguments:   @const char *name@ = pointer to user's name
492  *              @uid_t id@ = user id to find
493  *
494  * Returns:     Pointer to user block, or zero if not found.
495  *
496  * Use:         Looks up a user by name or id.
497  */
498
499 struct passwd *userdb_userByName(const char *name)
500 { return (userdb__byName(&userdb__users, name)); }
501
502 struct passwd *userdb_userById(uid_t id)
503 { return (userdb__byId(&userdb__users, id)); }
504
505 /* --- @userdb_iterateUsers@, @userdb_iterateUsers_r@ --- *
506  *
507  * Arguments:   @userdb_iter *i@ = pointer to a symbol table iterator object
508  *
509  * Returns:     ---
510  *
511  * Use:         Initialises an iteration for the user database.
512  */
513
514 void userdb_iterateUsers(void)
515 { userdb_iterateUsers_r(&userdb__useri); }
516
517 void userdb_iterateUsers_r(userdb_iter *i)
518 { sym_mkiter(i, &userdb__users.nmap); }
519
520 /* --- @userdb_nextUser@, @userdb_nextUser_r@ --- *
521  *
522  * Arguments:   @userdb_iter *i@ = pointer to a symbol table iterator oject
523  *
524  * Returns:     Pointer to the next user block, or null.
525  *
526  * Use:         Returns another user block.
527  */
528
529 struct passwd *userdb_nextUser(void)
530 { return (userdb_nextUser_r(&userdb__useri)); }
531
532 struct passwd *userdb_nextUser_r(userdb_iter *i)
533 {
534   userdb__sym *s = sym_next(i);
535   return (s ? s->rec : 0);
536 }
537
538 /* --- @userdb_groupByName@, @userdb_groupById@ --- *
539  *
540  * Arguments:   @const char *name@ = pointer to group's name
541  *              @gid_t id@ = group id to find
542  *
543  * Returns:     Pointer to group block, or zero if not found.
544  *
545  * Use:         Looks up a group by name or id.
546  */
547
548 struct group *userdb_groupByName(const char *name)
549 { return (userdb__byName(&userdb__groups, name)); }
550
551 struct group *userdb_groupById(gid_t id)
552 { return (userdb__byId(&userdb__groups, id)); }
553
554 /* --- @userdb_iterateGroups@, @userdb_iterateGroups_r@ --- *
555  *
556  * Arguments:   @userdb_iter *i@ = pointer to a symbol table iterator object
557  *
558  * Returns:     ---
559  *
560  * Use:         Initialises an iteration for the group database.
561  */
562
563 void userdb_iterateGroups(void)
564 { userdb_iterateGroups_r(&userdb__groupi); }
565
566 void userdb_iterateGroups_r(userdb_iter *i)
567 { sym_mkiter(i, &userdb__groups.nmap); }
568
569 /* --- @userdb_nextGroup@, @userdb_nextGroup_r@ --- *
570  *
571  * Arguments:   @userdb_iter *i@ = pointer to a symbol table iterator oject
572  *
573  * Returns:     Pointer to the next group block, or null.
574  *
575  * Use:         Returns another group block.
576  */
577
578 struct group *userdb_nextGroup(void)
579 { return (userdb_nextGroup_r(&userdb__groupi)); }
580
581 struct group *userdb_nextGroup_r(userdb_iter *i)
582 {
583   userdb__sym *s = sym_next(i);
584   return (s ? s->rec : 0);
585 }
586
587 /*----- Yellow pages support ----------------------------------------------*/
588
589 #ifdef HAVE_YP
590
591 /* --- @userdb__foreachUser@ --- *
592  *
593  * Arguments:   @int st@ = YP protocol-level status code
594  *              @char *k@ = address of the key for this record
595  *              @int ksz@ = size of the key
596  *              @char *v@ = address of the value for this record
597  *              @int vsz@ = size of the value
598  *              @char *data@ = pointer to some data passed to me
599  *
600  * Returns:     Zero to be called again, nonzero to end the enumeration.
601  *
602  * Use:         Handles an incoming user record.
603  */
604
605 static int userdb__foreachUser(int st, char *k, int ksz,
606                                char *v, int vsz, char *data)
607 {
608   char *cv;
609   struct passwd *pw;
610
611   if (st != YP_TRUE)
612     return (-1);
613   cv = xmalloc(vsz + 1);
614   memcpy(cv, v, vsz);
615   cv[vsz] = 0;
616   T( trace(TRACE_DEBUG, "debug: nis string: `%s'", cv); )
617   pw = userdb__buildUser(cv);
618   if (pw && !userdb__byName(&userdb__users, pw->pw_name)) {
619     IF_TRACING(TRACE_DEBUG, userdb__dumpUser(pw); )
620     userdb__addToMap(&userdb__users, pw->pw_name, pw->pw_uid, pw);
621   } else
622     userdb_freeUser(pw);
623   free(cv);
624   return (0);
625 }
626
627 /* --- @userdb__foreachGroup@ --- *
628  *
629  * Arguments:   @int st@ = YP protocol-level status code
630  *              @char *k@ = address of the key for this record
631  *              @int ksz@ = size of the key
632  *              @char *v@ = address of the value for this record
633  *              @int vsz@ = size of the value
634  *              @char *data@ = pointer to some data passed to me
635  *
636  * Returns:     Zero to be called again, nonzero to end the enumeration.
637  *
638  * Use:         Handles an incoming user record.
639  */
640
641 static int userdb__foreachGroup(int st, char *k, int ksz,
642                                 char *v, int vsz, char *data)
643 {
644   char *cv;
645   struct group *gr;
646
647   if (st != YP_TRUE)
648     return (-1);
649   cv = xmalloc(vsz + 1);
650   memcpy(cv, v, vsz);
651   cv[vsz] = 0;
652   T( trace(TRACE_DEBUG, "debug: nis string: `%s'", cv); )
653   gr = userdb__buildGroup(cv);
654   if (gr && !userdb__byName(&userdb__groups, gr->gr_name)) {
655     IF_TRACING(TRACE_DEBUG, userdb__dumpGroup(gr); )
656     userdb__addToMap(&userdb__groups, gr->gr_name, gr->gr_gid, gr);
657   } else
658     userdb_freeGroup(gr);
659   free(cv);
660   return (0);
661 }
662
663 /* --- @userdb_yp@ --- *
664  *
665  * Arguments:   ---
666  *
667  * Returns:     ---
668  *
669  * Use:         Fetches the YP database of users.
670  */
671
672 void userdb_yp(void)
673 {
674   /* --- Bind to a server --- */
675
676   ypstuff_bind();
677   if (!yp_domain)
678     return;
679
680   T( trace(TRACE_DEBUG, "debug: adding NIS users"); )
681
682   /* --- Fetch the users map --- */
683
684   {
685     static struct ypall_callback ucb = { userdb__foreachUser, 0 };
686     yp_all(yp_domain, "passwd.byuid", &ucb);
687   }
688
689   /* --- Fetch the groups map --- */
690
691   {
692     static struct ypall_callback gcb = { userdb__foreachGroup, 0 };
693     yp_all(yp_domain, "group.bygid", &gcb);
694   }
695 }
696
697 #else
698
699 void userdb_yp(void) { ; }
700
701 #endif
702
703 /*----- Building the databases --------------------------------------------*/
704
705 /* --- @userdb_local@ --- *
706  *
707  * Arguments:   ---
708  *
709  * Returns:     ---
710  *
711  * Use:         Reads the local list of users into the maps.
712  */
713
714 void userdb_local(void)
715 {
716   T( trace(TRACE_DEBUG, "debug: adding local users"); )
717
718   /* --- Fetch users first --- */
719
720   {
721     struct passwd *pw;
722
723     setpwent();
724     while ((pw = getpwent()) != 0) {
725       IF_TRACING(TRACE_DEBUG, userdb__dumpUser(pw); )
726       if (!userdb__byName(&userdb__users, pw->pw_name))
727         userdb__addToMap(&userdb__users, pw->pw_name, pw->pw_uid,
728                          userdb_copyUser(pw));
729     }
730     endpwent();
731   }
732
733   /* --- Then fetch groups --- */
734
735   {
736     struct group *gr;
737
738     setgrent();
739     while ((gr = getgrent()) != 0) {
740       IF_TRACING(TRACE_DEBUG, userdb__dumpGroup(gr); )
741       if (!userdb__byName(&userdb__groups, gr->gr_name))
742         userdb__addToMap(&userdb__groups, gr->gr_name, gr->gr_gid,
743                          userdb_copyGroup(gr));
744     }
745     endgrent();
746   }
747 }
748
749 /* --- @userdb_init@ --- *
750  *
751  * Arguments:   ---
752  *
753  * Returns:     ---
754  *
755  * Use:         Initialises the user database.
756  */
757
758 void userdb_init(void)
759 {
760   userdb__createMap(&userdb__users);
761   userdb__createMap(&userdb__groups);
762 }
763
764 /* --- @userdb_end@ --- *
765  *
766  * Arguments:   ---
767  *
768  * Returns:     ---
769  *
770  * Use:         Closes down the user database.
771  */
772
773 void userdb_end(void)
774 {
775   userdb__clearMap(&userdb__users, userdb_freeUser);
776   userdb__clearMap(&userdb__groups, userdb_freeGroup);
777 }
778
779 /*----- Test rig ----------------------------------------------------------*/
780
781 #ifdef TEST_RIG
782
783 void dumpit(const char *msg)
784 {
785   trace(TRACE_DEBUG, "debug: %s", msg);
786
787   {
788     struct passwd *pw;
789     for (userdb_iterateUsers(); (pw = userdb_nextUser()) != 0; )
790       userdb__dumpUser(pw);
791   }
792
793   {
794     struct group *gr;
795     for (userdb_iterateGroups(); (gr = userdb_nextGroup()) != 0; )
796       userdb__dumpGroup(gr);
797   }
798 }
799
800 int main(void)
801 {
802   ego("userdb-test");
803   trace_on(stdout, TRACE_ALL);
804   userdb_init();
805   userdb_local();
806   userdb_yp();
807   dumpit("spong");
808 /*  printf("loaded (%lu)\n", track_memused()); */
809   getchar();
810   for (;;) {
811     userdb_end();
812 /*    printf("cleared (%lu)\n", track_memused()); */
813 /*    track_memlist(); */
814     userdb_init();
815     userdb_local();
816     userdb_yp();
817 /*    printf("reloaded (%lu)\n", track_memused()); */
818     getchar();
819   }
820   return (0);
821 }
822
823 #endif
824
825 /*----- That's all, folks -------------------------------------------------*/