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