matchjobs.c revision 117541
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: head/usr.sbin/lpr/common_source/matchjobs.c 117541 2003-07-14 05:15:21Z gad $");
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
62static 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 */
73void
74format_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 */
166void
167free_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 */
207int
208parse_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
380bad_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 */
400static int
401match_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
491nomatch:
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 */
507int
508scanq_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