1/* $OpenBSD: rcsutil.c,v 1.48 2023/08/11 05:02:21 guenther Exp $ */ 2/* 3 * Copyright (c) 2005, 2006 Joris Vink <joris@openbsd.org> 4 * Copyright (c) 2006 Xavier Santolaria <xsa@openbsd.org> 5 * Copyright (c) 2006 Niall O'Higgins <niallo@openbsd.org> 6 * Copyright (c) 2006 Ray Lai <ray@openbsd.org> 7 * All rights reserved. 8 * 9 * Redistribution and use in source and binary forms, with or without 10 * modification, are permitted provided that the following conditions 11 * are met: 12 * 13 * 1. Redistributions of source code must retain the above copyright 14 * notice, this list of conditions and the following disclaimer. 15 * 2. The name of the author may not be used to endorse or promote products 16 * derived from this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 19 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 20 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 21 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 24 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 26 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 27 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 */ 29 30#include <sys/stat.h> 31#include <sys/time.h> 32 33#include <ctype.h> 34#include <err.h> 35#include <fcntl.h> 36#include <stdio.h> 37#include <stdlib.h> 38#include <string.h> 39#include <unistd.h> 40 41#include "rcsprog.h" 42 43/* 44 * rcs_get_mtime() 45 * 46 * Get <filename> last modified time. 47 * Returns last modified time on success, or a timespec with tv_nsec 48 * set to UTIME_OMIT on failure. 49 */ 50struct timespec 51rcs_get_mtime(RCSFILE *file) 52{ 53 struct stat st; 54 struct timespec mtime = { .tv_sec = 0, .tv_nsec = UTIME_OMIT }; 55 56 if (file->rf_file == NULL) 57 return mtime; 58 59 if (fstat(fileno(file->rf_file), &st) == -1) { 60 warn("%s", file->rf_path); 61 return mtime; 62 } 63 64 return st.st_mtim; 65} 66 67/* 68 * rcs_set_mtime() 69 * 70 * Set <filename> last modified time to <mtime> if its tv_nsec isn't UTIME_OMIT 71 */ 72void 73rcs_set_mtime(RCSFILE *file, struct timespec mtime) 74{ 75 struct timespec ts[2]; 76 77 if (file->rf_file == NULL || mtime.tv_nsec == UTIME_OMIT) 78 return; 79 80 ts[0] = ts[1] = mtime; 81 82 if (futimens(fileno(file->rf_file), ts) == -1) 83 err(1, "utimes"); 84} 85 86int 87rcs_getopt(int argc, char **argv, const char *optstr) 88{ 89 char *a; 90 const char *c; 91 static int i = 1; 92 int opt, hasargument, ret; 93 94 hasargument = 0; 95 rcs_optarg = NULL; 96 97 if (i >= argc) 98 return (-1); 99 100 a = argv[i++]; 101 if (*a++ != '-') 102 return (-1); 103 104 ret = 0; 105 opt = *a; 106 for (c = optstr; *c != '\0'; c++) { 107 if (*c == opt) { 108 a++; 109 ret = opt; 110 111 if (*(c + 1) == ':') { 112 if (*(c + 2) == ':') { 113 if (*a != '\0') 114 hasargument = 1; 115 } else { 116 if (*a != '\0') { 117 hasargument = 1; 118 } else { 119 ret = 1; 120 break; 121 } 122 } 123 } 124 125 if (hasargument == 1) 126 rcs_optarg = a; 127 128 if (ret == opt) 129 rcs_optind++; 130 break; 131 } 132 } 133 134 if (ret == 0) 135 warnx("unknown option -%c", opt); 136 else if (ret == 1) 137 warnx("missing argument for option -%c", opt); 138 139 return (ret); 140} 141 142/* 143 * rcs_choosefile() 144 * 145 * Given a relative filename, decide where the corresponding RCS file 146 * should be. Tries each extension until a file is found. If no file 147 * was found, returns a path with the first extension. 148 * 149 * Opens and returns file descriptor to RCS file. 150 */ 151int 152rcs_choosefile(const char *filename, char *out, size_t len) 153{ 154 int fd; 155 struct stat sb; 156 char *p, *ext, name[PATH_MAX], *next, *ptr, rcsdir[PATH_MAX], 157 *suffixes, rcspath[PATH_MAX]; 158 159 /* 160 * If `filename' contains a directory, `rcspath' contains that 161 * directory, including a trailing slash. Otherwise `rcspath' 162 * contains an empty string. 163 */ 164 if (strlcpy(rcspath, filename, sizeof(rcspath)) >= sizeof(rcspath)) 165 errx(1, "rcs_choosefile: truncation"); 166 167 /* If `/' is found, end string after `/'. */ 168 if ((ptr = strrchr(rcspath, '/')) != NULL) 169 *(++ptr) = '\0'; 170 else 171 rcspath[0] = '\0'; 172 173 /* Append RCS/ to `rcspath' if it exists. */ 174 if (strlcpy(rcsdir, rcspath, sizeof(rcsdir)) >= sizeof(rcsdir) || 175 strlcat(rcsdir, RCSDIR, sizeof(rcsdir)) >= sizeof(rcsdir)) 176 errx(1, "rcs_choosefile: truncation"); 177 178 if (stat(rcsdir, &sb) == 0 && S_ISDIR(sb.st_mode)) 179 if (strlcpy(rcspath, rcsdir, sizeof(rcspath)) 180 >= sizeof(rcspath) || 181 strlcat(rcspath, "/", sizeof(rcspath)) >= sizeof(rcspath)) 182 errx(1, "rcs_choosefile: truncation"); 183 184 /* Name of file without path. */ 185 if ((ptr = strrchr(filename, '/')) == NULL) { 186 if (strlcpy(name, filename, sizeof(name)) >= sizeof(name)) 187 errx(1, "rcs_choosefile: truncation"); 188 } else { 189 /* Skip `/'. */ 190 if (strlcpy(name, ptr + 1, sizeof(name)) >= sizeof(name)) 191 errx(1, "rcs_choosefile: truncation"); 192 } 193 194 /* Name of RCS file without an extension. */ 195 if (strlcat(rcspath, name, sizeof(rcspath)) >= sizeof(rcspath)) 196 errx(1, "rcs_choosefile: truncation"); 197 198 /* 199 * If only the empty suffix was given, use existing rcspath. 200 * This ensures that there is at least one suffix for strsep(). 201 */ 202 if (strcmp(rcs_suffixes, "") == 0) { 203 if (strlcpy(out, rcspath, len) >= len) 204 errx(1, "rcs_choosefile: truncation"); 205 fd = open(rcspath, O_RDONLY); 206 return (fd); 207 } 208 209 /* 210 * Cycle through slash-separated `rcs_suffixes', appending each 211 * extension to `rcspath' and testing if the file exists. If it 212 * does, return that string. Otherwise return path with first 213 * extension. 214 */ 215 suffixes = xstrdup(rcs_suffixes); 216 for (next = suffixes; (ext = strsep(&next, "/")) != NULL;) { 217 char fpath[PATH_MAX]; 218 219 if ((p = strrchr(rcspath, ',')) != NULL) { 220 if (!strcmp(p, ext)) { 221 if ((fd = open(rcspath, O_RDONLY)) == -1) 222 continue; 223 224 if (fstat(fd, &sb) == -1) 225 err(1, "%s", rcspath); 226 227 if (strlcpy(out, rcspath, len) >= len) 228 errx(1, "rcs_choosefile: truncation"); 229 230 free(suffixes); 231 return (fd); 232 } 233 234 continue; 235 } 236 237 /* Construct RCS file path. */ 238 if (strlcpy(fpath, rcspath, sizeof(fpath)) >= sizeof(fpath) || 239 strlcat(fpath, ext, sizeof(fpath)) >= sizeof(fpath)) 240 errx(1, "rcs_choosefile: truncation"); 241 242 /* Don't use `filename' as RCS file. */ 243 if (strcmp(fpath, filename) == 0) 244 continue; 245 246 if ((fd = open(fpath, O_RDONLY)) == -1) 247 continue; 248 249 if (fstat(fd, &sb) == -1) 250 err(1, "%s", fpath); 251 252 if (strlcpy(out, fpath, len) >= len) 253 errx(1, "rcs_choosefile: truncation"); 254 255 free(suffixes); 256 return (fd); 257 } 258 259 /* 260 * `suffixes' should now be NUL separated, so the first 261 * extension can be read just by reading `suffixes'. 262 */ 263 if (strlcat(rcspath, suffixes, sizeof(rcspath)) >= sizeof(rcspath)) 264 errx(1, "rcs_choosefile: truncation"); 265 266 free(suffixes); 267 268 if (strlcpy(out, rcspath, len) >= len) 269 errx(1, "rcs_choosefile: truncation"); 270 271 fd = open(rcspath, O_RDONLY); 272 273 return (fd); 274} 275 276/* 277 * Set <str> to <new_str>. Print warning if <str> is redefined. 278 */ 279void 280rcs_setrevstr(char **str, char *new_str) 281{ 282 if (new_str == NULL) 283 return; 284 if (*str != NULL) 285 warnx("redefinition of revision number"); 286 *str = new_str; 287} 288 289/* 290 * Set <str1> or <str2> to <new_str>, depending on which is not set. 291 * If both are set, error out. 292 */ 293void 294rcs_setrevstr2(char **str1, char **str2, char *new_str) 295{ 296 if (new_str == NULL) 297 return; 298 if (*str1 == NULL) 299 *str1 = new_str; 300 else if (*str2 == NULL) 301 *str2 = new_str; 302 else 303 errx(1, "too many revision numbers"); 304} 305 306/* 307 * Get revision from file. The revision can be specified as a symbol or 308 * a revision number. 309 */ 310RCSNUM * 311rcs_getrevnum(const char *rev_str, RCSFILE *file) 312{ 313 RCSNUM *rev; 314 315 /* Search for symbol. */ 316 rev = rcs_sym_getrev(file, rev_str); 317 318 /* Search for revision number. */ 319 if (rev == NULL) 320 rev = rcsnum_parse(rev_str); 321 322 return (rev); 323} 324 325/* 326 * Prompt for and store user's input in an allocated string. 327 * 328 * Returns the string's pointer. 329 */ 330char * 331rcs_prompt(const char *prompt, int flags) 332{ 333 BUF *bp; 334 size_t len; 335 char *buf; 336 337 if (!(flags & INTERACTIVE) && isatty(STDIN_FILENO)) 338 flags |= INTERACTIVE; 339 340 bp = buf_alloc(0); 341 if (flags & INTERACTIVE) 342 (void)fprintf(stderr, "%s", prompt); 343 if (flags & INTERACTIVE) 344 (void)fprintf(stderr, ">> "); 345 clearerr(stdin); 346 while ((buf = fgetln(stdin, &len)) != NULL) { 347 /* The last line may not be EOL terminated. */ 348 if (buf[0] == '.' && (len == 1 || buf[1] == '\n')) 349 break; 350 else 351 buf_append(bp, buf, len); 352 353 if (flags & INTERACTIVE) 354 (void)fprintf(stderr, ">> "); 355 } 356 buf_putc(bp, '\0'); 357 358 return (buf_release(bp)); 359} 360 361u_int 362rcs_rev_select(RCSFILE *file, const char *range) 363{ 364 int i; 365 u_int nrev; 366 const char *ep; 367 char *lstr, *rstr; 368 struct rcs_delta *rdp; 369 struct rcs_argvector *revargv, *revrange; 370 RCSNUM lnum, rnum; 371 372 nrev = 0; 373 (void)memset(&lnum, 0, sizeof(lnum)); 374 (void)memset(&rnum, 0, sizeof(rnum)); 375 376 if (range == NULL) { 377 TAILQ_FOREACH(rdp, &file->rf_delta, rd_list) 378 if (rcsnum_cmp(rdp->rd_num, file->rf_head, 0) == 0) { 379 rdp->rd_flags |= RCS_RD_SELECT; 380 return (1); 381 } 382 return (0); 383 } 384 385 revargv = rcs_strsplit(range, ","); 386 for (i = 0; revargv->argv[i] != NULL; i++) { 387 revrange = rcs_strsplit(revargv->argv[i], ":"); 388 if (revrange->argv[0] == NULL) 389 /* should not happen */ 390 errx(1, "invalid revision range: %s", revargv->argv[i]); 391 else if (revrange->argv[1] == NULL) 392 lstr = rstr = revrange->argv[0]; 393 else { 394 if (revrange->argv[2] != NULL) 395 errx(1, "invalid revision range: %s", 396 revargv->argv[i]); 397 lstr = revrange->argv[0]; 398 rstr = revrange->argv[1]; 399 if (strcmp(lstr, "") == 0) 400 lstr = NULL; 401 if (strcmp(rstr, "") == 0) 402 rstr = NULL; 403 } 404 405 if (lstr == NULL) 406 lstr = RCS_HEAD_INIT; 407 if (rcsnum_aton(lstr, &ep, &lnum) == 0 || (*ep != '\0')) 408 errx(1, "invalid revision: %s", lstr); 409 410 if (rstr != NULL) { 411 if (rcsnum_aton(rstr, &ep, &rnum) == 0 || (*ep != '\0')) 412 errx(1, "invalid revision: %s", rstr); 413 } else 414 rcsnum_cpy(file->rf_head, &rnum, 0); 415 416 rcs_argv_destroy(revrange); 417 418 TAILQ_FOREACH(rdp, &file->rf_delta, rd_list) 419 if (rcsnum_cmp(rdp->rd_num, &lnum, 0) <= 0 && 420 rcsnum_cmp(rdp->rd_num, &rnum, 0) >= 0 && 421 !(rdp->rd_flags & RCS_RD_SELECT)) { 422 rdp->rd_flags |= RCS_RD_SELECT; 423 nrev++; 424 } 425 } 426 rcs_argv_destroy(revargv); 427 428 free(lnum.rn_id); 429 free(rnum.rn_id); 430 431 return (nrev); 432} 433 434/* 435 * Load description from <in> to <file>. 436 * If <in> starts with a `-', <in> is taken as the description. 437 * Otherwise <in> is the name of the file containing the description. 438 * If <in> is NULL, the description is read from stdin. 439 * Returns 0 on success, -1 on failure, setting errno. 440 */ 441int 442rcs_set_description(RCSFILE *file, const char *in, int flags) 443{ 444 BUF *bp; 445 char *content; 446 const char *prompt = 447 "enter description, terminated with single '.' or end of file:\n" 448 "NOTE: This is NOT the log message!\n"; 449 450 /* Description is in file <in>. */ 451 if (in != NULL && *in != '-') { 452 if ((bp = buf_load(in)) == NULL) 453 return (-1); 454 buf_putc(bp, '\0'); 455 content = buf_release(bp); 456 /* Description is in <in>. */ 457 } else if (in != NULL) 458 /* Skip leading `-'. */ 459 content = xstrdup(in + 1); 460 /* Get description from stdin. */ 461 else 462 content = rcs_prompt(prompt, flags); 463 464 rcs_desc_set(file, content); 465 free(content); 466 return (0); 467} 468 469/* 470 * Split the contents of a file into a list of lines. 471 */ 472struct rcs_lines * 473rcs_splitlines(u_char *data, size_t len) 474{ 475 u_char *c, *p; 476 struct rcs_lines *lines; 477 struct rcs_line *lp; 478 size_t i, tlen; 479 480 lines = xcalloc(1, sizeof(*lines)); 481 TAILQ_INIT(&(lines->l_lines)); 482 483 lp = xcalloc(1, sizeof(*lp)); 484 TAILQ_INSERT_TAIL(&(lines->l_lines), lp, l_list); 485 486 487 p = c = data; 488 for (i = 0; i < len; i++) { 489 if (*p == '\n' || (i == len - 1)) { 490 tlen = p - c + 1; 491 lp = xmalloc(sizeof(*lp)); 492 lp->l_line = c; 493 lp->l_len = tlen; 494 lp->l_lineno = ++(lines->l_nblines); 495 TAILQ_INSERT_TAIL(&(lines->l_lines), lp, l_list); 496 c = p + 1; 497 } 498 p++; 499 } 500 501 return (lines); 502} 503 504void 505rcs_freelines(struct rcs_lines *lines) 506{ 507 struct rcs_line *lp; 508 509 while ((lp = TAILQ_FIRST(&(lines->l_lines))) != NULL) { 510 TAILQ_REMOVE(&(lines->l_lines), lp, l_list); 511 free(lp); 512 } 513 514 free(lines); 515} 516 517BUF * 518rcs_patchfile(u_char *data, size_t dlen, u_char *patch, size_t plen, 519 int (*p)(struct rcs_lines *, struct rcs_lines *)) 520{ 521 struct rcs_lines *dlines, *plines; 522 struct rcs_line *lp; 523 BUF *res; 524 525 dlines = rcs_splitlines(data, dlen); 526 plines = rcs_splitlines(patch, plen); 527 528 if (p(dlines, plines) < 0) { 529 rcs_freelines(dlines); 530 rcs_freelines(plines); 531 return (NULL); 532 } 533 534 res = buf_alloc(1024); 535 TAILQ_FOREACH(lp, &dlines->l_lines, l_list) { 536 if (lp->l_line == NULL) 537 continue; 538 buf_append(res, lp->l_line, lp->l_len); 539 } 540 541 rcs_freelines(dlines); 542 rcs_freelines(plines); 543 return (res); 544} 545 546/* 547 * rcs_yesno() 548 * 549 * Read a char from standard input, returns defc if the 550 * user enters an equivalent to defc, else whatever char 551 * was entered. Converts input to lower case. 552 */ 553int 554rcs_yesno(int defc) 555{ 556 int c, ret; 557 558 fflush(stderr); 559 fflush(stdout); 560 561 clearerr(stdin); 562 if (isalpha(c = getchar())) 563 c = tolower(c); 564 if (c == defc || c == '\n' || (c == EOF && feof(stdin))) 565 ret = defc; 566 else 567 ret = c; 568 569 while (c != EOF && c != '\n') 570 c = getchar(); 571 572 return (ret); 573} 574 575/* 576 * rcs_strsplit() 577 * 578 * Split a string <str> of <sep>-separated values and allocate 579 * an argument vector for the values found. 580 */ 581struct rcs_argvector * 582rcs_strsplit(const char *str, const char *sep) 583{ 584 struct rcs_argvector *av; 585 size_t i = 0; 586 char *cp, *p; 587 588 cp = xstrdup(str); 589 av = xmalloc(sizeof(*av)); 590 av->str = cp; 591 av->argv = xmalloc(sizeof(*(av->argv))); 592 593 while ((p = strsep(&cp, sep)) != NULL) { 594 av->argv[i++] = p; 595 av->argv = xreallocarray(av->argv, 596 i + 1, sizeof(*(av->argv))); 597 } 598 av->argv[i] = NULL; 599 600 return (av); 601} 602 603/* 604 * rcs_argv_destroy() 605 * 606 * Free an argument vector previously allocated by rcs_strsplit(). 607 */ 608void 609rcs_argv_destroy(struct rcs_argvector *av) 610{ 611 free(av->str); 612 free(av->argv); 613 free(av); 614} 615 616/* 617 * Strip suffix from filename. 618 */ 619void 620rcs_strip_suffix(char *filename) 621{ 622 char *p, *suffixes, *next, *ext; 623 624 if ((p = strrchr(filename, ',')) != NULL) { 625 suffixes = xstrdup(rcs_suffixes); 626 for (next = suffixes; (ext = strsep(&next, "/")) != NULL;) { 627 if (!strcmp(p, ext)) { 628 *p = '\0'; 629 break; 630 } 631 } 632 free(suffixes); 633 } 634} 635