xref: /freebsd/usr.sbin/lpr/common_source/matchjobs.c (revision f0adf7f5cdd241db2f2c817683191a6ef64a4e95)
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 #include "lp.cdefs.h"		/* A cross-platform version of <sys/cdefs.h> */
36 __FBSDID("$FreeBSD$");
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 }
584