xref: /freebsd/usr.sbin/lpr/common_source/matchjobs.c (revision f6a3b357e9be4c6423c85eff9a847163a0d307c8)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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 __FBSDID("$FreeBSD$");
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 	const char *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 	jnum = calc_jobnum(jq->job_cfname, &cf_hoststr);
424 	cfinf = NULL;
425 	match = 0;			/* assume the job will not match */
426 	jspec->matcheduser = NULL;
427 
428 	/*
429 	 * Check the job-number range.
430 	 */
431 	if (jspec->startnum >= 0) {
432 		if (jnum < jspec->startnum)
433 			goto nomatch;
434 		if (jnum > jspec->endrange)
435 			goto nomatch;
436 	}
437 
438 	/*
439 	 * Check the hostname.  Strictly speaking this should be done by
440 	 * reading the control file, but it is less expensive to check
441 	 * the hostname-part of the control file name.  Also, this value
442 	 * can be easily seen in 'lpq -l', while there is no easy way for
443 	 * a user/operator to see the hostname in the control file.
444 	 */
445 	if (jspec->wantedhost != NULL) {
446 		if (fnmatch(jspec->wantedhost, cf_hoststr, 0) != 0)
447 			goto nomatch;
448 	}
449 
450 	/*
451 	 * Check for a match on the user name.  This has to be done
452 	 * by reading the control file.
453 	 */
454 	if (jspec->wanteduser != NULL) {
455 		cfinf = ctl_readcf("fakeq", jq->job_cfname);
456 		if (cfinf == NULL)
457 			goto nomatch;
458 		if (fnmatch(jspec->wanteduser, cfinf->cji_acctuser, 0) != 0)
459 			goto nomatch;
460 	}
461 
462 	/* This job matches all of the specified criteria. */
463 	match = 1;
464 	jq->job_matched = 1;		/* avoid matching the job twice */
465 	jspec->matchcnt++;
466 	if (jspec->wanteduser != NULL) {
467 		/*
468 		 * If the user specified a userid (which may have been a
469 		 * pattern), then the caller's "doentry()" routine might
470 		 * want to know the userid of this job that matched.
471 		 */
472 		jspec->matcheduser = strdup(cfinf->cji_acctuser);
473 	}
474 #if DEBUG_SCANJS
475 	printf("\t [ job matched! ]\n");
476 #endif
477 
478 nomatch:
479 	if (cfinf != NULL)
480 		ctl_freeinf(cfinf);
481 	return (match);
482 }
483 
484 /*
485  * Scan a queue for all jobs which match a jobspec.  The queue is scanned
486  * from top to bottom.
487  *
488  * The caller can provide a routine which will be executed for each job
489  * that does match.  Note that the processing routine might do anything
490  * to the matched job -- including the removal of it.
491  *
492  * This returns the number of jobs which were matched.
493  */
494 int
495 scanq_jobspec(int qcount, struct jobqueue **squeue, int sopts, struct
496     jobspec_hdr *js_hdr, process_jqe doentry, void *doentryinfo)
497 {
498 	struct jobqueue **qent;
499 	struct jobspec *jspec;
500 	int cnt, matched, total;
501 
502 	if (qcount < 1)
503 		return (0);
504 	if (js_hdr == NULL)
505 		return (-1);
506 
507 	/* The caller must specify one of the scanning orders */
508 	if ((sopts & (SCQ_JSORDER|SCQ_QORDER)) == 0)
509 		return (-1);
510 
511 	total = 0;
512 	if (sopts & SCQ_JSORDER) {
513 		/*
514 		 * For each job specification, scan through the queue
515 		 * looking for every job that matches.
516 		 */
517 		STAILQ_FOREACH(jspec, js_hdr, nextjs) {
518 			for (qent = squeue, cnt = 0; cnt < qcount;
519 			    qent++, cnt++) {
520 				matched = match_jobspec(*qent, jspec);
521 				if (!matched)
522 					continue;
523 				total++;
524 				if (doentry != NULL)
525 					doentry(doentryinfo, *qent, jspec);
526 				if (jspec->matcheduser != NULL) {
527 					free(jspec->matcheduser);
528 					jspec->matcheduser = NULL;
529 				}
530 			}
531 			/*
532 			 * The entire queue has been scanned for this
533 			 * jobspec.  Call the user's routine again with
534 			 * a NULL queue-entry, so it can print out any
535 			 * kind of per-jobspec summary.
536 			 */
537 			if (doentry != NULL)
538 				doentry(doentryinfo, NULL, jspec);
539 		}
540 	} else {
541 		/*
542 		 * For each job in the queue, check all of the job
543 		 * specifications to see if any one of them matches
544 		 * that job.
545 		 */
546 		for (qent = squeue, cnt = 0; cnt < qcount;
547 		    qent++, cnt++) {
548 			STAILQ_FOREACH(jspec, js_hdr, nextjs) {
549 				matched = match_jobspec(*qent, jspec);
550 				if (!matched)
551 					continue;
552 				total++;
553 				if (doentry != NULL)
554 					doentry(doentryinfo, *qent, jspec);
555 				if (jspec->matcheduser != NULL) {
556 					free(jspec->matcheduser);
557 					jspec->matcheduser = NULL;
558 				}
559 				/*
560 				 * Once there is a match, then there is no
561 				 * point in checking this same job against
562 				 * all the other jobspec's.
563 				 */
564 				break;
565 			}
566 		}
567 	}
568 
569 	return (total);
570 }
571