matchjobs.c revision 101677
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
36static const char rcsid[] =
37  "$FreeBSD: head/usr.sbin/lpr/common_source/matchjobs.c 101677 2002-08-11 13:05:30Z schweikh $";
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
64static 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 */
75void
76format_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 */
168void
169free_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 */
209int
210parse_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
382bad_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 */
402static int
403match_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
493nomatch:
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 */
509int
510scanq_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}
586