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