1156230Smux/*- 2156230Smux * Copyright (c) 2003-2006, Maxime Henrion <mux@FreeBSD.org> 3156230Smux * All rights reserved. 4156230Smux * 5156230Smux * Redistribution and use in source and binary forms, with or without 6156230Smux * modification, are permitted provided that the following conditions 7156230Smux * are met: 8156230Smux * 1. Redistributions of source code must retain the above copyright 9156230Smux * notice, this list of conditions and the following disclaimer. 10156230Smux * 2. Redistributions in binary form must reproduce the above copyright 11156230Smux * notice, this list of conditions and the following disclaimer in the 12156230Smux * documentation and/or other materials provided with the distribution. 13156230Smux * 14156230Smux * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15156230Smux * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16156230Smux * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17156230Smux * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 18156230Smux * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19156230Smux * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20156230Smux * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21156230Smux * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22156230Smux * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23156230Smux * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24156230Smux * SUCH DAMAGE. 25156230Smux * 26156230Smux * $FreeBSD$ 27156230Smux */ 28156230Smux 29156230Smux#include <sys/types.h> 30156230Smux#include <sys/stat.h> 31156230Smux 32156230Smux#include <assert.h> 33156230Smux#include <err.h> 34156230Smux#include <errno.h> 35156230Smux#include <fcntl.h> 36156701Smux#include <limits.h> 37156230Smux#include <pthread.h> 38156230Smux#include <stdarg.h> 39156230Smux#include <stdio.h> 40156230Smux#include <stdlib.h> 41156230Smux#include <string.h> 42156230Smux#include <time.h> 43156230Smux#include <unistd.h> 44156230Smux 45156230Smux#include "fattr.h" 46156230Smux#include "main.h" 47156230Smux#include "misc.h" 48156230Smux 49156230Smuxstruct pattlist { 50156230Smux char **patterns; 51156230Smux size_t size; 52156230Smux size_t in; 53156230Smux}; 54156230Smux 55156230Smuxstruct backoff_timer { 56156230Smux time_t min; 57156230Smux time_t max; 58156230Smux time_t interval; 59156230Smux float backoff; 60156230Smux float jitter; 61156230Smux}; 62156230Smux 63156230Smuxstatic void bt_update(struct backoff_timer *); 64156230Smuxstatic void bt_addjitter(struct backoff_timer *); 65156230Smux 66156230Smuxint 67156701Smuxasciitoint(const char *s, int *val, int base) 68156701Smux{ 69156701Smux char *end; 70156701Smux long longval; 71156701Smux 72156701Smux errno = 0; 73156701Smux longval = strtol(s, &end, base); 74156701Smux if (errno || *end != '\0') 75156701Smux return (-1); 76156701Smux if (longval > INT_MAX || longval < INT_MIN) { 77156701Smux errno = ERANGE; 78156701Smux return (-1); 79156701Smux } 80156701Smux *val = longval; 81156701Smux return (0); 82156701Smux} 83156701Smux 84156701Smuxint 85156230Smuxlprintf(int level, const char *fmt, ...) 86156230Smux{ 87156230Smux FILE *to; 88156230Smux va_list ap; 89156230Smux int ret; 90156230Smux 91156230Smux if (level > verbose) 92156230Smux return (0); 93156230Smux if (level == -1) 94156230Smux to = stderr; 95156230Smux else 96156230Smux to = stdout; 97156230Smux va_start(ap, fmt); 98156230Smux ret = vfprintf(to, fmt, ap); 99156230Smux va_end(ap); 100156230Smux fflush(to); 101156230Smux return (ret); 102156230Smux} 103156230Smux 104156230Smux/* 105156230Smux * Compute the MD5 checksum of a file. The md parameter must 106156230Smux * point to a buffer containing at least MD5_DIGEST_SIZE bytes. 107156230Smux * 108156230Smux * Do not confuse OpenSSL's MD5_DIGEST_LENGTH with our own 109156230Smux * MD5_DIGEST_SIZE macro. 110156230Smux */ 111156230Smuxint 112156230SmuxMD5_File(char *path, char *md) 113156230Smux{ 114156230Smux char buf[1024]; 115156230Smux MD5_CTX ctx; 116156230Smux ssize_t n; 117156230Smux int fd; 118156230Smux 119156230Smux fd = open(path, O_RDONLY); 120156230Smux if (fd == -1) 121156230Smux return (-1); 122156230Smux MD5_Init(&ctx); 123156230Smux while ((n = read(fd, buf, sizeof(buf))) > 0) 124156230Smux MD5_Update(&ctx, buf, n); 125156230Smux close(fd); 126156230Smux if (n == -1) 127156230Smux return (-1); 128156230Smux MD5_End(md, &ctx); 129156230Smux return (0); 130156230Smux} 131156230Smux 132156230Smux/* 133156230Smux * Wrapper around MD5_Final() that converts the 128 bits MD5 hash 134156230Smux * to an ASCII string representing this value in hexadecimal. 135156230Smux */ 136156230Smuxvoid 137156230SmuxMD5_End(char *md, MD5_CTX *c) 138156230Smux{ 139156230Smux unsigned char md5[MD5_DIGEST_LENGTH]; 140156230Smux const char hex[] = "0123456789abcdef"; 141156230Smux int i, j; 142156230Smux 143156230Smux MD5_Final(md5, c); 144156230Smux j = 0; 145156230Smux for (i = 0; i < MD5_DIGEST_LENGTH; i++) { 146156230Smux md[j++] = hex[md5[i] >> 4]; 147156230Smux md[j++] = hex[md5[i] & 0xf]; 148156230Smux } 149156230Smux md[j] = '\0'; 150156230Smux} 151156230Smux 152156230Smuxint 153156230Smuxpathcmp(const char *s1, const char *s2) 154156230Smux{ 155156230Smux char c1, c2; 156156230Smux 157156230Smux do { 158156230Smux c1 = *s1++; 159156230Smux if (c1 == '/') 160156230Smux c1 = 1; 161156230Smux c2 = *s2++; 162156230Smux if (c2 == '/') 163156230Smux c2 = 1; 164156230Smux } while (c1 == c2 && c1 != '\0'); 165156230Smux 166156230Smux return (c1 - c2); 167156230Smux} 168156230Smux 169156230Smuxsize_t 170156230Smuxcommonpathlength(const char *a, size_t alen, const char *b, size_t blen) 171156230Smux{ 172156230Smux size_t i, minlen, lastslash; 173156230Smux 174156230Smux minlen = min(alen, blen); 175156230Smux lastslash = 0; 176156230Smux for (i = 0; i < minlen; i++) { 177156230Smux if (a[i] != b[i]) 178156230Smux return (lastslash); 179156230Smux if (a[i] == '/') { 180156230Smux if (i == 0) /* Include the leading slash. */ 181156230Smux lastslash = 1; 182156230Smux else 183156230Smux lastslash = i; 184156230Smux } 185156230Smux } 186156230Smux 187156230Smux /* One path is a prefix of the other/ */ 188156230Smux if (alen > minlen) { /* Path "b" is a prefix of "a". */ 189156230Smux if (a[minlen] == '/') 190156230Smux return (minlen); 191156230Smux else 192156230Smux return (lastslash); 193156230Smux } else if (blen > minlen) { /* Path "a" is a prefix of "b". */ 194156230Smux if (b[minlen] == '/') 195156230Smux return (minlen); 196156230Smux else 197156230Smux return (lastslash); 198156230Smux } 199156230Smux 200156230Smux /* The paths are identical. */ 201156230Smux return (minlen); 202156230Smux} 203156230Smux 204186781Slulfconst char * 205186781Slulfpathlast(const char *path) 206156230Smux{ 207186781Slulf const char *s; 208156230Smux 209156230Smux s = strrchr(path, '/'); 210156230Smux if (s == NULL) 211156230Smux return (path); 212156230Smux return (++s); 213156230Smux} 214156230Smux 215156230Smuxint 216156230Smuxrcsdatetotm(const char *revdate, struct tm *tm) 217156230Smux{ 218156230Smux char *cp; 219156230Smux size_t len; 220156230Smux 221156230Smux cp = strchr(revdate, '.'); 222156230Smux if (cp == NULL) 223156230Smux return (-1); 224156230Smux len = cp - revdate; 225156251Smux if (len >= 4) 226156230Smux cp = strptime(revdate, "%Y.%m.%d.%H.%M.%S", tm); 227156230Smux else if (len == 2) 228156230Smux cp = strptime(revdate, "%y.%m.%d.%H.%M.%S", tm); 229156230Smux else 230156230Smux return (-1); 231156230Smux if (cp == NULL || *cp != '\0') 232156230Smux return (-1); 233156230Smux return (0); 234156230Smux} 235156230Smux 236156230Smuxtime_t 237156230Smuxrcsdatetotime(const char *revdate) 238156230Smux{ 239156230Smux struct tm tm; 240156230Smux time_t t; 241156230Smux int error; 242156230Smux 243156230Smux error = rcsdatetotm(revdate, &tm); 244156230Smux if (error) 245156230Smux return (error); 246156230Smux t = timegm(&tm); 247156230Smux return (t); 248156230Smux} 249156230Smux 250156230Smux/* 251186781Slulf * Checks if a file is an RCS file. 252186781Slulf */ 253186781Slulfint 254186781Slulfisrcs(const char *file, size_t *len) 255186781Slulf{ 256186781Slulf const char *cp; 257186781Slulf 258186781Slulf if (file[0] == '/') 259186781Slulf return (0); 260186781Slulf cp = file; 261186781Slulf while ((cp = strstr(cp, "..")) != NULL) { 262186781Slulf if (cp == file || cp[2] == '\0' || 263186781Slulf (cp[-1] == '/' && cp[2] == '/')) 264186781Slulf return (0); 265186781Slulf cp += 2; 266186781Slulf } 267186781Slulf *len = strlen(file); 268186781Slulf if (*len < 2 || file[*len - 1] != 'v' || file[*len - 2] != ',') { 269186781Slulf return (0); 270186781Slulf } 271186781Slulf 272186781Slulf return (1); 273186781Slulf} 274186781Slulf 275186781Slulf/* 276156230Smux * Returns a buffer allocated with malloc() containing the absolute 277156230Smux * pathname to the checkout file made from the prefix and the path 278156230Smux * of the corresponding RCS file relatively to the prefix. If the 279156230Smux * filename is not an RCS filename, NULL will be returned. 280156230Smux */ 281156230Smuxchar * 282156230Smuxcheckoutpath(const char *prefix, const char *file) 283156230Smux{ 284156230Smux char *path; 285156230Smux size_t len; 286156230Smux 287186781Slulf if (!isrcs(file, &len)) 288156230Smux return (NULL); 289156230Smux xasprintf(&path, "%s/%.*s", prefix, (int)len - 2, file); 290156230Smux return (path); 291156230Smux} 292156230Smux 293186781Slulf/* 294186781Slulf * Returns a cvs path allocated with malloc() containing absolute pathname to a 295186781Slulf * file in cvs mode which can reside in the attic. XXX: filename has really no 296186781Slulf * restrictions. 297186781Slulf */ 298186781Slulfchar * 299186781Slulfcvspath(const char *prefix, const char *file, int attic) 300186781Slulf{ 301186781Slulf const char *last; 302186781Slulf char *path; 303186781Slulf 304186781Slulf last = pathlast(file); 305186781Slulf if (attic) 306186781Slulf xasprintf(&path, "%s/%.*sAttic/%s", prefix, (int)(last - file), 307186781Slulf file, last); 308186781Slulf else 309186781Slulf xasprintf(&path, "%s/%s", prefix, file); 310186781Slulf 311186781Slulf return (path); 312186781Slulf} 313186781Slulf 314186781Slulf/* 315186781Slulf * Regular or attic path if regular fails. 316186781Slulf * XXX: This should perhaps also check if the Attic file exists too, and return 317186781Slulf * NULL if not. 318186781Slulf */ 319186781Slulfchar * 320186781Slulfatticpath(const char *prefix, const char *file) 321186781Slulf{ 322186781Slulf char *path; 323186781Slulf 324186781Slulf path = cvspath(prefix, file, 0); 325186781Slulf if (access(path, F_OK) != 0) { 326186781Slulf free(path); 327186781Slulf path = cvspath(prefix, file, 1); 328186781Slulf } 329186781Slulf return (path); 330186781Slulf} 331186781Slulf 332156230Smuxint 333156230Smuxmkdirhier(char *path, mode_t mask) 334156230Smux{ 335156230Smux struct fattr *fa; 336156230Smux size_t i, last, len; 337156230Smux int error, finish, rv; 338156230Smux 339156230Smux finish = 0; 340156230Smux last = 0; 341156230Smux len = strlen(path); 342156230Smux for (i = len - 1; i > 0; i--) { 343156230Smux if (path[i] == '/') { 344156230Smux path[i] = '\0'; 345156230Smux if (access(path, F_OK) == 0) { 346156230Smux path[i] = '/'; 347156230Smux break; 348156230Smux } 349156230Smux if (errno != ENOENT) { 350156230Smux path[i] = '/'; 351156230Smux if (last == 0) 352156230Smux return (-1); 353156230Smux finish = 1; 354156230Smux break; 355156230Smux } 356156230Smux last = i; 357156230Smux } 358156230Smux } 359156230Smux if (last == 0) 360156230Smux return (0); 361156230Smux 362156230Smux i = strlen(path); 363156230Smux fa = fattr_new(FT_DIRECTORY, -1); 364156230Smux fattr_mergedefault(fa); 365156230Smux fattr_umask(fa, mask); 366156230Smux while (i < len) { 367156230Smux if (!finish) { 368156230Smux rv = 0; 369156230Smux error = fattr_makenode(fa, path); 370156230Smux if (!error) 371156230Smux rv = fattr_install(fa, path, NULL); 372156230Smux if (error || rv == -1) 373156230Smux finish = 1; 374156230Smux } 375156230Smux path[i] = '/'; 376156230Smux i += strlen(path + i); 377156230Smux } 378156230Smux assert(i == len); 379156230Smux if (finish) 380156230Smux return (-1); 381156230Smux return (0); 382156230Smux} 383156230Smux 384156230Smux/* 385156230Smux * Compute temporary pathnames. 386156230Smux * This can look a bit like overkill but we mimic CVSup's behaviour. 387156230Smux */ 388156230Smux#define TEMPNAME_PREFIX "#cvs.csup" 389156230Smux 390156230Smuxstatic pthread_mutex_t tempname_mtx = PTHREAD_MUTEX_INITIALIZER; 391156230Smuxstatic pid_t tempname_pid = -1; 392156230Smuxstatic int tempname_count; 393156230Smux 394156230Smuxchar * 395156230Smuxtempname(const char *path) 396156230Smux{ 397156230Smux char *cp, *temp; 398156230Smux int count, error; 399156230Smux 400156230Smux error = pthread_mutex_lock(&tempname_mtx); 401156230Smux assert(!error); 402156230Smux if (tempname_pid == -1) { 403156230Smux tempname_pid = getpid(); 404156230Smux tempname_count = 0; 405156230Smux } 406156230Smux count = tempname_count++; 407156230Smux error = pthread_mutex_unlock(&tempname_mtx); 408156230Smux assert(!error); 409156230Smux cp = strrchr(path, '/'); 410156230Smux if (cp == NULL) 411156230Smux xasprintf(&temp, "%s-%ld.%d", TEMPNAME_PREFIX, 412156230Smux (long)tempname_pid, count); 413156230Smux else 414156230Smux xasprintf(&temp, "%.*s%s-%ld.%d", (int)(cp - path + 1), path, 415156230Smux TEMPNAME_PREFIX, (long)tempname_pid, count); 416156230Smux return (temp); 417156230Smux} 418156230Smux 419156230Smuxvoid * 420156230Smuxxmalloc(size_t size) 421156230Smux{ 422156230Smux void *buf; 423156230Smux 424156230Smux buf = malloc(size); 425156230Smux if (buf == NULL) 426156230Smux err(1, "malloc"); 427156230Smux return (buf); 428156230Smux} 429156230Smux 430156230Smuxvoid * 431156230Smuxxrealloc(void *buf, size_t size) 432156230Smux{ 433156230Smux 434156230Smux buf = realloc(buf, size); 435156230Smux if (buf == NULL) 436156230Smux err(1, "realloc"); 437156230Smux return (buf); 438156230Smux} 439156230Smux 440156230Smuxchar * 441156230Smuxxstrdup(const char *str) 442156230Smux{ 443156230Smux char *buf; 444156230Smux 445156230Smux buf = strdup(str); 446156230Smux if (buf == NULL) 447156230Smux err(1, "strdup"); 448156230Smux return (buf); 449156230Smux} 450156230Smux 451156230Smuxint 452156230Smuxxasprintf(char **ret, const char *format, ...) 453156230Smux{ 454156230Smux va_list ap; 455156230Smux int rv; 456156230Smux 457156230Smux va_start(ap, format); 458156230Smux rv = vasprintf(ret, format, ap); 459156230Smux va_end(ap); 460156230Smux if (*ret == NULL) 461156230Smux err(1, "asprintf"); 462156230Smux return (rv); 463156230Smux} 464156230Smux 465156230Smuxstruct pattlist * 466156230Smuxpattlist_new(void) 467156230Smux{ 468156230Smux struct pattlist *p; 469156230Smux 470156230Smux p = xmalloc(sizeof(struct pattlist)); 471156230Smux p->size = 4; /* Initial size. */ 472156230Smux p->patterns = xmalloc(p->size * sizeof(char *)); 473156230Smux p->in = 0; 474156230Smux return (p); 475156230Smux} 476156230Smux 477156230Smuxvoid 478156230Smuxpattlist_add(struct pattlist *p, const char *pattern) 479156230Smux{ 480156230Smux 481156230Smux if (p->in == p->size) { 482156230Smux p->size *= 2; 483156230Smux p->patterns = xrealloc(p->patterns, p->size * sizeof(char *)); 484156230Smux } 485156230Smux assert(p->in < p->size); 486156230Smux p->patterns[p->in++] = xstrdup(pattern); 487156230Smux} 488156230Smux 489156230Smuxchar * 490156230Smuxpattlist_get(struct pattlist *p, size_t i) 491156230Smux{ 492156230Smux 493156230Smux assert(i < p->in); 494156230Smux return (p->patterns[i]); 495156230Smux} 496156230Smux 497156230Smuxsize_t 498156230Smuxpattlist_size(struct pattlist *p) 499156230Smux{ 500156230Smux 501156230Smux return (p->in); 502156230Smux} 503156230Smux 504156230Smuxvoid 505156230Smuxpattlist_free(struct pattlist *p) 506156230Smux{ 507156230Smux size_t i; 508156230Smux 509156230Smux for (i = 0; i < p->in; i++) 510156230Smux free(p->patterns[i]); 511156230Smux free(p->patterns); 512156230Smux free(p); 513156230Smux} 514156230Smux 515156230Smux/* Creates a backoff timer. */ 516156230Smuxstruct backoff_timer * 517156230Smuxbt_new(time_t min, time_t max, float backoff, float jitter) 518156230Smux{ 519156230Smux struct backoff_timer *bt; 520156230Smux 521156230Smux bt = xmalloc(sizeof(struct backoff_timer)); 522156230Smux bt->min = min; 523156230Smux bt->max = max; 524156230Smux bt->backoff = backoff; 525156230Smux bt->jitter = jitter; 526156230Smux bt->interval = min; 527156230Smux bt_addjitter(bt); 528156230Smux srandom(time(0)); 529156230Smux return (bt); 530156230Smux} 531156230Smux 532156230Smux/* Updates the backoff timer. */ 533156230Smuxstatic void 534156230Smuxbt_update(struct backoff_timer *bt) 535156230Smux{ 536156230Smux 537156230Smux bt->interval = (time_t)min(bt->interval * bt->backoff, bt->max); 538156230Smux bt_addjitter(bt); 539156230Smux} 540156230Smux 541156230Smux/* Adds some jitter. */ 542156230Smuxstatic void 543156230Smuxbt_addjitter(struct backoff_timer *bt) 544156230Smux{ 545156230Smux long mag; 546156230Smux 547156230Smux mag = (long)(bt->jitter * bt->interval); 548156230Smux /* We want a random number between -mag and mag. */ 549156230Smux bt->interval += (time_t)(random() % (2 * mag) - mag); 550156230Smux} 551156230Smux 552156230Smux/* Returns the current timer value. */ 553156230Smuxtime_t 554156230Smuxbt_get(struct backoff_timer *bt) 555156230Smux{ 556156230Smux 557156230Smux return (bt->interval); 558156230Smux} 559156230Smux 560156230Smux/* Times out for bt->interval seconds. */ 561156230Smuxvoid 562156230Smuxbt_pause(struct backoff_timer *bt) 563156230Smux{ 564156230Smux 565156230Smux sleep(bt->interval); 566156230Smux bt_update(bt); 567156230Smux} 568156230Smux 569156230Smuxvoid 570156230Smuxbt_free(struct backoff_timer *bt) 571156230Smux{ 572156230Smux 573156230Smux free(bt); 574156230Smux} 575186781Slulf 576186781Slulf/* Compare two revisions. */ 577186781Slulfint 578186781Slulfrcsnum_cmp(char *revision1, char *revision2) 579186781Slulf{ 580186781Slulf char *ptr1, *ptr2, *dot1, *dot2; 581186781Slulf int num1len, num2len, ret; 582186781Slulf 583186781Slulf ptr1 = revision1; 584186781Slulf ptr2 = revision2; 585186781Slulf while (*ptr1 != '\0' && *ptr2 != '\0') { 586186781Slulf dot1 = strchr(ptr1, '.'); 587186781Slulf dot2 = strchr(ptr2, '.'); 588186781Slulf if (dot1 == NULL) 589186781Slulf dot1 = strchr(ptr1, '\0'); 590186781Slulf if (dot2 == NULL) 591186781Slulf dot2 = strchr(ptr2, '\0'); 592186781Slulf 593186781Slulf num1len = dot1 - ptr1; 594186781Slulf num2len = dot2 - ptr2; 595186781Slulf /* Check the distance between each, showing how many digits */ 596186781Slulf if (num1len > num2len) 597186781Slulf return (1); 598186781Slulf else if (num1len < num2len) 599186781Slulf return (-1); 600186781Slulf 601186781Slulf /* Equal distance means we must check each character. */ 602186781Slulf ret = strncmp(ptr1, ptr2, num1len); 603186781Slulf if (ret != 0) 604186781Slulf return (ret); 605186781Slulf ptr1 = (*dot1 == '.') ? (dot1 + 1) : dot1; 606186781Slulf ptr2 = (*dot2 == '.') ? (dot2 + 1) : dot2; 607186781Slulf } 608186781Slulf 609186781Slulf if (*ptr1 != '\0' && *ptr2 == '\0') 610186781Slulf return (1); 611186781Slulf if (*ptr1 == '\0' && *ptr2 != '\0') 612186781Slulf return (-1); 613186781Slulf return (0); 614186781Slulf 615186781Slulf} 616186781Slulf 617186781Slulf/* Returns 0 if a rcsrev is not a trunk revision number. */ 618186781Slulfint 619186781Slulfrcsrev_istrunk(char *revnum) 620186781Slulf{ 621186781Slulf char *tmp; 622186781Slulf 623186781Slulf tmp = strchr(revnum, '.'); 624186781Slulf tmp++; 625186781Slulf if (strchr(tmp, '.') != NULL) 626186781Slulf return (0); 627186781Slulf return (1); 628186781Slulf} 629186781Slulf 630186781Slulf/* Return prefix of rcsfile. */ 631186781Slulfchar * 632186781Slulfrcsrev_prefix(char *revnum) 633186781Slulf{ 634186781Slulf char *modrev, *pos; 635186781Slulf 636186781Slulf modrev = xstrdup(revnum); 637186781Slulf pos = strrchr(modrev, '.'); 638186781Slulf if (pos == NULL) { 639186781Slulf free(modrev); 640186781Slulf return (NULL); 641186781Slulf } 642186781Slulf *pos = '\0'; 643186781Slulf return (modrev); 644186781Slulf} 645