1 /*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * ------+---------+---------+---------+---------+---------+---------+---------*
5 * Copyright (c) 2002,2011 - Garance Alistair Drosehn <gad@FreeBSD.org>.
6 * All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 * 1. Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 * SUCH DAMAGE.
28 *
29 * The views and conclusions contained in the software and documentation
30 * are those of the authors and should not be interpreted as representing
31 * official policies, either expressed or implied, of the FreeBSD Project
32 * or FreeBSD, Inc.
33 *
34 * ------+---------+---------+---------+---------+---------+---------+---------*
35 */
36
37 #include "lp.cdefs.h" /* A cross-platform version of <sys/cdefs.h> */
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
format_jobspec(struct jobspec * jspec,int fmt_wanted)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
free_jobspec(struct jobspec_hdr * js_hdr)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
parse_jobspec(char * jobstr,struct jobspec_hdr * js_hdr)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
match_jobspec(struct jobqueue * jq,struct jobspec * jspec)401 match_jobspec(struct jobqueue *jq, struct jobspec *jspec)
402 {
403 struct cjobinfo *cfinf;
404 const char *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 jnum = calc_jobnum(jq->job_cfname, &cf_hoststr);
422 cfinf = NULL;
423 match = 0; /* assume the job will not match */
424 jspec->matcheduser = NULL;
425
426 /*
427 * Check the job-number range.
428 */
429 if (jspec->startnum >= 0) {
430 if (jnum < jspec->startnum)
431 goto nomatch;
432 if (jnum > jspec->endrange)
433 goto nomatch;
434 }
435
436 /*
437 * Check the hostname. Strictly speaking this should be done by
438 * reading the control file, but it is less expensive to check
439 * the hostname-part of the control file name. Also, this value
440 * can be easily seen in 'lpq -l', while there is no easy way for
441 * a user/operator to see the hostname in the control file.
442 */
443 if (jspec->wantedhost != NULL) {
444 if (fnmatch(jspec->wantedhost, cf_hoststr, 0) != 0)
445 goto nomatch;
446 }
447
448 /*
449 * Check for a match on the user name. This has to be done
450 * by reading the control file.
451 */
452 if (jspec->wanteduser != NULL) {
453 cfinf = ctl_readcf("fakeq", jq->job_cfname);
454 if (cfinf == NULL)
455 goto nomatch;
456 if (fnmatch(jspec->wanteduser, cfinf->cji_acctuser, 0) != 0)
457 goto nomatch;
458 }
459
460 /* This job matches all of the specified criteria. */
461 match = 1;
462 jq->job_matched = 1; /* avoid matching the job twice */
463 jspec->matchcnt++;
464 if (jspec->wanteduser != NULL) {
465 /*
466 * If the user specified a userid (which may have been a
467 * pattern), then the caller's "doentry()" routine might
468 * want to know the userid of this job that matched.
469 */
470 jspec->matcheduser = strdup(cfinf->cji_acctuser);
471 }
472 #if DEBUG_SCANJS
473 printf("\t [ job matched! ]\n");
474 #endif
475
476 nomatch:
477 if (cfinf != NULL)
478 ctl_freeinf(cfinf);
479 return (match);
480 }
481
482 /*
483 * Scan a queue for all jobs which match a jobspec. The queue is scanned
484 * from top to bottom.
485 *
486 * The caller can provide a routine which will be executed for each job
487 * that does match. Note that the processing routine might do anything
488 * to the matched job -- including the removal of it.
489 *
490 * This returns the number of jobs which were matched.
491 */
492 int
scanq_jobspec(int qcount,struct jobqueue ** squeue,int sopts,struct jobspec_hdr * js_hdr,process_jqe doentry,void * doentryinfo)493 scanq_jobspec(int qcount, struct jobqueue **squeue, int sopts, struct
494 jobspec_hdr *js_hdr, process_jqe doentry, void *doentryinfo)
495 {
496 struct jobqueue **qent;
497 struct jobspec *jspec;
498 int cnt, matched, total;
499
500 if (qcount < 1)
501 return (0);
502 if (js_hdr == NULL)
503 return (-1);
504
505 /* The caller must specify one of the scanning orders */
506 if ((sopts & (SCQ_JSORDER|SCQ_QORDER)) == 0)
507 return (-1);
508
509 total = 0;
510 if (sopts & SCQ_JSORDER) {
511 /*
512 * For each job specification, scan through the queue
513 * looking for every job that matches.
514 */
515 STAILQ_FOREACH(jspec, js_hdr, nextjs) {
516 for (qent = squeue, cnt = 0; cnt < qcount;
517 qent++, cnt++) {
518 matched = match_jobspec(*qent, jspec);
519 if (!matched)
520 continue;
521 total++;
522 if (doentry != NULL)
523 doentry(doentryinfo, *qent, jspec);
524 if (jspec->matcheduser != NULL) {
525 free(jspec->matcheduser);
526 jspec->matcheduser = NULL;
527 }
528 }
529 /*
530 * The entire queue has been scanned for this
531 * jobspec. Call the user's routine again with
532 * a NULL queue-entry, so it can print out any
533 * kind of per-jobspec summary.
534 */
535 if (doentry != NULL)
536 doentry(doentryinfo, NULL, jspec);
537 }
538 } else {
539 /*
540 * For each job in the queue, check all of the job
541 * specifications to see if any one of them matches
542 * that job.
543 */
544 for (qent = squeue, cnt = 0; cnt < qcount;
545 qent++, cnt++) {
546 STAILQ_FOREACH(jspec, js_hdr, nextjs) {
547 matched = match_jobspec(*qent, jspec);
548 if (!matched)
549 continue;
550 total++;
551 if (doentry != NULL)
552 doentry(doentryinfo, *qent, jspec);
553 if (jspec->matcheduser != NULL) {
554 free(jspec->matcheduser);
555 jspec->matcheduser = NULL;
556 }
557 /*
558 * Once there is a match, then there is no
559 * point in checking this same job against
560 * all the other jobspec's.
561 */
562 break;
563 }
564 }
565 }
566
567 return (total);
568 }
569