Add the DragonFly cvs id and perform general cleanups on cvs/rcs/sccs ids. Most
[dragonfly.git] / usr.sbin / lpr / common_source / matchjobs.c
1 /*
2  * ------+---------+---------+---------+---------+---------+---------+---------*
3  * Copyright (c) 2002   - Garance Alistair Drosehn <gad@FreeBSD.org>.
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *   1. Redistributions of source code must retain the above copyright
10  *      notice, this list of conditions and the following disclaimer.
11  *   2. Redistributions in binary form must reproduce the above copyright
12  *      notice, this list of conditions and the following disclaimer in the
13  *      documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  *
27  * The views and conclusions contained in the software and documentation
28  * are those of the authors and should not be interpreted as representing
29  * official policies, either expressed or implied, of the FreeBSD Project
30  * or FreeBSD, Inc.
31  *
32  * ------+---------+---------+---------+---------+---------+---------+---------*
33  *
34  * $FreeBSD: src/usr.sbin/lpr/common_source/matchjobs.c,v 1.2.2.2 2002/08/15 18:53:17 schweikh Exp $
35  * $DragonFly: src/usr.sbin/lpr/common_source/matchjobs.c,v 1.2 2003/06/17 04:29:56 dillon Exp $
36  */
37
38 /*
39  * movejobs.c - The lpc commands which move jobs around.
40  */
41
42 #include <sys/file.h>
43 #include <sys/param.h>
44 #include <sys/queue.h>
45 #include <sys/time.h>
46
47 #include <dirent.h>     /* for MAXNAMLEN, for job_cfname in lp.h! */
48 #include <ctype.h>
49 #include <errno.h>
50 #include <fnmatch.h>
51 #include <stdio.h>
52 #include <stdlib.h>
53 #include <string.h>
54 #include <unistd.h>
55 #include "ctlinfo.h"
56 #include "lp.h"
57 #include "matchjobs.h"
58
59 #define DEBUG_PARSEJS   0       /* set to 1 when testing */
60 #define DEBUG_SCANJS    0       /* set to 1 when testing */
61
62 static int       match_jobspec(struct jobqueue *_jq, struct jobspec *_jspec);
63
64 /*
65  * isdigit is defined to work on an 'int', in the range 0 to 255, plus EOF.
66  * Define a wrapper which can take 'char', either signed or unsigned.
67  */
68 #define isdigitch(Anychar)    isdigit(((int) Anychar) & 255)
69
70 /*
71  * Format a single jobspec into a string fit for printing.
72  */
73 void
74 format_jobspec(struct jobspec *jspec, int fmt_wanted)
75 {
76         char rangestr[40], buildstr[200];
77         const char fromuser[] = "from user ";
78         const char fromhost[] = "from host ";
79         size_t strsize;
80
81         /*
82          * If the struct already has a fmtstring, then release it
83          * before building a new one.
84          */
85         if (jspec->fmtoutput != NULL) {
86                 free(jspec->fmtoutput);
87                 jspec->fmtoutput = NULL;
88         }
89
90         jspec->pluralfmt = 1;           /* assume a "plural result" */
91         rangestr[0] = '\0';
92         if (jspec->startnum >= 0) {
93                 if (jspec->startnum != jspec->endrange)
94                         snprintf(rangestr, sizeof(rangestr), "%ld-%ld",
95                             jspec->startnum, jspec->endrange);
96                 else {
97                         jspec->pluralfmt = 0;
98                         snprintf(rangestr, sizeof(rangestr), "%ld",
99                             jspec->startnum);
100                 }
101         }
102
103         strsize = sizeof(buildstr);
104         buildstr[0] = '\0';
105         switch (fmt_wanted) {
106         case FMTJS_TERSE:
107                 /* Build everything but the hostname in a temp string. */
108                 if (jspec->wanteduser != NULL)
109                         strlcat(buildstr, jspec->wanteduser, strsize);
110                 if (rangestr[0] != '\0') {
111                         if (buildstr[0] != '\0')
112                                 strlcat(buildstr, ":", strsize);
113                         strlcat(buildstr, rangestr, strsize);
114                 }
115                 if (jspec->wantedhost != NULL)
116                                 strlcat(buildstr, "@", strsize);
117
118                 /* Get space for the final result, including hostname */
119                 strsize = strlen(buildstr) + 1;
120                 if (jspec->wantedhost != NULL)
121                         strsize += strlen(jspec->wantedhost);
122                 jspec->fmtoutput = malloc(strsize);
123
124                 /* Put together the final result */
125                 strlcpy(jspec->fmtoutput, buildstr, strsize);
126                 if (jspec->wantedhost != NULL)
127                         strlcat(jspec->fmtoutput, jspec->wantedhost, strsize);
128                 break;
129
130         case FMTJS_VERBOSE:
131         default:
132                 /* Build everything but the hostname in a temp string. */
133                 strlcat(buildstr, rangestr, strsize);
134                 if (jspec->wanteduser != NULL) {
135                         if (rangestr[0] != '\0')
136                                 strlcat(buildstr, " ", strsize);
137                         strlcat(buildstr, fromuser, strsize);
138                         strlcat(buildstr, jspec->wanteduser, strsize);
139                 }
140                 if (jspec->wantedhost != NULL) {
141                         if (jspec->wanteduser == NULL) {
142                                 if (rangestr[0] != '\0')
143                                         strlcat(buildstr, " ", strsize);
144                                 strlcat(buildstr, fromhost, strsize);
145                         } else
146                                 strlcat(buildstr, "@", strsize);
147                 }
148
149                 /* Get space for the final result, including hostname */
150                 strsize = strlen(buildstr) + 1;
151                 if (jspec->wantedhost != NULL)
152                         strsize += strlen(jspec->wantedhost);
153                 jspec->fmtoutput = malloc(strsize);
154
155                 /* Put together the final result */
156                 strlcpy(jspec->fmtoutput, buildstr, strsize);
157                 if (jspec->wantedhost != NULL)
158                         strlcat(jspec->fmtoutput, jspec->wantedhost, strsize);
159                 break;
160         }
161 }
162
163 /*
164  * Free all the jobspec-related information.
165  */
166 void
167 free_jobspec(struct jobspec_hdr *js_hdr)
168 {
169         struct jobspec *jsinf;
170
171         while (!STAILQ_EMPTY(js_hdr)) {
172                 jsinf = STAILQ_FIRST(js_hdr);
173                 STAILQ_REMOVE_HEAD(js_hdr, nextjs);
174                 if (jsinf->fmtoutput)
175                         free(jsinf->fmtoutput);
176                 if (jsinf->matcheduser)
177                         free(jsinf->matcheduser);
178                 free(jsinf);
179         }
180 }
181
182 /*
183  * This routine takes a string as typed in from the user, and parses it
184  * into a job-specification.  A job specification would match one or more
185  * jobs in the queue of some single printer (the specification itself does
186  * not indicate which queue should be searched).
187  *
188  * This recognizes a job-number range by itself (all digits, or a range
189  * indicated by "digits-digits"), or a userid by itself.  If a `:' is
190  * found, it is treated as a separator between a job-number range and
191  * a userid, where the job number range is the side which has a digit as
192  * the first character.  If an `@' is found, everything to the right of
193  * it is treated as the hostname the job originated from.
194  *
195  * So, the user can specify:
196  *      jobrange       userid     userid:jobrange    jobrange:userid
197  *      jobrange@hostname   jobrange:userid@hostname
198  *      userid@hostname     userid:jobrange@hostname
199  *
200  * XXX - it would be nice to add "not options" too, such as ^user,
201  *      ^jobrange, and @^hostname.
202  *
203  * This routine may modify the original input string if that input is
204  * valid.  If the input was *not* valid, then this routine should return
205  * with the input string the same as when the routine was called.
206  */
207 int
208 parse_jobspec(char *jobstr, struct jobspec_hdr *js_hdr)
209 {
210         struct jobspec *jsinfo;
211         char *atsign, *colon, *lhside, *numstr, *period, *rhside;
212         int jobnum;
213
214 #if DEBUG_PARSEJS
215         printf("\t [ pjs-input = %s ]\n", jobstr);
216 #endif
217
218         if ((jobstr == NULL) || (*jobstr == '\0'))
219                 return (0);
220
221         jsinfo = malloc(sizeof(struct jobspec));
222         memset(jsinfo, 0, sizeof(struct jobspec));
223         jsinfo->startnum = jsinfo->endrange = -1;
224
225         /* Find the separator characters, and nullify them. */
226         numstr = NULL;
227         atsign = strchr(jobstr, '@');
228         colon = strchr(jobstr, ':');
229         if (atsign != NULL)
230                 *atsign = '\0';
231         if (colon != NULL)
232                 *colon = '\0';
233
234         /* The at-sign always indicates a hostname. */
235         if (atsign != NULL) {
236                 rhside = atsign + 1;
237                 if (*rhside != '\0')
238                         jsinfo->wantedhost = rhside;
239         }
240
241         /* Finish splitting the input into three parts. */
242         rhside = NULL;
243         if (colon != NULL) {
244                 rhside = colon + 1;
245                 if (*rhside == '\0')
246                         rhside = NULL;
247         }
248         lhside = NULL;
249         if (*jobstr != '\0')
250                 lhside = jobstr;
251
252         /*
253          * If there is a `:' here, then it's either jobrange:userid,
254          * userid:jobrange, or (if @hostname was not given) perhaps it
255          * might be hostname:jobnum.  The side which has a digit as the
256          * first character is assumed to be the jobrange.  It is an
257          * input error if both sides start with a digit, or if neither
258          * side starts with a digit.
259          */
260         if ((lhside != NULL) && (rhside != NULL)) {
261                 if (isdigitch(*lhside)) {
262                         if (isdigitch(*rhside))
263                                 goto bad_input;
264                         numstr = lhside;
265                         jsinfo->wanteduser = rhside;
266                 } else if (isdigitch(*rhside)) {
267                         numstr = rhside;
268                         /*
269                          * The original implementation of 'lpc topq' accepted
270                          * hostname:jobnum.  If the input did not include a
271                          * @hostname, then assume the userid is a hostname if
272                          * it includes a '.'.
273                          */
274                         period = strchr(lhside, '.');
275                         if ((atsign == NULL) && (period != NULL))
276                                 jsinfo->wantedhost = lhside;
277                         else
278                                 jsinfo->wanteduser = lhside;
279                 } else {
280                         /* Neither side is a job number = user error */
281                         goto bad_input;
282                 }
283         } else if (lhside != NULL) {
284                 if (isdigitch(*lhside))
285                         numstr = lhside;
286                 else
287                         jsinfo->wanteduser = lhside;
288         } else if (rhside != NULL) {
289                 if (isdigitch(*rhside))
290                         numstr = rhside;
291                 else
292                         jsinfo->wanteduser = rhside;
293         }
294
295         /*
296          * Break down the numstr.  It should be all digits, or a range
297          * specified as "\d+-\d+".
298          */
299         if (numstr != NULL) {
300                 errno = 0;
301                 jobnum = strtol(numstr, &numstr, 10);
302                 if (errno != 0)         /* error in conversion */
303                         goto bad_input;
304                 if (jobnum < 0)         /* a bogus value for this purpose */
305                         goto bad_input;
306                 if (jobnum > 99999)     /* too large for job number */
307                         goto bad_input;
308                 jsinfo->startnum = jsinfo->endrange = jobnum;
309
310                 /* Check for a range of numbers */
311                 if ((*numstr == '-') && (isdigitch(*(numstr + 1)))) {
312                         numstr++;
313                         errno = 0;
314                         jobnum = strtol(numstr, &numstr, 10);
315                         if (errno != 0)         /* error in conversion */
316                                 goto bad_input;
317                         if (jobnum < jsinfo->startnum)
318                                 goto bad_input;
319                         if (jobnum > 99999)     /* too large for job number */
320                                 goto bad_input;
321                         jsinfo->endrange = jobnum;
322                 }
323
324                 /*
325                  * If there is anything left in the numstr, and if the
326                  * original string did not include a userid or a hostname,
327                  * then this might be the ancient form of '\d+hostname'
328                  * (with no separator between jobnum and hostname).  Accept
329                  * that for backwards compatibility, but otherwise any
330                  * remaining characters mean a user-error.  Note that the
331                  * ancient form accepted only a single number, but this
332                  * will also accept a range of numbers.
333                  */
334                 if (*numstr != '\0') {
335                         if (atsign != NULL)
336                                 goto bad_input;
337                         if (jsinfo->wantedhost != NULL)
338                                 goto bad_input;
339                         if (jsinfo->wanteduser != NULL)
340                                 goto bad_input;
341                         /* Treat as the rest of the string as a hostname */
342                         jsinfo->wantedhost = numstr;
343                 }
344         }
345
346         if ((jsinfo->startnum < 0) && (jsinfo->wanteduser == NULL) &&
347             (jsinfo->wantedhost == NULL))
348                 goto bad_input;
349
350         /*
351          * The input was valid, in the sense that it could be parsed
352          * into the individual parts.  Add this jobspec to the list
353          * of jobspecs.
354          */
355         STAILQ_INSERT_TAIL(js_hdr, jsinfo, nextjs);
356
357 #if DEBUG_PARSEJS
358         printf("\t [   will check for");
359         if (jsinfo->startnum >= 0) {
360                 if (jsinfo->startnum == jsinfo->endrange)
361                         printf(" jobnum = %ld", jsinfo->startnum);
362                 else
363                         printf(" jobrange = %ld to %ld", jsinfo->startnum,
364                             jsinfo->endrange);
365         } else {
366                 printf(" jobs");
367         }
368         if ((jsinfo->wanteduser != NULL) || (jsinfo->wantedhost != NULL)) {
369                 printf(" from");
370                 if (jsinfo->wanteduser != NULL)
371                         printf(" user = %s", jsinfo->wanteduser);
372                 if (jsinfo->wantedhost != NULL)
373                         printf(" host = %s", jsinfo->wantedhost);
374         }
375         printf("]\n");
376 #endif
377
378         return (1);
379
380 bad_input:
381         /*
382          * Restore any `@' and `:', in case the calling routine wants to
383          * write an error message which includes the input string.
384          */
385         if (atsign != NULL)
386                 *atsign = '@';
387         if (colon != NULL)
388                 *colon = ':';
389         if (jsinfo != NULL)
390                 free(jsinfo);
391         return (0);
392 }
393
394 /*
395  * Check to see if a given job (specified by a jobqueue entry) matches
396  * all of the specifications in a given jobspec.
397  *
398  * Returns 0 if no match, 1 if the job does match.
399  */
400 static int
401 match_jobspec(struct jobqueue *jq, struct jobspec *jspec)
402 {
403         struct cjobinfo *cfinf;
404         char *cp, *cf_numstr, *cf_hoststr;
405         int jnum, match;
406
407 #if DEBUG_SCANJS
408         printf("\t [ match-js checking %s ]\n", jq->job_cfname);
409 #endif
410
411         if (jspec == NULL || jq == NULL)
412                 return (0);
413
414         /*
415          * Keep track of which jobs have already been matched by this
416          * routine, and thus (probably) already processed.
417          */
418         if (jq->job_matched)
419                 return (0);
420
421         /*
422          * The standard `cf' file has the job number start in position 4,
423          * but some implementations have that as an extra file-sequence
424          * letter, and start the job number in position 5.  The job
425          * number is usually three bytes, but may be as many as five.
426          *
427          * XXX - All this nonsense should really be handled in a single
428          *      place, like getq()...
429          */
430         cf_numstr = jq->job_cfname + 3;
431         if (!isdigitch(*cf_numstr))
432                 cf_numstr++;
433         jnum = 0;
434         for (cp = cf_numstr; (cp < cf_numstr + 5) && isdigitch(*cp); cp++)
435                 jnum = jnum * 10 + (*cp - '0');
436         cf_hoststr = cp;
437         cfinf = NULL;
438         match = 0;                      /* assume the job will not match */
439         jspec->matcheduser = NULL;
440
441         /*
442          * Check the job-number range.
443          */ 
444         if (jspec->startnum >= 0) {
445                 if (jnum < jspec->startnum)
446                         goto nomatch;
447                 if (jnum > jspec->endrange)
448                         goto nomatch;
449         }
450
451         /*
452          * Check the hostname.  Strictly speaking this should be done by
453          * reading the control file, but it is less expensive to check
454          * the hostname-part of the control file name.  Also, this value
455          * can be easily seen in 'lpq -l', while there is no easy way for
456          * a user/operator to see the hostname in the control file.
457          */
458         if (jspec->wantedhost != NULL) {
459                 if (fnmatch(jspec->wantedhost, cf_hoststr, 0) != 0)
460                         goto nomatch;
461         }
462
463         /*
464          * Check for a match on the user name.  This has to be done
465          * by reading the control file.
466          */
467         if (jspec->wanteduser != NULL) {
468                 cfinf = ctl_readcf("fakeq", jq->job_cfname);
469                 if (cfinf == NULL)
470                         goto nomatch;
471                 if (fnmatch(jspec->wanteduser, cfinf->cji_username, 0) != 0)
472                         goto nomatch;
473         }
474
475         /* This job matches all of the specified criteria. */
476         match = 1;
477         jq->job_matched = 1;            /* avoid matching the job twice */
478         jspec->matchcnt++;
479         if (jspec->wanteduser != NULL) {
480                 /*
481                  * If the user specified a userid (which may have been a
482                  * pattern), then the caller's "doentry()" routine might
483                  * want to know the userid of this job that matched.
484                  */
485                 jspec->matcheduser = strdup(cfinf->cji_username);
486         }
487 #if DEBUG_SCANJS
488         printf("\t [ job matched! ]\n");
489 #endif
490
491 nomatch:
492         if (cfinf != NULL)
493                 ctl_freeinf(cfinf);
494         return (match);
495 }
496
497 /*
498  * Scan a queue for all jobs which match a jobspec.  The queue is scanned
499  * from top to bottom.
500  *
501  * The caller can provide a routine which will be executed for each job
502  * that does match.  Note that the processing routine might do anything
503  * to the matched job -- including the removal of it.
504  *
505  * This returns the number of jobs which were matched.
506  */
507 int
508 scanq_jobspec(int qcount, struct jobqueue **squeue, int sopts, struct
509     jobspec_hdr *js_hdr, process_jqe doentry, void *doentryinfo)
510 {
511         struct jobqueue **qent;
512         struct jobspec *jspec;
513         int cnt, matched, total;
514
515         if (qcount < 1)
516                 return (0);
517         if (js_hdr == NULL)
518                 return (-1);
519
520         /* The caller must specify one of the scanning orders */
521         if ((sopts & (SCQ_JSORDER|SCQ_QORDER)) == 0)
522                 return (-1);
523
524         total = 0;
525         if (sopts & SCQ_JSORDER) {
526                 /*
527                  * For each job specification, scan through the queue
528                  * looking for every job that matches.
529                  */
530                 STAILQ_FOREACH(jspec, js_hdr, nextjs) {
531                         for (qent = squeue, cnt = 0; cnt < qcount;
532                             qent++, cnt++) {
533                                 matched = match_jobspec(*qent, jspec);
534                                 if (!matched)
535                                         continue;
536                                 total++;
537                                 if (doentry != NULL)
538                                         doentry(doentryinfo, *qent, jspec);
539                                 if (jspec->matcheduser != NULL) {
540                                         free(jspec->matcheduser);
541                                         jspec->matcheduser = NULL;
542                                 }
543                         }
544                         /*
545                          * The entire queue has been scanned for this
546                          * jobspec.  Call the user's routine again with
547                          * a NULL queue-entry, so it can print out any
548                          * kind of per-jobspec summary.
549                          */
550                         if (doentry != NULL)
551                                 doentry(doentryinfo, NULL, jspec);
552                 }
553         } else {
554                 /*
555                  * For each job in the queue, check all of the job
556                  * specifications to see if any one of them matches
557                  * that job.
558                  */
559                 for (qent = squeue, cnt = 0; cnt < qcount;
560                     qent++, cnt++) {
561                         STAILQ_FOREACH(jspec, js_hdr, nextjs) {
562                                 matched = match_jobspec(*qent, jspec);
563                                 if (!matched)
564                                         continue;
565                                 total++;
566                                 if (doentry != NULL)
567                                         doentry(doentryinfo, *qent, jspec);
568                                 if (jspec->matcheduser != NULL) {
569                                         free(jspec->matcheduser);
570                                         jspec->matcheduser = NULL;
571                                 }
572                                 /*
573                                  * Once there is a match, then there is no
574                                  * point in checking this same job against
575                                  * all the other jobspec's.
576                                  */
577                                 break;
578                         }
579                 }
580         }
581
582         return (total);
583 }