1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3 * 4 * ------+---------+---------+---------+---------+---------+---------+---------* 5 * Copyright (c) 2002,2011 - Garance Alistair Drosehn <gad@FreeBSD.org>. 6 * All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 * SUCH DAMAGE. 28 * 29 * The views and conclusions contained in the software and documentation 30 * are those of the authors and should not be interpreted as representing 31 * official policies, either expressed or implied, of the FreeBSD Project 32 * or FreeBSD, Inc. 33 * 34 * ------+---------+---------+---------+---------+---------+---------+---------* 35 */ 36 37 #include "lp.cdefs.h" /* A cross-platform version of <sys/cdefs.h> */ 38 __FBSDID("$FreeBSD$"); 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 64 static 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 */ 75 void 76 format_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 */ 168 void 169 free_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 */ 209 int 210 parse_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 382 bad_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 */ 402 static int 403 match_jobspec(struct jobqueue *jq, struct jobspec *jspec) 404 { 405 struct cjobinfo *cfinf; 406 const char *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 jnum = calc_jobnum(jq->job_cfname, &cf_hoststr); 424 cfinf = NULL; 425 match = 0; /* assume the job will not match */ 426 jspec->matcheduser = NULL; 427 428 /* 429 * Check the job-number range. 430 */ 431 if (jspec->startnum >= 0) { 432 if (jnum < jspec->startnum) 433 goto nomatch; 434 if (jnum > jspec->endrange) 435 goto nomatch; 436 } 437 438 /* 439 * Check the hostname. Strictly speaking this should be done by 440 * reading the control file, but it is less expensive to check 441 * the hostname-part of the control file name. Also, this value 442 * can be easily seen in 'lpq -l', while there is no easy way for 443 * a user/operator to see the hostname in the control file. 444 */ 445 if (jspec->wantedhost != NULL) { 446 if (fnmatch(jspec->wantedhost, cf_hoststr, 0) != 0) 447 goto nomatch; 448 } 449 450 /* 451 * Check for a match on the user name. This has to be done 452 * by reading the control file. 453 */ 454 if (jspec->wanteduser != NULL) { 455 cfinf = ctl_readcf("fakeq", jq->job_cfname); 456 if (cfinf == NULL) 457 goto nomatch; 458 if (fnmatch(jspec->wanteduser, cfinf->cji_acctuser, 0) != 0) 459 goto nomatch; 460 } 461 462 /* This job matches all of the specified criteria. */ 463 match = 1; 464 jq->job_matched = 1; /* avoid matching the job twice */ 465 jspec->matchcnt++; 466 if (jspec->wanteduser != NULL) { 467 /* 468 * If the user specified a userid (which may have been a 469 * pattern), then the caller's "doentry()" routine might 470 * want to know the userid of this job that matched. 471 */ 472 jspec->matcheduser = strdup(cfinf->cji_acctuser); 473 } 474 #if DEBUG_SCANJS 475 printf("\t [ job matched! ]\n"); 476 #endif 477 478 nomatch: 479 if (cfinf != NULL) 480 ctl_freeinf(cfinf); 481 return (match); 482 } 483 484 /* 485 * Scan a queue for all jobs which match a jobspec. The queue is scanned 486 * from top to bottom. 487 * 488 * The caller can provide a routine which will be executed for each job 489 * that does match. Note that the processing routine might do anything 490 * to the matched job -- including the removal of it. 491 * 492 * This returns the number of jobs which were matched. 493 */ 494 int 495 scanq_jobspec(int qcount, struct jobqueue **squeue, int sopts, struct 496 jobspec_hdr *js_hdr, process_jqe doentry, void *doentryinfo) 497 { 498 struct jobqueue **qent; 499 struct jobspec *jspec; 500 int cnt, matched, total; 501 502 if (qcount < 1) 503 return (0); 504 if (js_hdr == NULL) 505 return (-1); 506 507 /* The caller must specify one of the scanning orders */ 508 if ((sopts & (SCQ_JSORDER|SCQ_QORDER)) == 0) 509 return (-1); 510 511 total = 0; 512 if (sopts & SCQ_JSORDER) { 513 /* 514 * For each job specification, scan through the queue 515 * looking for every job that matches. 516 */ 517 STAILQ_FOREACH(jspec, js_hdr, nextjs) { 518 for (qent = squeue, cnt = 0; cnt < qcount; 519 qent++, cnt++) { 520 matched = match_jobspec(*qent, jspec); 521 if (!matched) 522 continue; 523 total++; 524 if (doentry != NULL) 525 doentry(doentryinfo, *qent, jspec); 526 if (jspec->matcheduser != NULL) { 527 free(jspec->matcheduser); 528 jspec->matcheduser = NULL; 529 } 530 } 531 /* 532 * The entire queue has been scanned for this 533 * jobspec. Call the user's routine again with 534 * a NULL queue-entry, so it can print out any 535 * kind of per-jobspec summary. 536 */ 537 if (doentry != NULL) 538 doentry(doentryinfo, NULL, jspec); 539 } 540 } else { 541 /* 542 * For each job in the queue, check all of the job 543 * specifications to see if any one of them matches 544 * that job. 545 */ 546 for (qent = squeue, cnt = 0; cnt < qcount; 547 qent++, cnt++) { 548 STAILQ_FOREACH(jspec, js_hdr, nextjs) { 549 matched = match_jobspec(*qent, jspec); 550 if (!matched) 551 continue; 552 total++; 553 if (doentry != NULL) 554 doentry(doentryinfo, *qent, jspec); 555 if (jspec->matcheduser != NULL) { 556 free(jspec->matcheduser); 557 jspec->matcheduser = NULL; 558 } 559 /* 560 * Once there is a match, then there is no 561 * point in checking this same job against 562 * all the other jobspec's. 563 */ 564 break; 565 } 566 } 567 } 568 569 return (total); 570 } 571