1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 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 /* 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 62 static 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 */ 73 void 74 format_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 */ 166 void 167 free_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 */ 207 int 208 parse_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 380 bad_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 */ 400 static int 401 match_jobspec(struct jobqueue *jq, struct jobspec *jspec) 402 { 403 struct cjobinfo *cfinf; 404 const char *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 jnum = calc_jobnum(jq->job_cfname, &cf_hoststr); 422 cfinf = NULL; 423 match = 0; /* assume the job will not match */ 424 jspec->matcheduser = NULL; 425 426 /* 427 * Check the job-number range. 428 */ 429 if (jspec->startnum >= 0) { 430 if (jnum < jspec->startnum) 431 goto nomatch; 432 if (jnum > jspec->endrange) 433 goto nomatch; 434 } 435 436 /* 437 * Check the hostname. Strictly speaking this should be done by 438 * reading the control file, but it is less expensive to check 439 * the hostname-part of the control file name. Also, this value 440 * can be easily seen in 'lpq -l', while there is no easy way for 441 * a user/operator to see the hostname in the control file. 442 */ 443 if (jspec->wantedhost != NULL) { 444 if (fnmatch(jspec->wantedhost, cf_hoststr, 0) != 0) 445 goto nomatch; 446 } 447 448 /* 449 * Check for a match on the user name. This has to be done 450 * by reading the control file. 451 */ 452 if (jspec->wanteduser != NULL) { 453 cfinf = ctl_readcf("fakeq", jq->job_cfname); 454 if (cfinf == NULL) 455 goto nomatch; 456 if (fnmatch(jspec->wanteduser, cfinf->cji_acctuser, 0) != 0) 457 goto nomatch; 458 } 459 460 /* This job matches all of the specified criteria. */ 461 match = 1; 462 jq->job_matched = 1; /* avoid matching the job twice */ 463 jspec->matchcnt++; 464 if (jspec->wanteduser != NULL) { 465 /* 466 * If the user specified a userid (which may have been a 467 * pattern), then the caller's "doentry()" routine might 468 * want to know the userid of this job that matched. 469 */ 470 jspec->matcheduser = strdup(cfinf->cji_acctuser); 471 } 472 #if DEBUG_SCANJS 473 printf("\t [ job matched! ]\n"); 474 #endif 475 476 nomatch: 477 if (cfinf != NULL) 478 ctl_freeinf(cfinf); 479 return (match); 480 } 481 482 /* 483 * Scan a queue for all jobs which match a jobspec. The queue is scanned 484 * from top to bottom. 485 * 486 * The caller can provide a routine which will be executed for each job 487 * that does match. Note that the processing routine might do anything 488 * to the matched job -- including the removal of it. 489 * 490 * This returns the number of jobs which were matched. 491 */ 492 int 493 scanq_jobspec(int qcount, struct jobqueue **squeue, int sopts, struct 494 jobspec_hdr *js_hdr, process_jqe doentry, void *doentryinfo) 495 { 496 struct jobqueue **qent; 497 struct jobspec *jspec; 498 int cnt, matched, total; 499 500 if (qcount < 1) 501 return (0); 502 if (js_hdr == NULL) 503 return (-1); 504 505 /* The caller must specify one of the scanning orders */ 506 if ((sopts & (SCQ_JSORDER|SCQ_QORDER)) == 0) 507 return (-1); 508 509 total = 0; 510 if (sopts & SCQ_JSORDER) { 511 /* 512 * For each job specification, scan through the queue 513 * looking for every job that matches. 514 */ 515 STAILQ_FOREACH(jspec, js_hdr, nextjs) { 516 for (qent = squeue, cnt = 0; cnt < qcount; 517 qent++, cnt++) { 518 matched = match_jobspec(*qent, jspec); 519 if (!matched) 520 continue; 521 total++; 522 if (doentry != NULL) 523 doentry(doentryinfo, *qent, jspec); 524 if (jspec->matcheduser != NULL) { 525 free(jspec->matcheduser); 526 jspec->matcheduser = NULL; 527 } 528 } 529 /* 530 * The entire queue has been scanned for this 531 * jobspec. Call the user's routine again with 532 * a NULL queue-entry, so it can print out any 533 * kind of per-jobspec summary. 534 */ 535 if (doentry != NULL) 536 doentry(doentryinfo, NULL, jspec); 537 } 538 } else { 539 /* 540 * For each job in the queue, check all of the job 541 * specifications to see if any one of them matches 542 * that job. 543 */ 544 for (qent = squeue, cnt = 0; cnt < qcount; 545 qent++, cnt++) { 546 STAILQ_FOREACH(jspec, js_hdr, nextjs) { 547 matched = match_jobspec(*qent, jspec); 548 if (!matched) 549 continue; 550 total++; 551 if (doentry != NULL) 552 doentry(doentryinfo, *qent, jspec); 553 if (jspec->matcheduser != NULL) { 554 free(jspec->matcheduser); 555 jspec->matcheduser = NULL; 556 } 557 /* 558 * Once there is a match, then there is no 559 * point in checking this same job against 560 * all the other jobspec's. 561 */ 562 break; 563 } 564 } 565 } 566 567 return (total); 568 } 569