chiark / gitweb /
fixes
[inn-innduct.git] / backends / actsync.c
1 /* @(#) $Id: actsync.c 6372 2003-05-31 19:48:28Z rra $ */
2 /* @(#) Under RCS control in /usr/local/news/src/inn/local/RCS/actsync.c,v */
3 /*
4  * actsync - sync or merge two active files
5  *
6  * usage:
7  *    actsync [-b hostid][-d hostid][-g max][-i ignore_file][-I][-k][-l hostid]
8  *            [-m][-n name][-o fmt][-p %][-q hostid][-s size]
9  *            [-t hostid][-T][-v verbose_lvl][-z sec]
10  *            [host1] host2
11  *
12  *      -A              use authentication to server
13  *      -b hostid       ignore *.bork.bork.bork groups from:      (def: -b 0)
14  *                          0   from neither host
15  *                          1   from host1
16  *                          2   from host2
17  *                          12  from host1 and host2
18  *                          21  from host1 and host2
19  *      -d hostid       ignore groups with all numeric components (def: -d 0)
20  *      -g max          ignore group >max levels (0=dont ignore)  (def: -g 0)
21  *      -i ignore_file  file with list/types of groups to ignore  (def: no file)
22  *      -I hostid       ignore_file applies only to hostid        (def: -I 12)
23  *      -k              keep host1 groups with errors             (def: remove)
24  *      -l hostid       flag =group problems as errors            (def: -l 12)
25  *      -m              merge, keep group not on host2            (def: sync)
26  *      -n name         name given to ctlinnd newgroup commands   (def: actsync)
27  *      -o fmt          type of output:                           (def: -o c)
28  *                          a   output groups in active format
29  *                          a1  like 'a', but output ignored non-err host1 grps
30  *                          ak  like 'a', keep host2 hi/low values on new groups
31  *                          aK  like 'a', use host2 hi/low values always
32  *                          c   output in ctlinnd change commands
33  *                          x   no output, safely exec ctlinnd commands
34  *                          xi  no output, safely exec commands interactively
35  *      -p %            min % host1 lines unchanged allowed       (def: -p 96)
36  *      -q hostid       silence errors from a host (see -b)       (def: -q 0)
37  *      -s size         ignore names longer than size (0=no lim)  (def: -s 0)
38  *      -t hostid       ignore bad top level groups from:(see -b) (def: -t 2)
39  *      -T              no new hierarchies                        (def: allow)
40  *      -v verbose_lvl  verbosity level                           (def: -v 0)
41  *                          0   no debug or status reports
42  *                          1   summary if work done
43  *                          2   summary & actions (if exec output) only if done
44  *                          3   summary & actions (if exec output)
45  *                          4   debug output plus all -v 3 messages
46  *      -z sec          sleep sec seconds per exec if -o x        (def: -z 4)
47  *      host1           host to be changed                  (def: local server)
48  *      host2           reference host used in merge
49  */
50 /* 
51  * By: Landon Curt Noll         chongo@toad.com         (chongo was here /\../\)
52  *
53  * Copyright (c) Landon Curt Noll, 1996.
54  * All rights reserved.
55  *
56  * Permission to use and modify is hereby granted so long as this 
57  * notice remains.  Use at your own risk.  No warranty is implied.
58  */
59
60 #include "config.h"
61 #include "clibrary.h"
62 #include "portable/wait.h"
63 #include <ctype.h>
64 #include <dirent.h>
65 #include <fcntl.h>
66 #include <errno.h>
67 #include <math.h>
68 #include <sys/stat.h>
69 #include <signal.h>
70
71 #include "inn/innconf.h"
72 #include "inn/messages.h"
73 #include "inn/qio.h"
74 #include "libinn.h"
75 #include "paths.h"
76
77 static const char usage[] = "\
78 Usage: actsync [-A][-b hostid][-d hostid][-i ignore_file][-I hostid][-k]\n\
79         [-l hostid][-m][-n name][-o fmt][-p min_%_unchg][-q hostid]\n\
80         [-s size][-t hostid][-T][-v verbose_lvl][-z sec]\n\
81         [host1] host2\n\
82 \n\
83     -A          use authentication to server\n\
84     -b hostid   ignore *.bork.bork.bork groups from:    (def: -b 0)\n\
85                 0       from neither host\n\
86                 1       from host1\n\
87                 2       from host2\n\
88                 12      from host1 and host2\n\
89                 21      from host1 and host2\n\
90     -d hostid   ignore grps with all numeric components (def: -d 0)\n\
91     -g max      ignore group >max levels (0=don't)      (def: -g 0)\n\
92     -i file     file with groups to ignore              (def: no file)\n\
93     -I hostid   ignore_file applies only to hostid      (def: -I 12)\n\
94     -k          keep host1 groups with errors           (def: remove)\n\
95     -l hostid   flag =group problems as errors          (def: -l 12)\n\
96     -m          merge, keep group not on host2          (def: sync)\n\
97     -n name     name given to ctlinnd newgroup cmds     (def: actsync)\n\
98     -o fmt      type of output:                         (def: -o c)\n\
99                 a       output groups in active format\n\
100                 a1      like 'a', but output ignored non-err host1 grps\n\
101                 ak      like 'a', keep host2 hi/low values on new groups\n\
102                 aK      like 'a', use host2 hi/low values always\n\
103                 c       output in ctlinnd change commands\n\
104                 x       no output, safely exec ctlinnd commands\n\
105                 xi      no output, safely exec commands interactively\n\
106     -p %        min % host1 lines unchanged allowed     (def: -p 96)\n\
107     -q hostid   silence errors from a host (see -b)     (def: -q 0)\n\
108     -s size     ignore names > than size (0=no lim)     (def: -s 0)\n\
109     -t hostid   ignore bad top level grps from: (see -b)(def: -t 2)\n\
110     -T          no new hierarchies                      (def: allow)\n\
111     -v level    verbosity level                         (def: -v 0)\n\
112                 0       no debug or status reports\n\
113                 1       summary if work done\n\
114                 2       summary & actions (if exec output) only if done\n\
115                 3       summary & actions (if exec output)\n\
116                 4       debug output plus all -v 3 messages\n\
117     -z sec      sleep sec seconds per exec if -o x      (def: -z 4)\n\
118 \n\
119     host1       host to be changed                      (def: local server)\n\
120     host2       reference host used in merge\n";
121
122
123 /*
124  * pat - internal ignore/check pattern
125  *
126  * A pattern, derived from an ignore file, will determine if a group
127  * is will be checked if it is on both hosts or ignored altogether.
128  *
129  * The type related to the 4th field of an active file.  Types may
130  * currently be one of [ymjnx=].  If '=' is one of the types, an
131  * optional equivalence pattern may be given in the 'epat' element.
132  *
133  * For example, to ignore "foo.bar.*", if it is junked or equated to
134  * a group of the form "alt.*.foo.bar.*":
135  *
136  *      x.pat = "foo.bar.*";
137  *      x.type = "j=";
138  *      x.epat = "alt.*.foo.bar.*";
139  *      x.ignore = 1;
140  *
141  * To further check "foo.bar.mod" if it is moderated:
142  *
143  *      x.pat = "foo.bar.mod";
144  *      x.type = "m";
145  *      x.epat = NULL;
146  *      x.ignore = 0;
147  *
148  * The 'i' value means ignore, 'c' value means 'compare'.   The last pattern
149  * that matches a group determines the fate of the group.  By default all
150  * groups are included.
151  */
152 struct pat {
153     char *pat;          /* newsgroup pattern */
154     int type_match;     /* 1 => match only if group type matches */
155     int y_type;         /* 1 => match if a 'y' type group */
156     int m_type;         /* 1 => match if a 'm' type group */
157     int n_type;         /* 1 => match if a 'n' type group */
158     int j_type;         /* 1 => match if a 'j' type group */
159     int x_type;         /* 1 => match if a 'x' type group */
160     int eq_type;        /* 1 => match if a 'eq' type group */
161     char *epat;         /* =pattern to match, if non-NULL and = is in type */
162     int ignore;         /* 0 => check matching group, 1 => ignore it */
163 };
164
165 /* internal representation of an active line */
166 struct grp {
167     int ignore;         /* ignore reason, 0 => not ignore (see below) */
168     int hostid;         /* HOSTID this group is from */
169     int linenum;        /* >0 => active line number, <=0 => not a line */
170     int output;         /* 1 => output to produce the merged active file */
171     int remove;         /* 1 => remove this group */
172     char *name;         /* newsgroup name */
173     char *hi;           /* high article string */
174     char *low;          /* low article string */
175     char *type;         /* newsgroup type string */
176     char *outhi;        /* output high article string */
177     char *outlow;       /* output low article string */
178     char *outtype;      /* output newsgroup type string */
179 };
180
181 /* structure used in the process of looking for =group type problems */
182 struct eqgrp {
183     int skip;           /* 1 => skip this entry */
184     struct grp *g;      /* =group that is being examined */
185     char *eq;           /* current equivalence name */
186 };
187
188 /*
189  * These ignore reasons are listed in order severity; from mild to severe.
190  */
191 #define NOT_IGNORED     0x0000  /* newsgroup has not been ignored */
192 #define CHECK_IGNORE    0x0001  /* ignore file ignores this entry */
193 #define CHECK_TYPE      0x0002  /* group type is ignored */
194 #define CHECK_BORK      0x0004  /* group is a *.bork.bork.bork group */
195 #define CHECK_HIER      0x0008  /* -T && new group's hierarchy does not exist */
196 #define ERROR_LONGLOOP  0x0010  /* =name refers to long =grp chain or cycle */
197 #define ERROR_EQLOOP    0x0020  /* =name refers to itself in some way */
198 #define ERROR_NONEQ     0x0040  /* =name does not refer to a valid group */
199 #define ERROR_DUP       0x0080  /* newsgroup is a duplicate of another */
200 #define ERROR_EQNAME    0x0100  /* =name is a bad group name */
201 #define ERROR_BADTYPE   0x0200  /* newsgroup type is invalid */
202 #define ERROR_BADNAME   0x0400  /* newsgroup name is invalid */
203 #define ERROR_FORMAT    0x0800  /* entry line is malformed */
204
205 #define IS_IGNORE(ign) ((ign) & (CHECK_IGNORE|CHECK_TYPE|CHECK_BORK|CHECK_HIER))
206 #define IS_ERROR(ign) ((ign) & ~(CHECK_IGNORE|CHECK_TYPE|CHECK_BORK|CHECK_HIER))
207
208 #define NOHOST 0                /* neither host1 nor host2 */
209 #define HOSTID1 1               /* entry from the first host */
210 #define HOSTID2 2               /* entry from the second host */
211
212 #define CHUNK 5000              /* number of elements to alloc at a time */
213
214 #define TYPES "ymjnx="          /* group types (1st char of 4th active fld) */
215 #define TYPECNT (sizeof(TYPES)-1)
216
217 #define DEF_HI   "0000000000"   /* default hi string value for new groups */
218 #define DEF_LOW  "0000000001"   /* default low string value for new groups */
219 #define WATER_LEN 10            /* string length of hi/low water mark */
220
221 #define DEF_NAME "actsync"      /* default name to use for ctlinnd newgroup */
222
223 #define MIN_UNCHG (double)96.0  /* min % of host1 lines unchanged allowed */
224
225 #define DEV_NULL "/dev/null"    /* path to the bit bucket */
226 #define CTLINND_NAME "ctlinnd"  /* basename of ctlinnd command */
227 #define CTLINND_TIME_OUT "-t30" /* seconds to wait before timeout */
228
229 #define READ_SIDE 0             /* read side of a pipe */
230 #define WRITE_SIDE 1            /* write side of a pipe */
231
232 #define EQ_LOOP 16              /* give up if =eq loop/chain is this long */
233 #define NOT_REACHED 127         /* exit value if unable to get active files */
234
235 #define NEWGRP_EMPTY 0          /* no new group dir was found */
236 #define NEWGRP_NOCHG 1          /* new group dir found but no hi/low change */
237 #define NEWGRP_CHG 2            /* new group dir found but no hi/low change */
238
239 /* -b macros */
240 #define BORK_CHECK(hostid)  \
241     ((hostid == HOSTID1 && bork_host1_flag) || \
242      (hostid == HOSTID2 && bork_host2_flag))
243
244 /* -d macros */
245 #define NUM_CHECK(hostid)  \
246     ((hostid == HOSTID1 && num_host1_flag) || \
247      (hostid == HOSTID2 && num_host2_flag))
248
249 /* -t macros */
250 #define TOP_CHECK(hostid)  \
251     ((hostid == HOSTID1 && t_host1_flag) || \
252      (hostid == HOSTID2 && t_host2_flag))
253
254 /* -o output types */
255 #define OUTPUT_ACTIVE 1         /* output in active file format */
256 #define OUTPUT_CTLINND 2        /* output in ctlinnd change commands */
257 #define OUTPUT_EXEC 3           /* no output, safely exec commands */
258 #define OUTPUT_IEXEC 4          /* no output, exec commands interactively */
259
260 /* -q macros */
261 #define QUIET(hostid)  \
262     ((hostid == HOSTID1 && quiet_host1) || (hostid == HOSTID2 && quiet_host2))
263
264 /* -v verbosity level */
265 #define VER_MIN 0               /* minimum -v level */
266 #define VER_NONE 0              /* no -v output */
267 #define VER_SUMM_IF_WORK 1      /* output summary if actions were performed */
268 #define VER_REPT_IF_WORK 2      /* output summary & actions only if performed */
269 #define VER_REPORT 3            /* output summary & actions performed */
270 #define VER_FULL 4              /* output all summary, actins and debug */
271 #define VER_MAX 4               /* maximum -v level */
272 #define D_IF_SUMM (v_flag >= VER_SUMM_IF_WORK) /* true => give summary always */
273 #define D_REPORT (v_flag >= VER_REPT_IF_WORK)  /* true => give reports */
274 #define D_BUG (v_flag == VER_FULL)             /* true => debug processing */
275 #define D_SUMMARY (v_flag >= VER_REPORT)       /* true => give summary always */
276
277 /* flag and arg related defaults */
278 int bork_host1_flag = 0;        /* 1 => -b 1 or -b 12 or -b 21 given */
279 int bork_host2_flag = 0;        /* 1 => -b 2 or -b 12 or -b 21 given */
280 int num_host1_flag = 0;         /* 1 => -d 1 or -d 12 or -d 21 given */
281 int num_host2_flag = 0;         /* 1 => -d 2 or -d 12 or -d 21 given */
282 char *ign_file = NULL;          /* default ignore file */
283 int ign_host1_flag = 1;         /* 1 => -i ign_file applies to host1 */
284 int ign_host2_flag = 1;         /* 1 => -i ign_file applies to host2 */
285 int g_flag = 0;                 /* ignore grps deeper than > g_flag, 0=>dont */
286 int k_flag = 0;                 /* 1 => -k given */
287 int l_host1_flag = HOSTID1;     /* HOSTID1 => host1 =group error detection */
288 int l_host2_flag = HOSTID2;     /* HOSTID2 => host2 =group error detection */
289 int m_flag = 0;                 /* 1 => merge active files, don't sync */
290 const char *new_name = DEF_NAME;        /* ctlinnd newgroup name */
291 int o_flag = OUTPUT_CTLINND;    /* default output type */
292 double p_flag = MIN_UNCHG;      /* min % host1 lines allowed to be unchanged */
293 int host1_errs = 0;             /* errors found in host1 active file */
294 int host2_errs = 0;             /* errors found in host2 active file */
295 int quiet_host1 = 0;            /* 1 => -q 1 or -q 12 or -q 21 given */
296 int quiet_host2 = 0;            /* 1 => -q 2 or -q 12 or -q 21 given */
297 int s_flag = 0;                 /* max group size (length), 0 => do not check */
298 int t_host1_flag = 0;           /* 1 => -t 1 or -t 12 or -t 21 given */
299 int t_host2_flag = 1;           /* 1 => -t 2 or -d 12 or -t 21 given */
300 int no_new_hier = 0;            /* 1 => -T; no new hierarchies */
301 int host2_hilow_newgrp = 0;     /* 1 => use host2 hi/low on new groups */
302 int host2_hilow_all = 0;        /* 1 => use host2 hi/low on all groups */
303 int host1_ign_print = 0;        /* 1 => print host1 ignored groups too */
304 int v_flag = 0;                 /* default verbosity level */
305 int z_flag = 4;                 /* sleep z_flag sec per exec if -o x */
306 int A_flag = 0;
307
308 /* forward declarations */
309 static struct grp *get_active();    /* get an active file from a remote host */
310 static int bad_grpname();           /* test if string is a valid group name */
311 static struct pat *get_ignore();    /* read in an ignore file */
312 static void ignore();               /* ignore newsgroups given an ignore list */
313 static int merge_cmp();             /* qsort compare for active file merge */
314 static void merge_grps();           /* merge groups from active files */
315 static int active_cmp();            /* qsort compare for active file output */
316 static void output_grps();          /* output the merged groups */
317 static void process_args();         /* process command line arguments */
318 static void error_mark();           /* mark for removal, error grps from host */
319 static int eq_merge_cmp();          /* qsort compare for =type grp processing */
320 static int mark_eq_probs();         /* mark =type problems from a host */
321 static int exec_cmd();              /* exec a ctlinnd command */
322 static int new_top_hier();          /* see if we have a new top level */
323
324 int
325 main(argc, argv)
326     int  argc;                  /* arg count */
327     char *argv[];               /* the args */
328 {
329     struct grp *grp;            /* struct grp array for host1 & host2 */
330     struct pat *ignor;          /* ignore list from ignore file */
331     int grplen;                 /* length of host1/host2 group array */
332     int iglen;                  /* length of ignore list */
333     char *host1;                /* host to change */
334     char *host2;                /* comparison host */
335
336     /* First thing, set up our identity. */
337     message_program_name = "actsync";
338
339     /* Read in default info from inn.conf. */
340     if (!innconf_read(NULL))
341         exit(1);
342     process_args(argc, argv, &host1, &host2);
343
344     /* obtain the active files */
345     grp = get_active(host1, HOSTID1, &grplen, NULL, &host1_errs);
346     grp = get_active(host2, HOSTID2, &grplen, grp, &host2_errs);
347
348     /* ignore groups from both active files, if -i */
349     if (ign_file != NULL) {
350
351         /* read in the ignore file */
352         ignor = get_ignore(ign_file, &iglen);
353
354         /* ignore groups */
355         ignore(grp, grplen, ignor, iglen);
356     }
357
358     /* compare groups from both hosts */
359     merge_grps(grp, grplen, host1, host2);
360
361     /* mark for removal, error groups from host1 if -e */
362     if (! k_flag) {
363
364         /* mark error groups for removal */
365         error_mark(grp, grplen, HOSTID1);
366     }
367
368     /* output result of merge */
369     output_grps(grp, grplen);
370
371     /* all done */
372     exit(0);
373 }
374
375 /*
376  * process_args - process the command line arguments
377  *
378  * given:
379  *      argc    arg count
380  *      argv    the args
381  *      host1   name of first host (may be 2nd if -R)
382  *      host2   name of second host2 *may be 1st if -R)
383  */
384 static void
385 process_args(argc, argv, host1, host2)
386     int argc;           /* arg count */
387     char *argv[];       /* the arg array */
388     char **host1;       /* where to place name of host1 */
389     char **host2;       /* where to place name of host2 */
390 {
391     char *def_serv = NULL;      /* name of default server */
392     int i;
393
394     /* parse args */
395     while ((i = getopt(argc,argv,"Ab:d:g:i:I:kl:mn:o:p:q:s:t:Tv:z:")) != EOF) {
396         switch (i) {
397         case 'A':
398             A_flag = 1;
399             break;
400         case 'b':               /* -b {0|1|2|12|21} */
401             switch (atoi(optarg)) {
402             case 0:
403                 bork_host1_flag = 0;
404                 bork_host2_flag = 0;
405                 break;
406             case 1:
407                 bork_host1_flag = 1;
408                 break;
409             case 2:
410                 bork_host2_flag = 1;
411                 break;
412             case 12:
413             case 21:
414                 bork_host1_flag = 1;
415                 bork_host2_flag = 1;
416                 break;
417             default:
418                 warn("-b option must be 0, 1, 2, 12, or 21");
419                 die("%s", usage);
420             }
421             break;
422         case 'd':               /* -d {0|1|2|12|21} */
423             switch (atoi(optarg)) {
424             case 0:
425                 num_host1_flag = 0;
426                 num_host2_flag = 0;
427                 break;
428             case 1:
429                 num_host1_flag = 1;
430                 break;
431             case 2:
432                 num_host2_flag = 1;
433                 break;
434             case 12:
435             case 21:
436                 num_host1_flag = 1;
437                 num_host2_flag = 1;
438                 break;
439             default:
440                 warn("-d option must be 0, 1, 2, 12, or 21");
441                 die("%s", usage);
442             }
443             break;
444         case 'g':               /* -g max */
445             g_flag = atoi(optarg);
446             break;
447         case 'i':               /* -i ignore_file */
448             ign_file = optarg;
449             break;
450         case 'I':               /* -I {0|1|2|12|21} */
451             switch (atoi(optarg)) {
452             case 0:
453                 ign_host1_flag = 0;
454                 ign_host2_flag = 0;
455                 break;
456             case 1:
457                 ign_host1_flag = 1;
458                 ign_host2_flag = 0;
459                 break;
460             case 2:
461                 ign_host1_flag = 0;
462                 ign_host2_flag = 1;
463                 break;
464             case 12:
465             case 21:
466                 ign_host1_flag = 1;
467                 ign_host2_flag = 1;
468                 break;
469             default:
470                 warn("-I option must be 0, 1, 2, 12, or 21");
471                 die("%s", usage);
472             }
473             break;
474         case 'k':               /* -k */
475             k_flag = 1;
476             break;
477         case 'l':               /* -l {0|1|2|12|21} */
478             switch (atoi(optarg)) {
479             case 0:
480                 l_host1_flag = NOHOST;
481                 l_host2_flag = NOHOST;
482                 break;
483             case 1:
484                 l_host1_flag = HOSTID1;
485                 l_host2_flag = NOHOST;
486                 break;
487             case 2:
488                 l_host1_flag = NOHOST;
489                 l_host2_flag = HOSTID2;
490                 break;
491             case 12:
492             case 21:
493                 l_host1_flag = HOSTID1;
494                 l_host2_flag = HOSTID2;
495                 break;
496             default:
497                 warn("-l option must be 0, 1, 2, 12, or 21");
498                 die("%s", usage);
499             }
500             break;
501         case 'm':               /* -m */
502             m_flag = 1;
503             break;
504         case 'n':               /* -n name */
505             new_name = optarg;
506             break;
507         case 'o':               /* -o out_type */
508             switch (optarg[0]) {
509             case 'a':
510                 o_flag = OUTPUT_ACTIVE;
511                 switch (optarg[1]) {
512                 case '1':
513                     switch(optarg[2]) {
514                     case 'K':   /* -o a1K */
515                         host1_ign_print = 1;
516                         host2_hilow_all = 1;
517                         host2_hilow_newgrp = 1;
518                         break;
519                     case 'k':   /* -o a1k */
520                         host1_ign_print = 1;
521                         host2_hilow_newgrp = 1;
522                         break;
523                     default:    /* -o a1 */
524                         host1_ign_print = 1;
525                         break;
526                     }
527                     break;
528                 case 'K':
529                     switch(optarg[2]) {
530                     case '1':   /* -o aK1 */
531                         host1_ign_print = 1;
532                         host2_hilow_all = 1;
533                         host2_hilow_newgrp = 1;
534                         break;
535                     default:    /* -o aK */
536                         host2_hilow_all = 1;
537                         host2_hilow_newgrp = 1;
538                         break;
539                     };
540                     break;
541                 case 'k':
542                     switch(optarg[2]) {
543                     case '1':   /* -o ak1 */
544                         host1_ign_print = 1;
545                         host2_hilow_newgrp = 1;
546                         break;
547                     default:    /* -o ak */
548                         host2_hilow_newgrp = 1;
549                         break;
550                     };
551                     break;
552                 case '\0':      /* -o a */
553                     break;
554                 default:
555                     warn("-o type must be a, a1, ak, aK, ak1, or aK1");
556                     die("%s", usage);
557                 }
558                 break;
559             case 'c':
560                 o_flag = OUTPUT_CTLINND;
561                 break;
562             case 'x':
563                 if (optarg[1] == 'i') {
564                     o_flag = OUTPUT_IEXEC;
565                 } else {
566                     o_flag = OUTPUT_EXEC;
567                 }
568                 break;
569             default:
570                 warn("-o type must be a, a1, ak, aK, ak1, aK1, c, x, or xi");
571                 die("%s", usage);
572             }
573             break;
574         case 'p':               /* -p %_min_host1_change */
575             /* parse % into [0,100] */
576             p_flag = atof(optarg);
577             if (p_flag > (double)100.0) {
578                 p_flag = (double)100.0;
579             } else if (p_flag < (double)0.0) {
580                 p_flag = (double)0.0;
581             }
582             break;
583         case 'q':               /* -q {0|1|2|12|21} */
584             switch (atoi(optarg)) {
585             case 0:
586                 quiet_host1 = 0;
587                 quiet_host2 = 0;
588                 break;
589             case 1:
590                 quiet_host1 = 1;
591                 break;
592             case 2:
593                 quiet_host2 = 1;
594                 break;
595             case 12:
596             case 21:
597                 quiet_host1 = 1;
598                 quiet_host2 = 1;
599                 break;
600             default:
601                 warn("-q option must be 0, 1, 2, 12, or 21");
602                 die("%s", usage);
603             }
604             break;
605         case 's':               /* -s size */
606             s_flag = atoi(optarg);
607             break;
608         case 't':               /* -t {0|1|2|12|21} */
609             switch (atoi(optarg)) {
610             case 0:
611                 t_host1_flag = NOHOST;
612                 t_host2_flag = NOHOST;
613                 break;
614             case 1:
615                 t_host1_flag = HOSTID1;
616                 t_host2_flag = NOHOST;
617                 break;
618             case 2:
619                 t_host1_flag = NOHOST;
620                 t_host2_flag = HOSTID2;
621                 break;
622             case 12:
623             case 21:
624                 t_host1_flag = HOSTID1;
625                 t_host2_flag = HOSTID2;
626                 break;
627             default:
628                 warn("-t option must be 0, 1, 2, 12, or 21");
629                 die("%s", usage);
630             }
631             break;
632         case 'T':               /* -T */
633             no_new_hier = 1;
634             break;
635         case 'v':               /* -v verbose_lvl */
636             v_flag = atoi(optarg);
637             if (v_flag < VER_MIN || v_flag > VER_MAX) {
638                 warn("-v level must be >= %d and <= %d", VER_MIN, VER_MAX);
639                 die("%s", usage);
640             }
641             break;
642         case 'z':               /* -z sec */
643             z_flag = atoi(optarg);
644             break;
645         default:
646             warn("unknown flag");
647             die("%s", usage);
648         }
649     }
650
651     /* process the remaining args */
652     argc -= optind;
653     argv += optind;
654     *host1 = NULL;
655     switch (argc) {
656     case 1:
657         /* assume host1 is the local server */
658         *host2 = argv[0];
659         break;
660     case 2:
661         *host1 = argv[0];
662         *host2 = argv[1];
663         break;
664     default:
665         warn("expected 1 or 2 host args, found %d", argc);
666         die("%s", usage);
667     }
668
669     /* determine default host name if needed */
670     if (*host1 == NULL || strcmp(*host1, "-") == 0) {
671         def_serv = innconf->server;
672         *host1 = def_serv;
673     }
674     if (*host2 == NULL || strcmp(*host2, "-") == 0) {
675         def_serv = innconf->server;
676         *host2 = def_serv;
677     }
678     if (*host1 == NULL || *host2 == NULL)
679         die("unable to determine default server name");
680     if (D_BUG && def_serv != NULL)
681         warn("STATUS: using default server: %s", def_serv);
682
683     /* processing done */
684     return;
685 }
686
687 /*
688  * get_active - get an active file from a host
689  *
690  * given:
691  *      host    host to contact or file to read, NULL => local server
692  *      hostid  HOST_ID of host
693  *      len     pointer to length of grp return array
694  *      grp     existing host array to add, or NULL
695  *      errs    count of lines that were found to have some error
696  *
697  * returns;
698  *      Pointer to an array of grp structures describing each active entry.
699  *      Does not return on fatal error.
700  *
701  * If host starts with a '/' or '.', then it is assumed to be a local file.
702  * In that case, the local file is opened and read.
703  */
704 static struct grp *
705 get_active(host, hostid, len, grp, errs)
706     char *host;                 /* the host to contact */
707     int hostid;                 /* HOST_ID of host */
708     int *len;                   /* length of returned grp array in elements */
709     struct grp* grp;            /* existing group array or NULL */
710     int *errs;                  /* line error count */
711 {
712     FILE *active;               /* stream for fetched active data */
713     FILE *FromServer;           /* stream from server */
714     FILE *ToServer;             /* stream to server */
715     QIOSTATE *qp;               /* QIO active state */
716     char buff[8192+1];          /* QIO buffer */
717     char *line;                 /* the line just read */
718     struct grp *ret;            /* array of groups to return */
719     struct grp *cur;            /* current grp entry being formed */
720     int max;                    /* max length of ret */
721     int cnt;                    /* number of entries read */
722     int ucnt;                   /* number of entries to be used */
723     int namelen;                /* length of newsgroup name */
724     int is_file;                /* 1 => host is actually a filename */
725     int num_check;              /* true => check for all numeric components */
726     char *rhost;
727     int rport;
728     char *p;
729     int i;
730
731     /* firewall */
732     if (len == NULL)
733         die("internal error #1: len is NULL");
734     if (errs == NULL)
735         die("internal error #2: errs in NULL");
736     if (D_BUG)
737         warn("STATUS: obtaining active file from %s", host);
738
739     /* setup return array if needed */
740     if (grp == NULL) {
741         ret = xmalloc(CHUNK * sizeof(struct grp));
742         max = CHUNK;
743         *len = 0;
744
745     /* or prep to use the existing array */
746     } else {
747         ret = grp;
748         max = ((*len + CHUNK-1)/CHUNK)*CHUNK;
749     }
750
751     /* check for host being a filename */
752     if (host != NULL && (host[0] == '/' || host[0] == '.')) {
753
754         /* note that host is actually a file */
755         is_file = 1;
756
757         /* setup to read the local file quickly */
758         if ((qp = QIOopen(host)) == NULL)
759             sysdie("cannot open active file");
760
761     /* case: host is a hostname or NULL (default server) */
762     } else {
763
764         /* note that host is actually a hostname or NULL */
765         is_file = 0;
766
767         /* prepare remote host variables */
768         if ((p = strchr(host, ':')) != NULL) {
769                 rport = atoi(p + 1);
770                 *p = '\0';
771                 rhost = xstrdup(host);
772                 *p = ':';
773         } else {
774                 rhost = xstrdup(host);
775                 rport = NNTP_PORT;
776         }
777
778         /* open a connection to the server */
779         buff[0] = '\0';
780         if (NNTPconnect(rhost, rport, &FromServer, &ToServer, buff) < 0)
781             die("cannot connect to server: %s",
782                 buff[0] ? buff : strerror(errno));
783
784         if (A_flag && NNTPsendpassword(rhost, FromServer, ToServer) < 0)
785             die("cannot authenticate to server");
786
787         free(rhost);
788
789         /* get the active data from the server */
790         active = CAlistopen(FromServer, ToServer, NULL);
791         if (active == NULL)
792             sysdie("cannot retrieve data");
793
794         /* setup to read the retrieved data quickly */
795         if ((qp = QIOfdopen((int)fileno(active))) == NULL)
796             sysdie("cannot read temp file");
797     }
798
799     /* scan server's output, processing appropriate lines */
800     num_check = NUM_CHECK(hostid);
801     for (cnt=0, ucnt=0; (line = QIOread(qp)) != NULL; ++(*len), ++cnt) {
802
803         /* expand return array if needed */
804         if (*len >= max) {
805             max += CHUNK;
806             ret = xrealloc(ret, sizeof(struct grp) * max);
807         }
808
809         /* setup the next return element */
810         cur = &ret[*len];
811         cur->ignore = NOT_IGNORED;
812         cur->hostid = hostid;
813         cur->linenum = cnt+1;
814         cur->output = 0;
815         cur->remove = 0;
816         cur->name = NULL;
817         cur->hi = NULL;
818         cur->low = NULL;
819         cur->type = NULL;
820         cur->outhi = NULL;
821         cur->outlow = NULL;
822         cur->outtype = NULL;
823
824         /* obtain a copy of the current line */
825         cur->name = xstrdup(line);
826
827         /* get the group name */
828         if ((p = strchr(cur->name, ' ')) == NULL) {
829             if (!QUIET(hostid))
830                 warn("line %d from %s is malformed, skipping line", cnt + 1,
831                      host);
832
833             /* don't form an entry for this group */
834             --(*len);
835             continue;
836         }
837         *p = '\0';
838         namelen = p - cur->name;
839
840         /* find the other 3 fields, ignore if not found */
841         cur->hi = p+1;
842         if ((p = strchr(p + 1, ' ')) == NULL) {
843             if (!QUIET(hostid))
844                 warn("skipping malformed line %d (field 2) from %s", cnt + 1,
845                      host);
846
847             /* don't form an entry for this group */
848             --(*len);
849             continue;
850         }
851         *p = '\0';
852         cur->low = p+1;
853         if ((p = strchr(p + 1, ' ')) == NULL) {
854             if (!QUIET(hostid))
855                 warn("skipping malformed line %d (field 3) from %s", cnt + 1,
856                      host);
857
858             /* don't form an entry for this group */
859             --(*len);
860             continue;
861         }
862         *p = '\0';
863         cur->type = p+1;
864         if ((p = strchr(p + 1, ' ')) != NULL) {
865             if (!QUIET(hostid))
866                 warn("skipping line %d from %s, it has more than 4 fields",
867                      cnt + 1, host);
868
869             /* don't form an entry for this group */
870             --(*len);
871             continue;
872         }
873
874         /* check for bad group name */
875         if (bad_grpname(cur->name, num_check)) {
876             if (!QUIET(hostid))
877                 warn("line %d <%s> from %s has a bad newsgroup name",
878                      cnt + 1, cur->name, host);
879             cur->ignore |= ERROR_BADNAME;
880             continue;
881         }
882
883         /* check for long name if requested */
884         if (s_flag > 0 && strlen(cur->name) > (size_t)s_flag) {
885             if (!QUIET(hostid))
886                 warn("line %d <%s> from %s has a name that is too long",
887                      cnt + 1, cur->name, host);
888             cur->ignore |= ERROR_BADNAME;
889             continue;
890         }
891
892         /* look for only a bad top level element if the proper -t was given */
893         if (TOP_CHECK(hostid)) {
894
895             /* look for a '.' in the name */
896             if (strcmp(cur->name, "junk") != 0 && 
897                 strcmp(cur->name, "control") != 0 && 
898                 strcmp(cur->name, "to") != 0 && 
899                 strcmp(cur->name, "test") != 0 && 
900                 strcmp(cur->name, "general") != 0 && 
901                 strchr(cur->name, '.') == NULL) {
902                 if (!QUIET(hostid))
903                     warn("line %d <%s> from %s is an invalid top level name",
904                          cnt + 1, cur->name, host);
905                 cur->ignore |= ERROR_BADNAME;
906                 continue;
907             }
908         }
909
910         /* look for *.bork.bork.bork groups if the proper -b was given */
911         if (BORK_CHECK(cur->hostid)) {
912             int elmlen;         /* length of element */
913             char *q;            /* beyond end of element */
914
915             /* scan the name backwards */
916             q = &(cur->name[namelen]);
917             for (p = &(cur->name[namelen-1]); p >= cur->name; --p) {
918                 /* if '.', see if this is a bork element */
919                 if (*p == '.') {
920                     /* see if the bork element is short enough */
921                     elmlen = q-p;
922                     if (3*elmlen <= q-cur->name) {
923                         /* look for a triple match */
924                         if (strncmp(p,p-elmlen,elmlen) == 0 &&
925                             strncmp(p,p-(elmlen*2),elmlen) == 0) {
926                             /* found a *.bork.bork.bork group */
927                             cur->ignore |= CHECK_BORK;
928                             break;
929                         }
930                     }
931                     /* note the end of a new element */
932                     q = p;
933                 }
934             }
935         }
936
937         /* 
938          * check for bad chars in the hi water mark 
939          */
940         for (p=cur->hi, i=0; *p && isascii(*p) && isdigit((int)*p); ++p, ++i) {
941         }
942         if (*p) {
943             if (!QUIET(hostid))
944                 warn("line %d <%s> from %s has non-digits in hi water",
945                      cnt + 1, cur->name, cur->hi);
946             cur->ignore |= ERROR_FORMAT;
947             continue;
948         }
949
950         /*
951          * check for excessive hi water length
952          */
953         if (i > WATER_LEN) {
954             if (!QUIET(hostid))
955                 warn("line %d <%s> from %s hi water len: %d < %d",
956                      cnt + 1, cur->name, cur->hi, i, WATER_LEN);
957             cur->ignore |= ERROR_FORMAT;
958             continue;
959         }
960
961         /*
962          * if the hi water length is too small, malloc and resize
963          */
964         if (i != WATER_LEN) {
965             p = xmalloc(WATER_LEN + 1);
966             memcpy(p, cur->hi, ((i > WATER_LEN) ? WATER_LEN : i)+1);
967         }
968
969         /* 
970          * check for bad chars in the low water mark 
971          */
972         for (p=cur->low, i=0; *p && isascii(*p) && isdigit((int)*p); ++p, ++i) {
973         }
974         if (*p) {
975             if (!QUIET(hostid))
976                 warn("line %d <%s> from %s has non-digits in low water",
977                      cnt + 1, cur->name, cur->low);
978             cur->ignore |= ERROR_FORMAT;
979             continue;
980         }
981
982         /*
983          * check for excessive low water length
984          */
985         if (i > WATER_LEN) {
986             if (!QUIET(hostid))
987                 warn("line %d <%s> from %s low water len: %d < %d",
988                      cnt + 1, cur->name, cur->hi, i, WATER_LEN);
989             cur->ignore |= ERROR_FORMAT;
990             continue;
991         }
992
993         /*
994          * if the low water length is too small, malloc and resize
995          */
996         if (i != WATER_LEN) {
997             p = xmalloc(WATER_LEN + 1);
998             memcpy(p, cur->low, ((i > WATER_LEN) ? WATER_LEN : i)+1);
999         }
1000
1001         /* check for a bad group type */
1002         switch (cur->type[0]) {
1003         case 'y':
1004                 /* of COURSE: collabra has incompatible flags. but it   */
1005                 /* looks like they can be fixed easily enough.          */
1006                 if (cur->type[1] == 'g') {
1007                         cur->type[1] = '\0';
1008                 }
1009         case 'm':
1010         case 'j':
1011         case 'n':
1012         case 'x':
1013             if (cur->type[1] != '\0') {
1014                 if (!QUIET(hostid))
1015                     warn("line %d <%s> from %s has a bad newsgroup type",
1016                          cnt + 1, cur->name, host);
1017                 cur->ignore |= ERROR_BADTYPE;
1018             }
1019             break;
1020         case '=':
1021             if (cur->type[1] == '\0') {
1022                 if (!QUIET(hostid))
1023                     warn("line %d <%s> from %s has an empty =group name",
1024                          cnt + 1, cur->name, host);
1025                 cur->ignore |= ERROR_BADTYPE;
1026             }
1027             break;
1028         default:
1029             if (!QUIET(hostid))
1030                 warn("line %d <%s> from %s has an unknown newsgroup type",
1031                      cnt + 1, cur->name, host);
1032             cur->ignore |= ERROR_BADTYPE;
1033             break;
1034         }
1035         if (cur->ignore & ERROR_BADTYPE) {
1036             continue;
1037         }
1038
1039         /* if an = type, check for bad = name */
1040         if (cur->type[0] == '=' && bad_grpname(&(cur->type[1]), num_check)) {
1041             if (!QUIET(hostid))
1042                 warn("line %d <%s> from %s is equivalenced to a bad name:"
1043                      " <%s>", cnt+1, cur->name, host,
1044                      (cur->type) ? cur->type : "NULL");
1045             cur->ignore |= ERROR_EQNAME;
1046             continue;
1047         }
1048
1049         /* if an = type, check for long = name if requested */
1050         if (cur->type[0] == '=' && s_flag > 0 &&
1051             strlen(&(cur->type[1])) > (size_t)s_flag) {
1052             if (!QUIET(hostid))
1053                 warn("line %d <%s> from %s is equivalenced to a long name:"
1054                      " <%s>", cnt+1, cur->name, host,
1055                      (cur->type) ? cur->type : "NULL");
1056             cur->ignore |= ERROR_EQNAME;
1057             continue;
1058         }
1059
1060         /* count this entry which will be used */
1061         ++ucnt;
1062     }
1063     if (D_BUG)
1064         warn("STATUS: read %d groups, will merge %d groups from %s",
1065              cnt, ucnt, host);
1066
1067     /* count the errors */
1068     *errs = cnt - ucnt;
1069     if (D_BUG)
1070         warn("STATUS: found %d line errors from %s", *errs, host);
1071
1072     /* determine why we stopped */
1073     if (QIOerror(qp))
1074         sysdie("cannot read temp file for %s at line %d", host, cnt);
1075     else if (QIOtoolong(qp))
1076         sysdie("line %d from host %s is too long", cnt, host);
1077
1078     /* all done */
1079     if (is_file) {
1080         QIOclose(qp);
1081     } else {
1082         CAclose();
1083         fprintf(ToServer, "quit\r\n");
1084         fclose(ToServer);
1085         fgets(buff, sizeof buff, FromServer);
1086         fclose(FromServer);
1087     }
1088     return ret;
1089 }
1090
1091 /*
1092  * bad_grpname - test if the string is a valid group name
1093  *
1094  * Newsgroup names must consist of only alphanumeric chars and
1095  * characters from the following regular expression:
1096  *
1097  *      [.+-_]
1098  *
1099  * One cannot have two '.'s in a row.  The first character must be
1100  * alphanumeric.  The character following a '.' must be alphanumeric.
1101  * The name cannot end in a '.' character.
1102  *
1103  * If we are checking for all numeric compnents, (see num_chk) then
1104  * a component cannot be all numeric.  I.e,. there must be a non-numeric
1105  * character in the name, there must be a non-numeric character between
1106  * the start and the first '.', there must be a non-numeric character
1107  * between two '.'s anmd there must be a non-numeric character between
1108  * the last '.' and the end.
1109  *
1110  * given:
1111  *      name    newsgroup name to check
1112  *      num_chk true => all numeric newsgroups components are invalid
1113  *              false => do not check for numeric newsgroups
1114  *
1115  * returns:
1116  *      0       group is ok
1117  *      1       group is bad
1118  */
1119 static int
1120 bad_grpname(name, num_chk)
1121     char *name;                 /* newsgroup name to check */
1122     int num_chk;                /* true => check for numeric newsgroup */
1123 {
1124     char *p;
1125     int non_num;        /* true => found a non-numeric, non-. character */
1126     int level;          /* group levels (.'s) */
1127
1128     /* firewall */
1129     if (name == NULL) {
1130         return 1;
1131     }
1132
1133     /* must start with a alpha numeric ascii character */
1134     if (!isascii(name[0])) {
1135         return 1;
1136     }
1137     /* set non_num as needed */
1138     if (isalpha((int)name[0])) {
1139         non_num = true;
1140     } else if ((int)isdigit((int)name[0])) {
1141         non_num = false;
1142     } else {
1143         return 1;
1144     }
1145
1146     /* scan each char */
1147     level = 0;
1148     for (p=name+1; *p; ++p) {
1149
1150         /* name must contain ASCII chars */
1151         if (!isascii(*p)) {
1152             return 1;
1153         }
1154
1155         /* alpha chars are ok */
1156         if (isalpha((int)*p)) {
1157             non_num = true;
1158             continue;
1159         }
1160
1161         /* numeric chars are ok */
1162         if (isdigit((int)*p)) {
1163             continue;
1164         }
1165
1166         /* +, - and _ are ok */
1167         if (*p == '+' || *p == '-' || *p == '_') {
1168             non_num = true;
1169             continue;
1170         }
1171
1172         /* check for the '.' case */
1173         if (*p == '.') {
1174             /*
1175              * look for groups that are too deep, if requested by -g
1176              */
1177             if (g_flag > 0 && ++level > g_flag) {
1178                 /* we are too deep */
1179                 return 1;
1180             }
1181
1182             /*
1183              * A '.' is ok as long as the next character is alphanumeric.
1184              * This imples that '.' cannot before a previous '.' and
1185              * that it cannot be at the end.
1186              *
1187              * If we are checking for all numeric compnents, then
1188              * '.' is ok if we saw a non-numeric char before the
1189              * last '.', or before the beginning if no previous '.'
1190              * has been seen.
1191              */
1192             if ((!num_chk || non_num) && isascii(*(p+1)) && isalnum((int)*(p+1))) {
1193                 ++p;            /* '.' is ok, and so is the next char */
1194                 if (isdigit((int)*p)) { /* reset non_num as needed */
1195                     non_num = false;
1196                 } else {
1197                     non_num = true;
1198                 }
1199                 continue;
1200             }
1201         }
1202
1203         /* this character must be invalid */
1204         return 1;
1205     }
1206     if (num_chk && !non_num) {
1207         /* last component is all numeric */
1208         return 1;
1209     }
1210
1211     /* the name must be ok */
1212     return 0;
1213 }
1214
1215 /*
1216  * get_ignore - get the ignore list from an ignore file
1217  *
1218  * given:
1219  *      filename        name of the ignore file to read
1220  *      *len            pointer to length of ignore return array
1221  *
1222  * returns:
1223  *      returns a malloced ignore pattern array, changes len
1224  *
1225  * An ignore file is of the form:
1226  *
1227  *      # this is a comment which is ignored
1228  *      # comments begin at the first # character
1229  *      # comments may follow text on the same line
1230  *
1231  *      # blank lines are ignored too
1232  *
1233  *      # lines are [ic] <spaces-tabs> pattern [<spaces-tabs> type] ...
1234  *      i    foo.*              # ignore foo.* groups,
1235  *      c    foo.bar m          # but check foo.bar if moderated
1236  *      c    foo.keep.*         # and check foo.keep.*
1237  *      i    foo.keep.* j =alt.*      # except when foo.keep.* is junked
1238  *                                    #     or equivalenced to an alt.* group
1239  *
1240  * The 'i' value means ignore, 'c' value means 'compare'.   The last pattern
1241  * that matches a group determines the fate of the group.  By default all
1242  * groups are included.
1243  *
1244  * NOTE: Only one '=name' is allowed per line.
1245  *       "=" is considered to be equivalent to "=*".
1246  */
1247 static struct pat *
1248 get_ignore(filename, len)
1249     char *filename;             /* name of the ignore file to read */
1250     int *len;                   /* length of return array */
1251 {
1252     QIOSTATE *qp;               /* QIO ignore file state */
1253     char *line;                 /* the line just read */
1254     struct pat *ret;            /* array of ignore patterns to return */
1255     struct pat *cur;            /* current pattern entry being formed */
1256     int max;                    /* max length (in elements) of ret */
1257     int linenum;                /* current line number */
1258     char *p;
1259     int i;
1260
1261     /* firewall */
1262     if (filename == NULL)
1263         die("internal error #3: filename is NULL");
1264     if (len == NULL)
1265         die("internal error #4: len is NULL");
1266     if (D_BUG)
1267         warn("STATUS: reading ignore file %s", filename);
1268
1269     /* setup return array */
1270     ret = xmalloc(CHUNK * sizeof(struct grp));
1271     max = CHUNK;
1272
1273     /* setup to read the ignore file data quickly */
1274     if ((qp = QIOopen(filename)) == NULL)
1275         sysdie("cannot read ignore file %s", filename);
1276
1277     /* scan server's output, displaying appropriate lines */
1278     *len = 0;
1279     for (linenum = 1; (line = QIOread(qp)) != NULL; ++linenum) {
1280
1281         /* expand return array if needed */
1282         if (*len >= max) {
1283             max += CHUNK;
1284             ret = xrealloc(ret, sizeof(struct pat) * max);
1285         }
1286
1287         /* remove any trailing comments */
1288         p = strchr(line, '#');
1289         if (p != NULL) {
1290             *p = '\0';
1291         }
1292
1293         /* remove any trailing spaces and tabs */
1294         for (p = &line[strlen(line)-1];
1295              p >= line && (*p == ' ' || *p == '\t');
1296              --p) {
1297             *p = '\0';
1298         }
1299
1300         /* ignore line if the remainder of the line is empty */
1301         if (line[0] == '\0') {
1302             continue;
1303         }
1304
1305         /* ensure that the line starts with an i or c token */
1306         if ((line[0] != 'i' && line[0] != 'c') ||
1307             (line[1] != ' ' && line[1] != '\t'))
1308             die("first token is not i or c in line %d of %s", linenum,
1309                 filename);
1310
1311         /* ensure that the second newsgroup pattern token follows */
1312         p = strtok(line+2, " \t");
1313         if (p == NULL)
1314             die("did not find 2nd field in line %d of %s", linenum,
1315                 filename);
1316
1317         /* setup the next return element */
1318         cur = &ret[*len];
1319         cur->pat = NULL;
1320         cur->type_match = 0;
1321         cur->y_type = 0;
1322         cur->m_type = 0;
1323         cur->n_type = 0;
1324         cur->j_type = 0;
1325         cur->x_type = 0;
1326         cur->eq_type = 0;
1327         cur->epat = NULL;
1328         cur->ignore = (line[0] == 'i');
1329
1330         /* obtain a copy of the newsgroup pattern token */
1331         cur->pat = xstrdup(p);
1332
1333         /* process any other type tokens */
1334         for (p=strtok(NULL, " \t"), i=3;
1335              p != NULL;
1336              p=strtok(NULL, " \t"), ++i) {
1337
1338             /* ensure that this next token is a valid type */
1339             switch (p[0]) {
1340             case 'y':
1341             case 'm':
1342             case 'j':
1343             case 'n':
1344             case 'x':
1345                 if (p[1] != '\0') {
1346                     warn("field %d on line %d of %s not a valid type",
1347                          i, linenum, filename);
1348                     die("valid types are a char from [ymnjx=] or =name");
1349                 }
1350                 break;
1351             case '=':
1352                 break;
1353             default:
1354                 warn("field %d on line %d of %s is not a valid type",
1355                      i, linenum, filename);
1356                 die("valid types are a char from [ymnjx=] or =name");
1357             }
1358
1359             /* note that we have a type specific pattern */
1360             cur->type_match = 1;
1361
1362             /* ensure that type is not a duplicate */
1363             if ((p[0] == 'y' && cur->y_type) ||
1364                 (p[0] == 'm' && cur->m_type) ||
1365                 (p[0] == 'n' && cur->n_type) ||
1366                 (p[0] == 'j' && cur->j_type) ||
1367                 (p[0] == 'x' && cur->x_type) ||
1368                 (p[0] == '=' && cur->eq_type)) {
1369                 warn("only one %c type allowed per line", p[0]);
1370                 die("field %d on line %d of %s is a duplicate type",
1371                     i, linenum, filename);
1372             }
1373
1374             /* note what we have seen */
1375             switch (p[0]) {
1376             case 'y':
1377                 cur->y_type = 1;
1378                 break;
1379             case 'm':
1380                 cur->m_type = 1;
1381                 break;
1382             case 'j':
1383                 cur->j_type = 1;
1384                 break;
1385             case 'n':
1386                 cur->n_type = 1;
1387                 break;
1388             case 'x':
1389                 cur->x_type = 1;
1390                 break;
1391             case '=':
1392                 cur->eq_type = 1;
1393                 if (p[0] == '=' && p[1] != '\0')
1394                     cur->epat = xstrdup(p + 1);
1395                 break;
1396             }
1397
1398             /* object if too many fields */
1399             if (i-3 > TYPECNT)
1400                 die("too many fields on line %d of %s", linenum, filename);
1401         }
1402
1403         /* count another pat element */
1404         ++(*len);
1405     }
1406
1407     /* return the pattern array */
1408     return ret;
1409 }
1410
1411 /*
1412  * ignore - ignore newsgroups given an ignore list
1413  *
1414  * given:
1415  *      grp     array of groups
1416  *      grplen  length of grp array in elements
1417  *      igcl    array of ignore
1418  *      iglen   length of igcl array in elements
1419  */
1420 static void
1421 ignore(grp, grplen, igcl, iglen)
1422     struct grp *grp;            /* array of groups */
1423     int grplen;                 /* length of grp array in elements */
1424     struct pat *igcl;           /* array of ignore patterns */
1425     int iglen;                  /* length of igcl array in elements */
1426 {
1427     struct grp *gp;             /* current group element being examined */
1428     struct pat *pp;             /* current pattern element being examined */
1429     int g;                      /* current group index number */
1430     int p;                      /* current pattern index number */
1431     int ign;                    /* 1 => ignore this group, 0 => check it */
1432     int icnt;                   /* groups ignored */
1433     int ccnt;                   /* groups to be checked */
1434
1435     /* firewall */
1436     if (grp == NULL)
1437         die("internal error #5: grp is NULL");
1438     if (igcl == NULL)
1439         die("internal error $6: igcl is NULL");
1440     if (D_BUG)
1441         warn("STATUS: determining which groups to ignore");
1442
1443     /* if nothing to do, return quickly */
1444     if (grplen <= 0 || iglen <= 0) {
1445         return;
1446     }
1447
1448     /* examine each group */
1449     icnt = 0;
1450     ccnt = 0;
1451     for (g=0; g < grplen; ++g) {
1452
1453         /* check the group to examine */
1454         gp = &grp[g];
1455         if (gp->ignore) {
1456             /* already ignored no need to examine */
1457             continue;
1458         }
1459
1460         /* check group against all patterns */
1461         ign = 0;
1462         for (p=0, pp=igcl; p < iglen; ++p, ++pp) {
1463
1464             /* if pattern has a specific type, check it first */
1465             if (pp->type_match) {
1466
1467                 /* specific type required, check for match */
1468                 switch (gp->type[0]) {
1469                 case 'y':
1470                     if (! pp->y_type) continue;  /* pattern does not apply */
1471                     break;
1472                 case 'm':
1473                     if (! pp->m_type) continue;  /* pattern does not apply */
1474                     break;
1475                 case 'n':
1476                     if (! pp->n_type) continue;  /* pattern does not apply */
1477                     break;
1478                 case 'j':
1479                     if (! pp->j_type) continue;  /* pattern does not apply */
1480                     break;
1481                 case 'x':
1482                     if (! pp->x_type) continue;  /* pattern does not apply */
1483                     break;
1484                 case '=':
1485                     if (! pp->eq_type) continue;  /* pattern does not apply */
1486                     if (pp->epat != NULL && !uwildmat(&gp->type[1], pp->epat)) {
1487                         /* equiv pattern doesn't match, patt does not apply */
1488                         continue;
1489                     }
1490                     break;
1491                 }
1492             }
1493
1494             /* perform a match on group name */
1495             if (uwildmat(gp->name, pp->pat)) {
1496                 /* this pattern fully matches, use the ignore value */
1497                 ign = pp->ignore;
1498             }
1499         }
1500
1501         /* if this group is to be ignored, note it */
1502         if (ign) {
1503             switch (gp->hostid) {
1504             case HOSTID1:
1505                 if (ign_host1_flag) {
1506                     gp->ignore |= CHECK_IGNORE;
1507                     ++icnt;
1508                 }
1509                 break;
1510             case HOSTID2:
1511                 if (ign_host2_flag) {
1512                     gp->ignore |= CHECK_IGNORE;
1513                     ++icnt;
1514                 }
1515                 break;
1516             default:
1517                 die("newsgroup %s bad hostid: %d", gp->name, gp->hostid);
1518             }
1519         } else {
1520             ++ccnt;
1521         }
1522     }
1523     if (D_BUG)
1524         warn("STATUS: examined %d groups: %d ignored, %d to be checked",
1525              grplen, icnt, ccnt);
1526 }
1527
1528 /*
1529  * merge_cmp - qsort compare function for later group merge
1530  *
1531  * given:
1532  *      a       group a to compare
1533  *      b       group b to compare
1534  *
1535  * returns:
1536  *      >0      a > b
1537  *      0       a == b elements match (fatal error if a and b are different)
1538  *      <0      a < b
1539  *
1540  * To speed up group comparison, we compare by the following items listed
1541  * in order of sorting:
1542  *
1543  *      group name
1544  *      hostid                  (host1 ahead of host2)
1545  *      linenum                 (active file line number)
1546  */
1547 static int
1548 merge_cmp(arg_a, arg_b)
1549     const void *arg_a;          /* first qsort compare arg */
1550     const void *arg_b;          /* first qsort compare arg */
1551 {
1552     const struct grp *a = arg_a;        /* group a to compare */
1553     const struct grp *b = arg_b;        /* group b to compare */
1554     int i;
1555
1556     /* firewall */
1557     if (a == b) {
1558         /* we guess this could happen */
1559         return(0);
1560     }
1561
1562     /* compare group names */
1563     i = strcmp(a->name, b->name);
1564     if (i != 0) {
1565         return i;
1566     }
1567
1568     /* compare hostid's */
1569     if (a->hostid != b->hostid) {
1570         if (a->hostid > b->hostid) {
1571             return 1;
1572         } else {
1573             return -1;
1574         }
1575     }
1576
1577     /* compare active line numbers */
1578     if (a->linenum != b->linenum) {
1579         if (a->linenum > b->linenum) {
1580             return 1;
1581         } else {
1582             return -1;
1583         }
1584     }
1585
1586     /* two different elements match, this should not happen! */
1587     die("two internal grp elements match!");
1588     /*NOTREACHED*/
1589 }
1590
1591 /*
1592  * merge_grps - compare groups from both hosts
1593  *
1594  * given:
1595  *      grp     array of groups
1596  *      grplen  length of grp array in elements
1597  *      host1   name of host with HOSTID1
1598  *      host2   name of host with HOSTID2
1599  *
1600  * This routine will select which groups to output form a merged active file.
1601  */
1602 static void
1603 merge_grps(grp, grplen, host1, host2)
1604     struct grp *grp;            /* array of groups */
1605     int grplen;                 /* length of grp array in elements */
1606     char *host1;                /* name of host with HOSTID1 */
1607     char *host2;                /* name of host with HOSTID2 */
1608 {
1609     int cur;            /* current group index being examined */
1610     int nxt;            /* next group index being examined */
1611     int outcnt;         /* groups to output */
1612     int rmcnt;          /* groups to remove */
1613     int h1_probs;       /* =type problem groups from host1 */
1614     int h2_probs;       /* =type problem groups from host2 */
1615
1616     /* firewall */
1617     if (grp == NULL)
1618         die("internal error #7: grp is NULL");
1619
1620     /* sort groups for the merge */
1621     if (D_BUG)
1622         warn("STATUS: sorting groups");
1623     qsort((char *)grp, grplen, sizeof(grp[0]), merge_cmp);
1624
1625     /* mark =type problem groups from host2, if needed */
1626     h2_probs = mark_eq_probs(grp, grplen, l_host2_flag, host1, host2);
1627
1628     /*
1629      * We will walk thru the sorted group array, looking for pairs
1630      * among the groups that we have not already ignored.
1631      *
1632      * If a host has duplicate groups, then the duplicates will
1633      * be next to each other.
1634      *
1635      * If both hosts have the name group, they will be next to each other.
1636      */
1637     if (D_BUG)
1638         warn("STATUS: merging groups");
1639     outcnt = 0;
1640     rmcnt = 0;
1641     for (cur=0; cur < grplen; cur=nxt) {
1642
1643         /* determine the next group index */
1644         nxt = cur+1;
1645
1646         /* skip if this group is ignored */
1647         if (grp[cur].ignore) {
1648             continue;
1649         }
1650         /* assert: cur is not ignored */
1651
1652         /* check for duplicate groups from the same host */
1653         while (nxt < grplen) {
1654
1655             /* mark the later as a duplicate */
1656             if (grp[cur].hostid == grp[nxt].hostid &&
1657                 strcmp(grp[cur].name, grp[nxt].name) == 0) {
1658                 grp[nxt].ignore |= ERROR_DUP;
1659                 if (!QUIET(grp[cur].hostid))
1660                     warn("lines %d and %d from %s refer to the same group",
1661                          grp[cur].linenum, grp[nxt].linenum,
1662                          ((grp[cur].hostid == HOSTID1) ? host1 : host2));
1663                 ++nxt;
1664             } else {
1665                 break;
1666             }
1667         }
1668         /* assert: cur is not ignored */
1669         /* assert: cur & nxt are not the same group from the same host */
1670
1671         /* if nxt is ignored, look for the next non-ignored group */
1672         while (nxt < grplen && grp[nxt].ignore) {
1673             ++nxt;
1674         }
1675         /* assert: cur is not ignored */
1676         /* assert: nxt is not ignored or is beyond end */
1677         /* assert: cur & nxt are not the same group from the same host */
1678
1679         /* case: cur and nxt are the same group */
1680         if (nxt < grplen && strcmp(grp[cur].name, grp[nxt].name) == 0) {
1681
1682             /* assert: cur is HOSTID1 */
1683             if (grp[cur].hostid != HOSTID1)
1684                 die("internal error #8: grp[%d].hostid: %d != %d",
1685                     cur, grp[cur].hostid, HOSTID1);
1686
1687             /*
1688              * Both hosts have the same group.  Make host1 group type
1689              * match host2.  (it may already)
1690              */
1691             grp[cur].output = 1;
1692             grp[cur].outhi = (host2_hilow_all ? grp[nxt].hi : grp[cur].hi);
1693             grp[cur].outlow = (host2_hilow_all ? grp[nxt].low : grp[cur].low);
1694             grp[cur].outtype = grp[nxt].type;
1695             ++outcnt;
1696
1697             /* do not process nxt, skip to the one beyond */
1698             ++nxt;
1699
1700         /* case: cur and nxt are different groups */
1701         } else {
1702
1703             /*
1704              * if cur is host2, then host1 doesn't have it, so output it
1705              */
1706             if (grp[cur].hostid == HOSTID2) {
1707                 grp[cur].output = 1;
1708                 grp[cur].outhi = (host2_hilow_newgrp ? grp[cur].hi : DEF_HI);
1709                 grp[cur].outlow = (host2_hilow_newgrp ? grp[cur].low : DEF_LOW);
1710                 grp[cur].outtype = grp[cur].type;
1711                 ++outcnt;
1712
1713             /*
1714              * If cur is host1, then host2 doesn't have it.
1715              * Mark for removal if -m was not given.
1716              */
1717             } else {
1718                 grp[cur].output = 1;
1719                 grp[cur].outhi = grp[cur].hi;
1720                 grp[cur].outlow = grp[cur].low;
1721                 grp[cur].outtype = grp[cur].type;
1722                 if (! m_flag) {
1723                     grp[cur].remove = 1;
1724                     ++rmcnt;
1725                 }
1726             }
1727
1728             /* if no more groups to examine, we are done */
1729             if (nxt >= grplen) {
1730                 break;
1731             }
1732         }
1733     }
1734
1735     /* mark =type problem groups from host1, if needed */
1736     h1_probs = mark_eq_probs(grp, grplen, l_host1_flag, host1, host2);
1737
1738     /* all done */
1739     if (D_BUG) {
1740         warn("STATUS: sort-merge passed thru %d groups", outcnt);
1741         warn("STATUS: sort-merge marked %d groups for removal", rmcnt);
1742         warn("STATUS: marked %d =type error groups from host1", h1_probs);
1743         warn("STATUS: marked %d =type error groups from host2", h2_probs);
1744     }
1745     return;
1746 }
1747
1748 /*
1749  * active_cmp - qsort compare function for active file style output
1750  *
1751  * given:
1752  *      a       group a to compare
1753  *      b       group b to compare
1754  *
1755  * returns:
1756  *      >0      a > b
1757  *      0       a == b elements match (fatal error if a and b are different)
1758  *      <0      a < b
1759  *
1760  * This sort will sort groups so that the lines that will we output
1761  * host1 lines followed by host2 lines.  Thus, we will sort by
1762  * the following keys:
1763  *
1764  *      hostid                  (host1 ahead of host2)
1765  *      linenum                 (active file line number)
1766  */
1767 static int
1768 active_cmp(arg_a, arg_b)
1769     const void *arg_a;          /* first qsort compare arg */
1770     const void *arg_b;          /* first qsort compare arg */
1771 {
1772     const struct grp *a = arg_a;        /* group a to compare */
1773     const struct grp *b = arg_b;        /* group b to compare */
1774
1775     /* firewall */
1776     if (a == b) {
1777         /* we guess this could happen */
1778         return(0);
1779     }
1780
1781     /* compare hostid's */
1782     if (a->hostid != b->hostid) {
1783         if (a->hostid > b->hostid) {
1784             return 1;
1785         } else {
1786             return -1;
1787         }
1788     }
1789
1790     /* compare active line numbers */
1791     if (a->linenum != b->linenum) {
1792         if (a->linenum > b->linenum) {
1793             return 1;
1794         } else {
1795             return -1;
1796         }
1797     }
1798
1799     /* two different elements match, this should not happen! */
1800     die("two internal grp elements match!");
1801     /*NOTREACHED*/
1802 }
1803
1804 /*
1805  * output_grps - output the result of the merge
1806  *
1807  * given:
1808  *      grp     array of groups
1809  *      grplen  length of grp array in elements
1810  */
1811 static void
1812 output_grps(grp, grplen)
1813     struct grp *grp;            /* array of groups */
1814     int grplen;                 /* length of grp array in elements */
1815 {
1816     int add;            /* number of groups added */
1817     int change;         /* number of groups changed */
1818     int remove;         /* number of groups removed */
1819     int no_new_dir;     /* number of new groups with missing/empty dirs */
1820     int new_dir;        /* number of new groupsm, non-empty dir no water chg */
1821     int water_change;   /* number of new groups where hi&low water changed */
1822     int work;           /* adds + changes + removals */
1823     int same;           /* the number of groups the same */
1824     int ignore;         /* host1 newsgroups to ignore */
1825     int not_done;       /* exec errors and execs not performed */
1826     int rm_cycle;       /* 1 => removals only, 0 => adds & changes only */
1827     int sleep_msg;      /* 1 => -o x sleep message was given */
1828     int top_ignore;     /* number of groups ignored because of no top level */
1829     int restore;        /* host1 groups restored due to -o a1 */
1830     double host1_same;  /* % of host1 that is the same */
1831     int i;
1832
1833     /* firewall */
1834     if (grp == NULL)
1835         die("internal error #9: grp is NULL");
1836
1837     /*
1838      * If -a1 was given, mark for output any host1 newsgroup that was
1839      * simply ignored due to the -i ign_file.
1840      */
1841     if (host1_ign_print) {
1842         restore = 0;
1843         for (i=0; i < grplen; ++i) {
1844             if (grp[i].hostid == HOSTID1 && 
1845                 (grp[i].ignore == CHECK_IGNORE ||
1846                  grp[i].ignore == CHECK_TYPE ||
1847                  grp[i].ignore == (CHECK_IGNORE|CHECK_TYPE))) {
1848                 /* force group to output and not be ignored */
1849                 grp[i].ignore = 0;
1850                 grp[i].output = 1;
1851                 grp[i].remove = 0;
1852                 grp[i].outhi = grp[i].hi;
1853                 grp[i].outlow = grp[i].low;
1854                 grp[i].outtype = grp[i].type;
1855                 ++restore;
1856             }
1857         }
1858         if (D_BUG)
1859             warn("STATUS: restored %d host1 groups", restore);
1860     }
1861
1862     /*
1863      * If -T, ignore new top level groups from host2
1864      */
1865     if (no_new_hier) {
1866         top_ignore = 0;
1867         for (i=0; i < grplen; ++i) {
1868             /* look at new newsgroups */
1869             if (grp[i].hostid == HOSTID2 &&
1870                 grp[i].output != 0 &&
1871                 new_top_hier(grp[i].name)) {
1872                  /* no top level ignore this new group */
1873                  grp[i].ignore |= CHECK_HIER;
1874                  grp[i].output = 0;
1875                  if (D_BUG)
1876                      warn("ignore new newsgroup: %s, new hierarchy",
1877                           grp[i].name);
1878                  ++top_ignore;
1879             }
1880         }
1881         if (D_SUMMARY)
1882             warn("STATUS: ignored %d new newsgroups due to new hierarchy",
1883                  top_ignore);
1884     }
1885
1886     /* sort by active file order if active style output (-a) */
1887     if (o_flag == OUTPUT_ACTIVE) {
1888         if (D_BUG)
1889             warn("STATUS: sorting groups in output order");
1890         qsort((char *)grp, grplen, sizeof(grp[0]), active_cmp);
1891     }
1892
1893     /*
1894      * Determine the % of lines from host1 active file that remain unchanged
1895      * ignoring any low/high water mark changes.
1896      *
1897      * Determine the number of old groups that will remain the same
1898      * the number of new groups that will be added.
1899      */
1900     add = 0;
1901     change = 0;
1902     remove = 0;
1903     same = 0;
1904     ignore = 0;
1905     no_new_dir = 0;
1906     new_dir = 0;
1907     water_change = 0;
1908     for (i=0; i < grplen; ++i) {
1909         /* skip non-output ...  */
1910         if (grp[i].output == 0) {
1911             if (grp[i].hostid == HOSTID1) {
1912                 ++ignore;
1913             }
1914             continue;
1915
1916         /* case: group needs removal */
1917         } else if (grp[i].remove) {
1918             ++remove;
1919
1920         /* case: group is from host2, so we need a newgroup */
1921         } else if (grp[i].hostid == HOSTID2) {
1922             ++add;
1923
1924         /* case: group is from host1, but the type changed */
1925         } else if (grp[i].type != grp[i].outtype &&
1926                    strcmp(grp[i].type,grp[i].outtype) != 0) {
1927             ++change;
1928
1929         /* case: group did not change */
1930         } else {
1931             ++same;
1932         }
1933     }
1934     work = add+change+remove;
1935     if (same+work+host1_errs <= 0) {
1936         /* no lines, no work, no errors == nothing changed == 100% the same */
1937         host1_same = (double)100.0;
1938     } else {
1939         /* calculate % unchanged */
1940         host1_same = (double)100.0 *
1941                      ((double)same / (double)(same+work+host1_errs));
1942     }
1943     if (D_BUG) {
1944         warn("STATUS: same=%d add=%d, change=%d, remove=%d",
1945              same, add, change, remove);
1946         warn("STATUS: ignore=%d, work=%d, err=%d",
1947              ignore, work, host1_errs);
1948         warn("STATUS: same+work+err=%d, host1_same=%.2f%%",
1949              same+work+host1_errs, host1_same);
1950     }
1951
1952     /* 
1953      * Bail out if we too few lines in host1 active file (ignoring
1954      * low/high water mark changes) remaining unchanged.
1955      *
1956      * We define change as:
1957      *
1958      *  line errors from host1 active file
1959      *  newsgroups to be added to host1
1960      *  newsgroups to be removed from host1
1961      *  newsgroups to be change in host1
1962      */
1963     if (host1_same < p_flag) {
1964         warn("HALT: lines unchanged: %.2f%% < min change limit: %.2f%%",
1965              host1_same, p_flag);
1966         warn("    No output or commands executed.  Determine if the degree");
1967         warn("    of changes is okay and re-execute with a lower -p value");
1968         die("    or with the problem fixed.");
1969     }
1970
1971     /*
1972      * look at all groups
1973      *
1974      * If we are not producing active file output, we must do removals
1975      * before we do any adds and changes.
1976      *
1977      * We recalculate the work stats in finer detail as well as noting how
1978      * many actions were successful.
1979      */
1980     add = 0;
1981     change = 0;
1982     remove = 0;
1983     same = 0;
1984     ignore = 0;
1985     work = 0;
1986     not_done = 0;
1987     sleep_msg = 0;
1988     rm_cycle = ((o_flag == OUTPUT_ACTIVE) ? 0 : 1);
1989     do {
1990         for (i=0; i < grplen; ++i) {
1991
1992             /* if -o Ax, output ignored non-error groups too */
1993
1994             /*
1995              * skip non-output ...
1996              *
1997              * but if '-a' and active output mode, then don't skip ignored,
1998              * non-error, non-removed groups from host1
1999              */
2000             if (grp[i].output == 0) {
2001                 if (grp[i].hostid == HOSTID1) {
2002                     ++ignore;
2003                 }
2004                 continue;
2005             }
2006
2007             /* case: output active lines */
2008             if (o_flag == OUTPUT_ACTIVE) {
2009
2010                 /* case: group needs removal */
2011                 if (grp[i].remove) {
2012                     ++remove;
2013                     ++work;
2014
2015                 /* case: group will be kept */
2016                 } else {
2017
2018                     /* output in active file format */
2019                     printf("%s %s %s %s\n",
2020                         grp[i].name,  grp[i].outhi, grp[i].outlow,
2021                         grp[i].outtype);
2022
2023                     /* if -v level is high enough, do group accounting */
2024                     if (D_IF_SUMM) {
2025
2026                         /* case: group is from host2, so we need a newgroup */
2027                         if (grp[i].hostid == HOSTID2) {
2028                             ++add;
2029                             ++work;
2030
2031                         /* case: group is from host1, but the type changed */
2032                         } else if (grp[i].type != grp[i].outtype &&
2033                                    strcmp(grp[i].type,grp[i].outtype) != 0) {
2034                             ++change;
2035                             ++work;
2036
2037                         /* case: group did not change */
2038                         } else {
2039                             ++same;
2040                         }
2041                     }
2042                 }
2043
2044             /* case: output ctlinnd commands */
2045             } else if (o_flag == OUTPUT_CTLINND) {
2046
2047                 /* case: group needs removal */
2048                 if (grp[i].remove) {
2049
2050                     /* output rmgroup */
2051                     if (rm_cycle) {
2052                         printf("ctlinnd rmgroup %s\n", grp[i].name);
2053                         ++remove;
2054                         ++work;
2055                     }
2056
2057                 /* case: group is from host2, so we need a newgroup */
2058                 } else if (grp[i].hostid == HOSTID2) {
2059
2060                     /* output newgroup */
2061                     if (! rm_cycle) {
2062                         printf("ctlinnd newgroup %s %s %s\n",
2063                             grp[i].name, grp[i].outtype, new_name);
2064                         ++add;
2065                         ++work;
2066                     }
2067
2068                 /* case: group is from host1, but the type changed */
2069                 } else if (grp[i].type != grp[i].outtype &&
2070                            strcmp(grp[i].type,grp[i].outtype) != 0) {
2071
2072                     /* output changegroup */
2073                     if (! rm_cycle) {
2074                         printf("ctlinnd changegroup %s %s\n",
2075                             grp[i].name, grp[i].outtype);
2076                         ++change;
2077                         ++work;
2078                     }
2079
2080                 /* case: group did not change */
2081                 } else {
2082                     if (! rm_cycle) {
2083                         ++same;
2084                     }
2085                 }
2086
2087             /* case: exec ctlinnd commands */
2088             } else if (o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) {
2089
2090                 /* warn about sleeping if needed and first time */
2091                 if (o_flag == OUTPUT_EXEC && z_flag > 0 && sleep_msg == 0) {
2092                     if (D_SUMMARY)
2093                         warn("will sleep %d seconds before each fork/exec",
2094                              z_flag);
2095                     sleep_msg = 1;
2096                 }
2097
2098                 /* case: group needs removal */
2099                 if (grp[i].remove) {
2100
2101                     /* exec rmgroup */
2102                     if (rm_cycle) {
2103                         if (D_REPORT && o_flag == OUTPUT_EXEC)
2104                             warn("rmgroup %s", grp[i].name);
2105                         if (! exec_cmd(o_flag, "rmgroup",
2106                             grp[i].name, NULL, NULL)) {
2107                             ++not_done;
2108                         } else {
2109                             ++remove;
2110                             ++work;
2111                         }
2112                     }
2113
2114                 /* case: group is from host2, so we need a newgroup */
2115                 } else if (grp[i].hostid == HOSTID2) {
2116
2117                     /* exec newgroup */
2118                     if (!rm_cycle) {
2119                         if (D_REPORT && o_flag == OUTPUT_EXEC)
2120                             warn("newgroup %s %s %s",
2121                                  grp[i].name, grp[i].outtype, new_name);
2122                         if (! exec_cmd(o_flag, "newgroup", grp[i].name,
2123                                  grp[i].outtype, new_name)) {
2124                             ++not_done;
2125                         } else {
2126                             ++add;
2127                             ++work;
2128                         }
2129                     }
2130
2131                 /* case: group is from host1, but the type changed */
2132                 } else if (grp[i].type != grp[i].outtype &&
2133                            strcmp(grp[i].type,grp[i].outtype) != 0) {
2134
2135                     /* exec changegroup */
2136                     if (!rm_cycle) {
2137                         if (D_REPORT && o_flag == OUTPUT_EXEC)
2138                             warn("changegroup %s %s",
2139                                  grp[i].name, grp[i].outtype);
2140                         if (! exec_cmd(o_flag, "changegroup", grp[i].name,
2141                                  grp[i].outtype, NULL)) {
2142                             ++not_done;
2143                         } else {
2144                             ++change;
2145                             ++work;
2146                         }
2147                     }
2148
2149                 /* case: group did not change */
2150                 } else {
2151                     if (! rm_cycle) {
2152                         ++same;
2153                     }
2154                 }
2155             }
2156         }
2157     } while (--rm_cycle >= 0);
2158
2159     /* final accounting, if -v */
2160     if (D_SUMMARY || (D_IF_SUMM && (work > 0 || not_done > 0))) {
2161         warn("STATUS: %d group(s)", add+remove+change+same);
2162         warn("STATUS: %d group(s)%s added", add,
2163              ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2164               "" : " to be"));
2165         warn("STATUS: %d group(s)%s removed",   remove,
2166              ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2167               "" : " to be"));
2168         warn("STATUS: %d group(s)%s changed", change,
2169              ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2170               "" : " to be"));
2171         warn("STATUS: %d group(s) %s the same", same,
2172              ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2173               "remain" : "are"));
2174         warn("STATUS: %.2f%% of lines unchanged", host1_same);
2175         warn("STATUS: %d group(s) ignored", ignore);
2176         if (o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC)
2177             warn("STATUS: %d exec(s) not performed", not_done);
2178     }
2179 }
2180
2181 /*
2182  * error_mark - mark for removal, error groups from a given host
2183  *
2184  * given:
2185  *      grp     array of groups
2186  *      grplen  length of grp array in elements
2187  *      hostid  host to mark error groups for removal
2188  */
2189 static void
2190 error_mark(grp, grplen, hostid)
2191     struct grp *grp;            /* array of groups */
2192     int grplen;                 /* length of grp array in elements */
2193     int hostid;                 /* host to mark error groups for removal */
2194 {
2195     int i;
2196     int errcnt;
2197
2198     /* firewall */
2199     if (grp == NULL)
2200         die("internal error #11: grp is NULL");
2201
2202     /* loop thru groups, looking for error groups from a given host */
2203     errcnt = 0;
2204     for (i=0; i < grplen; ++i) {
2205
2206         /* skip if not from hostid */
2207         if (grp[i].hostid != hostid) {
2208             continue;
2209         }
2210
2211         /* mark for removal if an error group not already removed */
2212         if (IS_ERROR(grp[i].ignore)) {
2213
2214             /* mark for removal */
2215             if (grp[i].output != 1 || grp[i].remove != 1) {
2216                 grp[i].output = 1;
2217                 grp[i].remove = 1;
2218             }
2219             ++errcnt;
2220         }
2221     }
2222
2223     /* all done */
2224     if (D_SUMMARY || (D_IF_SUMM && errcnt > 0))
2225         warn("STATUS: marked %d error groups for removal", errcnt);
2226     return;
2227 }
2228
2229 /*
2230  * eq_merge_cmp - qsort compare function for =type group processing
2231  *
2232  * given:
2233  *      a       =group a to compare
2234  *      b       =group b to compare
2235  *
2236  * returns:
2237  *      >0      a > b
2238  *      0       a == b elements match (fatal error if a and b are different)
2239  *      <0      a < b
2240  *
2241  * To speed up group comparison, we compare by the following items listed
2242  * in order of sorting:
2243  *
2244  *      skip                    (non-skipped groups after skipped ones)
2245  *      group equiv name
2246  *      group name
2247  *      hostid                  (host1 ahead of host2)
2248  *      linenum                 (active file line number)
2249  */
2250 static int
2251 eq_merge_cmp(arg_a, arg_b)
2252     const void *arg_a;          /* first qsort compare arg */
2253     const void *arg_b;          /* first qsort compare arg */
2254 {
2255     const struct eqgrp *a = arg_a;      /* group a to compare */
2256     const struct eqgrp *b = arg_b;      /* group b to compare */
2257     int i;
2258
2259     /* firewall */
2260     if (a == b) {
2261         /* we guess this could happen */
2262         return(0);
2263     }
2264
2265     /* compare skip values */
2266     if (a->skip != b->skip) {
2267         if (a->skip > b->skip) {
2268             /* a is skipped, b is not */
2269             return 1;
2270         } else {
2271             /* b is skipped, a is not */
2272             return -1;
2273         }
2274     }
2275
2276     /* compare the names the groups are equivalenced to */
2277     i = strcmp(a->eq, b->eq);
2278     if (i != 0) {
2279         return i;
2280     }
2281
2282     /* compare the group names themselves */
2283     i = strcmp(a->g->name, b->g->name);
2284     if (i != 0) {
2285         return i;
2286     }
2287
2288     /* compare hostid's */
2289     if (a->g->hostid != b->g->hostid) {
2290         if (a->g->hostid > b->g->hostid) {
2291             return 1;
2292         } else {
2293             return -1;
2294         }
2295     }
2296
2297     /* compare active line numbers */
2298     if (a->g->linenum != b->g->linenum) {
2299         if (a->g->linenum > b->g->linenum) {
2300             return 1;
2301         } else {
2302             return -1;
2303         }
2304     }
2305
2306     /* two different elements match, this should not happen! */
2307     die("two internal eqgrp elements match!");
2308 }
2309
2310 /*
2311  * mark_eq_probs - mark =type groups from a given host that have problems
2312  *
2313  * given:
2314  *      grp      sorted array of groups
2315  *      grplen   length of grp array in elements
2316  *      hostid   host to mark error groups for removal, or NOHOST
2317  *      host1   name of host with HOSTID1
2318  *      host2   name of host with HOSTID2
2319  *
2320  * This function assumes that the grp array has been sorted by name.
2321  */
2322 static int
2323 mark_eq_probs(grp, grplen, hostid, host1, host2)
2324     struct grp *grp;            /* array of groups */
2325     int grplen;                 /* length of grp array in elements */
2326     int hostid;                 /* host to mark error groups for removal */
2327     char *host1;                /* name of host with HOSTID1 */
2328     char *host2;                /* name of host with HOSTID2 */
2329 {
2330     struct eqgrp *eqgrp;        /* =type pointer array */
2331     int eq_cnt;                 /* number of =type groups from host */
2332     int new_eq_cnt;             /* number of =type groups remaining */
2333     int missing;                /* =type groups equiv to missing groups */
2334     int cycled;                 /* =type groups equiv to themselves */
2335     int chained;                /* =type groups in long chain or loop */
2336     int cmp;                    /* strcmp of two names */
2337     int step;                   /* equiv loop step */
2338     int i;
2339     int j;
2340
2341     /* firewall */
2342     if (grp == NULL)
2343         die("internal error #12: grp is NULL");
2344     if (hostid == NOHOST) {
2345         /* nothing to detect, nothing else to do */
2346         return 0;
2347     }
2348
2349     /* count the =type groups from hostid that are not in error */
2350     eq_cnt = 0;
2351     for (i=0; i < grplen; ++i) {
2352         if (grp[i].hostid == hostid &&
2353             ! IS_ERROR(grp[i].ignore) &&
2354             grp[i].type != NULL &&
2355             grp[i].type[0] == '=') {
2356             ++eq_cnt;
2357         }
2358     }
2359     if (D_BUG && hostid != NOHOST)
2360         warn("STATUS: host%d has %d =type groups", hostid, eq_cnt);
2361
2362     /* if no groups, then there is nothing to do */
2363     if (eq_cnt == 0) {
2364         return 0;
2365     }
2366
2367     /* setup the =group record array */
2368     eqgrp = xmalloc(eq_cnt * sizeof(eqgrp[0]));
2369     for (i=0, j=0; i < grplen && j < eq_cnt; ++i) {
2370         if (grp[i].hostid == hostid &&
2371             ! IS_ERROR(grp[i].ignore) &&
2372             grp[i].type != NULL &&
2373             grp[i].type[0] == '=') {
2374
2375             /* initialize record */
2376             eqgrp[j].skip = 0;
2377             eqgrp[j].g = &grp[i];
2378             eqgrp[j].eq = &(grp[i].type[1]);
2379             ++j;
2380         }
2381     }
2382
2383     /*
2384      * try to resolve =type groups in at least EQ_LOOP equiv links
2385      */
2386     new_eq_cnt = eq_cnt;
2387     missing = 0;
2388     cycled = 0;
2389     for (step=0; step < EQ_LOOP && new_eq_cnt >= 0; ++step) {
2390
2391         /* sort the =group record array */
2392         qsort((char *)eqgrp, eq_cnt, sizeof(eqgrp[0]), eq_merge_cmp);
2393
2394         /* look for the groups to which =type group point at */
2395         eq_cnt = new_eq_cnt;
2396         for (i=0, j=0; i < grplen && j < eq_cnt; ++i) {
2397
2398             /* we will skip any group in error or from the wrong host */
2399             if (grp[i].hostid != hostid || IS_ERROR(grp[i].ignore)) {
2400                 continue;
2401             }
2402
2403             /* we will skip any skipped eqgrp's */
2404             if (eqgrp[j].skip) {
2405                 /* try the same group against the next eqgrp */
2406                 --i;
2407                 ++j;
2408                 continue;
2409             }
2410
2411             /* compare the =name of the eqgrp with the name of the grp */
2412             cmp = strcmp(grp[i].name, eqgrp[j].eq);
2413
2414             /* case: this group is pointed at by an eqgrp */
2415             if (cmp == 0) {
2416
2417                  /* see if we have looped around to the original group name */
2418                  if (strcmp(grp[i].name, eqgrp[j].g->name) == 0) {
2419
2420                     /* note the detected loop */
2421                     if (! QUIET(hostid))
2422                         warn("%s from %s line %d =loops around to itself",
2423                              eqgrp[j].g->name,
2424                              ((eqgrp[j].g->hostid == HOSTID1) ? host1 : host2),
2425                              eqgrp[j].g->linenum);
2426                      eqgrp[j].g->ignore |= ERROR_EQLOOP;
2427
2428                     /* the =group is bad, so we don't need to bother with it */
2429                     eqgrp[j].skip = 1;
2430                     --new_eq_cnt;
2431                     ++cycled;
2432                     --i;
2433                     ++j;
2434                     continue;
2435                 }
2436
2437                 /* if =group refers to a valid group, we are done with it */
2438                 if (grp[i].type != NULL && grp[i].type[0] != '=') {
2439                     eqgrp[j].skip = 1;
2440                     --new_eq_cnt;
2441                 /* otherwise note the equiv name */
2442                 } else {
2443                     eqgrp[j].eq = &(grp[i].type[1]);
2444                 }
2445                 --i;
2446                 ++j;
2447
2448             /* case: we missed the =name */
2449             } else if (cmp > 0) {
2450
2451                 /* mark the eqgrp in error */
2452                 eqgrp[j].g->ignore |= ERROR_NONEQ;
2453                 if (! QUIET(hostid))
2454                     warn("%s from %s line %d not equiv to a valid group",
2455                          eqgrp[j].g->name,
2456                          ((eqgrp[j].g->hostid == HOSTID1) ? host1 : host2),
2457                          eqgrp[j].g->linenum);
2458
2459                 /* =group is bad, so we don't need to bother with it anymore */
2460                 eqgrp[j].skip = 1;
2461                 --new_eq_cnt;
2462                 ++missing;
2463                 ++j;
2464             }
2465         }
2466
2467         /* any remaining non-skipped eqgrps are bad */
2468         while (j < eq_cnt) {
2469
2470             /* mark the eqgrp in error */
2471             eqgrp[j].g->ignore |= ERROR_NONEQ;
2472             if (! QUIET(hostid))
2473                 warn("%s from %s line %d isn't equiv to a valid group",
2474                      eqgrp[j].g->name,
2475                      ((hostid == HOSTID1) ? host1 : host2),
2476                      eqgrp[j].g->linenum);
2477
2478             /* the =group is bad, so we don't need to bother with it anymore */
2479             eqgrp[j].skip = 1;
2480             --new_eq_cnt;
2481             ++missing;
2482             ++j;
2483         }
2484     }
2485
2486     /* note groups that are in a long chain or loop */
2487     chained = new_eq_cnt;
2488     qsort((char *)eqgrp, eq_cnt, sizeof(eqgrp[0]), eq_merge_cmp);
2489     for (j=0; j < new_eq_cnt; ++j) {
2490
2491         /* skip if already skipped */
2492         if (eqgrp[j].skip == 1) {
2493             continue;
2494         }
2495
2496         /* mark as a long loop group */
2497         eqgrp[j].g->ignore |= ERROR_LONGLOOP;
2498         if (! QUIET(hostid))
2499             warn("%s from %s line %d in a long equiv chain or loop > %d",
2500                  eqgrp[j].g->name,
2501                  ((hostid == HOSTID1) ? host1 : host2),
2502                  eqgrp[j].g->linenum, EQ_LOOP);
2503     }
2504
2505     /* all done */
2506     if (D_BUG) {
2507         warn("%d =type groups from %s are not equiv to a valid group",
2508              missing, ((hostid == HOSTID1) ? host1 : host2));
2509         warn("%d =type groups from %s are equiv to themselves",
2510              cycled, ((hostid == HOSTID1) ? host1 : host2));
2511         warn("%d =type groups from %s are in a long chain or loop > %d",
2512              chained, ((hostid == HOSTID1) ? host1 : host2), EQ_LOOP);
2513     }
2514     free(eqgrp);
2515     return missing+cycled+chained;
2516 }
2517
2518 /*
2519  * exec_cmd - exec a ctlinnd command in forked process
2520  *
2521  * given:
2522  *      mode    OUTPUT_EXEC or OUTPUT_IEXEC (interactive mode)
2523  *      cmd     "changegroup", "newgroup", "rmgroup"
2524  *      grp     name of group
2525  *      type    type of group or NULL
2526  *      who     newgroup creator or NULL
2527  *
2528  * returns:
2529  *      1       exec was performed
2530  *      0       exec was not performed
2531  */
2532 static int
2533 exec_cmd(mode, cmd, grp, type, who)
2534     int mode;           /* OUTPUT_EXEC or OUTPUT_IEXEC (interactive mode) */
2535     char *cmd;          /* changegroup, newgroup or rmgroup */
2536     char *grp;          /* name of group to change, add, remove */
2537     char *type;         /* type of group or NULL */
2538     char *who;          /* newgroup creator or NULL */
2539 {
2540     FILE *ch_stream = NULL;     /* stream from a child process */
2541     char buf[BUFSIZ+1];         /* interactive buffer */
2542     int pid;                    /* pid of child process */
2543     int io[2];                  /* pair of pipe descriptors */
2544     int status;                 /* wait status */
2545     int exitval;                /* exit status of the child */
2546     char *p;
2547
2548     /* firewall */
2549     if (cmd == NULL || grp == NULL)
2550         die("internal error #13, cmd or grp is NULL");
2551
2552     /* if interactive, ask the question */
2553     if (mode == OUTPUT_IEXEC) {
2554
2555         /* ask the question */
2556         fflush(stdin);
2557         fflush(stdout);
2558         fflush(stderr);
2559         if (type == NULL) {
2560             printf("%s %s  [yn]? ", cmd, grp);
2561         } else if (who == NULL) {
2562             printf("%s %s %s  [yn]? ", cmd, grp, type);
2563         } else {
2564             printf("%s %s %s %s  [yn]? ", cmd, grp, type, who);
2565         }
2566         fflush(stdout);
2567         buf[0] = '\0';
2568         buf[BUFSIZ] = '\0';
2569         p = fgets(buf, BUFSIZ, stdin);
2570         if (p == NULL) {
2571             /* EOF/ERROR on interactive input, silently stop processing */
2572             exit(43);
2573         }
2574
2575         /* if non-empty line doesn't start with 'y' or 'Y', skip command */
2576         if (buf[0] != 'y' && buf[0] != 'Y' && buf[0] != '\n') {
2577             /* indicate nothing was done */
2578             return 0;
2579         }
2580     }
2581
2582     /* build a pipe for output from child interactive mode */
2583     if (mode == OUTPUT_IEXEC) {
2584         if (pipe(io) < 0)
2585             sysdie("pipe create failed");
2586
2587     /* setup a fake pipe to /dev/null for non-interactive mode */
2588     } else {
2589         io[READ_SIDE] = open(DEV_NULL, 0);
2590         if (io[READ_SIDE] < 0)
2591             sysdie("unable to open %s for reading", DEV_NULL);
2592         io[WRITE_SIDE] = open(DEV_NULL, 1);
2593         if (io[WRITE_SIDE] < 0)
2594             sysdie("unable to open %s for writing", DEV_NULL);
2595     }
2596
2597     /* pause if in non-interactive mode so as to not busy-out the server */
2598     if (mode == OUTPUT_EXEC && z_flag > 0) {
2599         if (D_BUG)
2600             warn("sleeping %d seconds before fork/exec", z_flag);
2601             /* be sure they know what we are stalling */
2602             fflush(stderr);
2603         sleep(z_flag);
2604     }
2605
2606     /* fork the child process */
2607     fflush(stdout);
2608     fflush(stderr);
2609     pid = fork();
2610     if (pid == -1)
2611         sysdie("fork failed");
2612
2613     /* case: child process */
2614     if (pid == 0) {
2615
2616         /*
2617          * prep file descriptors
2618          */
2619         fclose(stdin);
2620         close(io[READ_SIDE]);
2621         if (dup2(io[WRITE_SIDE], 1) < 0)
2622             sysdie("child: dup of write I/O pipe to stdout failed");
2623         if (dup2(io[WRITE_SIDE], 2) < 0)
2624             sysdie("child: dup of write I/O pipe to stderr failed");
2625
2626         /* exec the ctlinnd command */
2627         p = concatpath(innconf->pathbin, _PATH_CTLINND);
2628         if (type == NULL) {
2629             execl(p,
2630                   CTLINND_NAME, CTLINND_TIME_OUT, cmd, grp, (char *) 0);
2631         } else if (who == NULL) {
2632             execl(p,
2633                   CTLINND_NAME, CTLINND_TIME_OUT, cmd, grp, type, (char *) 0);
2634         } else {
2635             execl(p,
2636                   CTLINND_NAME, CTLINND_TIME_OUT, cmd, grp, type, who, (char *) 0);
2637         }
2638
2639         /* child exec failed */
2640         sysdie("child process exec failed");
2641
2642     /* case: parent process */
2643     } else {
2644
2645         /* prep file descriptors */
2646         if (mode != OUTPUT_IEXEC) {
2647             close(io[READ_SIDE]);
2648         }
2649         close(io[WRITE_SIDE]);
2650
2651         /* print a line from the child, if interactive */
2652         if (mode == OUTPUT_IEXEC) {
2653
2654             /* read what the child says */
2655             buf[0] = '\0';
2656             buf[BUFSIZ] = '\0';
2657             ch_stream = fdopen(io[READ_SIDE], "r");
2658             if (ch_stream == NULL)
2659                 sysdie("fdopen of pipe failed");
2660             p = fgets(buf, BUFSIZ, ch_stream);
2661
2662             /* print what the child said, if anything */
2663             if (p != NULL) {
2664                 if (buf[strlen(buf)-1] == '\n')
2665                     buf[strlen(buf)-1] = '\0';
2666                 warn("    %s", buf);
2667             }
2668         }
2669
2670         /* look for abnormal child termination/status */
2671         errno = 0;
2672         while (wait(&status) < 0) {
2673             if (errno == EINTR) {
2674                 /* just an interrupt, try to wait again */
2675                 errno = 0;
2676             } else {
2677                 sysdie("wait returned -1");
2678             }
2679         }
2680         if (mode == OUTPUT_IEXEC) {
2681             /* close the pipe now that we are done with reading it */
2682             fclose(ch_stream);
2683         }
2684         if (WIFSTOPPED(status)) {
2685             warn("    %s %s %s%s%s%s%s stopped",
2686                  CTLINND_NAME, cmd, grp,
2687                  (type ? "" : " "), (type ? type : ""),
2688                  (who ? "" : " "), (who ? who : ""));
2689             /* assume no work was done */
2690             return 0;
2691         }
2692         if (WIFSIGNALED(status)) {
2693             warn("    %s %s %s%s%s%s%s killed by signal %d",
2694                  CTLINND_NAME, cmd, grp,
2695                  (type ? "" : " "), (type ? type : ""),
2696                  (who ? "" : " "), (who ? who : ""), WTERMSIG(status));
2697             /* assume no work was done */
2698             return 0;
2699         }
2700         if (!WIFEXITED(status)) {
2701             warn("    %s %s %s%s%s%s%s returned unknown wait status: 0x%x",
2702                  CTLINND_NAME, cmd, grp,
2703                  (type ? "" : " "), (type ? type : ""),
2704                  (who ? "" : " "), (who ? who : ""), status);
2705             /* assume no work was done */
2706             return 0;
2707         }
2708         exitval = WEXITSTATUS(status);
2709         if (exitval != 0) {
2710             warn("    %s %s %s%s%s%s%s exited with status: %d",
2711                  CTLINND_NAME, cmd, grp,
2712                  (type ? "" : " "), (type ? type : ""),
2713                  (who ? "" : " "), (who ? who : ""), exitval);
2714             /* assume no work was done */
2715             return 0;
2716         }
2717     }
2718
2719     /* all done */
2720     return 1;
2721 }
2722
2723 /*
2724  * new_top_hier - determine if the newsgroup represents a new hierarchy
2725  *
2726  * Determine of the newsgroup name is a new hierarchy.
2727  *
2728  * given:
2729  *      name    name of newsgroup to check
2730  *
2731  * returns:
2732  *      false   hierarchy already exists
2733  *      true    hierarchy does not exist, name represents a new hierarchy
2734  *
2735  * NOTE: This function assumes that we are at the top of the news spool.
2736  */
2737 static int
2738 new_top_hier(name)
2739     char *name;
2740 {
2741     struct stat statbuf;        /* stat of the hierarchy */
2742     int result;                 /* return result */
2743     char *dot;
2744
2745     /*
2746      * temp change name to just the top level
2747      */
2748     dot = strchr(name, '.');
2749     if (dot != NULL) {
2750         *dot = '\0';
2751     }
2752
2753     /*
2754      * determine if we can find this top level hierarchy directory
2755      */
2756     result = !(stat(name, &statbuf) >= 0 && S_ISDIR(statbuf.st_mode));
2757     /* restore name */
2758     if (dot != NULL) {
2759         *dot = '.';
2760     }
2761
2762     /*
2763      * return the result
2764      */
2765     return result;
2766 }