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