1 # include <stdio.h> 2 # include <sys/types.h> 3 # include <sys/stat.h> 4 # include <sys/dir.h> 5 # include <sysexits.h> 6 # include <whoami.h> 7 8 /* 9 ** SCCS.C -- human-oriented front end to the SCCS system. 10 ** 11 ** Without trying to add any functionality to speak of, this 12 ** program tries to make SCCS a little more accessible to human 13 ** types. The main thing it does is automatically put the 14 ** string "SCCS/s." on the front of names. Also, it has a 15 ** couple of things that are designed to shorten frequent 16 ** combinations, e.g., "delget" which expands to a "delta" 17 ** and a "get". 18 ** 19 ** This program can also function as a setuid front end. 20 ** To do this, you should copy the source, renaming it to 21 ** whatever you want, e.g., "syssccs". Change any defaults 22 ** in the program (e.g., syssccs might default -d to 23 ** "/usr/src/sys"). Then recompile and put the result 24 ** as setuid to whomever you want. In this mode, sccs 25 ** knows to not run setuid for certain programs in order 26 ** to preserve security, and so forth. 27 ** 28 ** Usage: 29 ** sccs [flags] command [args] 30 ** 31 ** Flags: 32 ** -d<dir> <dir> represents a directory to search 33 ** out of. It should be a full pathname 34 ** for general usage. E.g., if <dir> is 35 ** "/usr/src/sys", then a reference to the 36 ** file "dev/bio.c" becomes a reference to 37 ** "/usr/src/sys/dev/bio.c". 38 ** -p<path> prepends <path> to the final component 39 ** of the pathname. By default, this is 40 ** "SCCS". For example, in the -d example 41 ** above, the path then gets modified to 42 ** "/usr/src/sys/dev/SCCS/s.bio.c". In 43 ** more common usage (without the -d flag), 44 ** "prog.c" would get modified to 45 ** "SCCS/s.prog.c". In both cases, the 46 ** "s." gets automatically prepended. 47 ** -r run as the real user. 48 ** 49 ** Commands: 50 ** admin, 51 ** get, 52 ** delta, 53 ** rmdel, 54 ** chghist, 55 ** etc. Straight out of SCCS; only difference 56 ** is that pathnames get modified as 57 ** described above. 58 ** edit Macro for "get -e". 59 ** unedit Removes a file being edited, knowing 60 ** about p-files, etc. 61 ** delget Macro for "delta" followed by "get". 62 ** deledit Macro for "delta" followed by "get -e". 63 ** info Tell what files being edited. 64 ** clean Remove all files that can be 65 ** regenerated from SCCS files. 66 ** status Like info, but return exit status, for 67 ** use in makefiles. 68 ** fix Remove a top delta & reedit, but save 69 ** the previous changes in that delta. 70 ** 71 ** Compilation Flags: 72 ** UIDUSER -- determine who the user is by looking at the 73 ** uid rather than the login name -- for machines 74 ** where SCCS gets the user in this way. 75 ** 76 ** Compilation Instructions: 77 ** cc -O -n -s sccs.c 78 ** 79 ** Author: 80 ** Eric Allman, UCB/INGRES 81 */ 82 83 # ifdef CSVAX 84 # define UIDUSER 85 # endif 86 87 static char SccsId[] = "@(#)sccs.c 1.26 09/02/80"; 88 89 # define bitset(bit, word) ((bit) & (word)) 90 91 typedef char bool; 92 # define TRUE 1 93 # define FALSE 0 94 95 # ifdef UIDUSER 96 # include <pwd.h> 97 # endif UIDUSER 98 99 struct sccsprog 100 { 101 char *sccsname; /* name of SCCS routine */ 102 short sccsoper; /* opcode, see below */ 103 short sccsflags; /* flags, see below */ 104 char *sccspath; /* pathname of binary implementing */ 105 }; 106 107 /* values for sccsoper */ 108 # define PROG 0 /* call a program */ 109 # define CMACRO 1 /* command substitution macro */ 110 # define FIX 2 /* fix a delta */ 111 # define CLEAN 3 /* clean out recreatable files */ 112 # define UNEDIT 4 /* unedit a file */ 113 114 /* bits for sccsflags */ 115 # define NO_SDOT 0001 /* no s. on front of args */ 116 # define REALUSER 0002 /* protected (e.g., admin) */ 117 118 /* modes for the "clean", "info", "check" ops */ 119 # define CLEANC 0 /* clean command */ 120 # define INFOC 1 /* info command */ 121 # define CHECKC 2 /* check command */ 122 123 # ifdef CSVAX 124 # define PROGPATH(name) "/usr/local/name" 125 # endif CSVAX 126 127 # ifndef PROGPATH 128 # define PROGPATH(name) "/usr/sccs/name" 129 # endif PROGPATH 130 131 struct sccsprog SccsProg[] = 132 { 133 "admin", PROG, REALUSER, PROGPATH(admin), 134 "chghist", PROG, 0, PROGPATH(rmdel), 135 "comb", PROG, 0, PROGPATH(comb), 136 "delta", PROG, 0, PROGPATH(delta), 137 "get", PROG, 0, PROGPATH(get), 138 "help", PROG, NO_SDOT, PROGPATH(help), 139 "prt", PROG, 0, PROGPATH(prt), 140 "rmdel", PROG, REALUSER, PROGPATH(rmdel), 141 "what", PROG, NO_SDOT, PROGPATH(what), 142 "edit", CMACRO, 0, "get -e", 143 "delget", CMACRO, 0, "delta/get", 144 "deledit", CMACRO, 0, "delta/get -e", 145 "fix", FIX, 0, NULL, 146 "clean", CLEAN, REALUSER, (char *) CLEANC, 147 "info", CLEAN, REALUSER, (char *) INFOC, 148 "check", CLEAN, REALUSER, (char *) CHECKC, 149 "unedit", UNEDIT, 0, NULL, 150 NULL, -1, 0, NULL 151 }; 152 153 struct pfile 154 { 155 char *p_osid; /* old SID */ 156 char *p_nsid; /* new SID */ 157 char *p_user; /* user who did edit */ 158 char *p_date; /* date of get */ 159 char *p_time; /* time of get */ 160 }; 161 162 char *SccsPath = "SCCS"; /* pathname of SCCS files */ 163 char *SccsDir = ""; /* directory to begin search from */ 164 bool RealUser; /* if set, running as real user */ 165 # ifdef DEBUG 166 bool Debug; /* turn on tracing */ 167 # endif 168 169 main(argc, argv) 170 int argc; 171 char **argv; 172 { 173 register char *p; 174 extern struct sccsprog *lookup(); 175 176 /* 177 ** Detect and decode flags intended for this program. 178 */ 179 180 if (argc < 2) 181 { 182 fprintf(stderr, "Usage: sccs [flags] command [flags]\n"); 183 exit(EX_USAGE); 184 } 185 argv[argc] = NULL; 186 187 if (lookup(argv[0]) == NULL) 188 { 189 while ((p = *++argv) != NULL) 190 { 191 if (*p != '-') 192 break; 193 switch (*++p) 194 { 195 case 'r': /* run as real user */ 196 setuid(getuid()); 197 RealUser++; 198 break; 199 200 case 'p': /* path of sccs files */ 201 SccsPath = ++p; 202 break; 203 204 case 'd': /* directory to search from */ 205 SccsDir = ++p; 206 break; 207 208 # ifdef DEBUG 209 case 'T': /* trace */ 210 Debug++; 211 break; 212 # endif 213 214 default: 215 fprintf(stderr, "Sccs: unknown option -%s\n", p); 216 break; 217 } 218 } 219 if (SccsPath[0] == '\0') 220 SccsPath = "."; 221 } 222 223 command(argv, FALSE); 224 exit(EX_OK); 225 } 226 227 command(argv, forkflag) 228 char **argv; 229 bool forkflag; 230 { 231 register struct sccsprog *cmd; 232 register char *p; 233 register char *q; 234 char buf[40]; 235 extern struct sccsprog *lookup(); 236 char *nav[200]; 237 char **avp; 238 register int i; 239 extern bool unedit(); 240 241 # ifdef DEBUG 242 if (Debug) 243 { 244 printf("command:\n"); 245 for (avp = argv; *avp != NULL; avp++) 246 printf(" \"%s\"\n", *avp); 247 } 248 # endif 249 250 /* 251 ** Look up command. 252 ** At this point, argv points to the command name. 253 */ 254 255 cmd = lookup(argv[0]); 256 if (cmd == NULL) 257 { 258 fprintf(stderr, "Sccs: Unknown command \"%s\"\n", argv[0]); 259 exit(EX_USAGE); 260 } 261 262 /* 263 ** Interpret operation associated with this command. 264 */ 265 266 switch (cmd->sccsoper) 267 { 268 case PROG: /* call an sccs prog */ 269 callprog(cmd->sccspath, cmd->sccsflags, argv, forkflag); 270 break; 271 272 case CMACRO: /* command macro */ 273 for (p = cmd->sccspath; *p != '\0'; p++) 274 { 275 avp = nav; 276 *avp++ = buf; 277 for (q = buf; *p != '/' && *p != '\0'; p++, q++) 278 { 279 if (*p == ' ') 280 { 281 *q = '\0'; 282 *avp++ = &q[1]; 283 } 284 else 285 *q = *p; 286 } 287 *q = '\0'; 288 *avp = NULL; 289 xcommand(&argv[1], *p != '\0', nav[0], nav[1], nav[2], 290 nav[3], nav[4], nav[5], nav[6]); 291 } 292 fprintf(stderr, "Sccs internal error: CMACRO\n"); 293 exit(EX_SOFTWARE); 294 295 case FIX: /* fix a delta */ 296 if (strncmp(argv[1], "-r", 2) != 0) 297 { 298 fprintf(stderr, "Sccs: -r flag needed for fix command\n"); 299 break; 300 } 301 xcommand(&argv[1], TRUE, "get", "-k", NULL); 302 xcommand(&argv[1], TRUE, "rmdel", NULL); 303 xcommand(&argv[2], FALSE, "get", "-e", "-g", NULL); 304 fprintf(stderr, "Sccs internal error: FIX\n"); 305 exit(EX_SOFTWARE); 306 307 case CLEAN: 308 clean((int) cmd->sccspath); 309 break; 310 311 case UNEDIT: 312 i = 0; 313 for (avp = &argv[1]; *avp != NULL; avp++) 314 { 315 if (unedit(*avp)) 316 nav[i++] = *avp; 317 } 318 nav[i] = NULL; 319 if (i > 0) 320 xcommand(nav, FALSE, "get", NULL); 321 break; 322 323 default: 324 fprintf(stderr, "Sccs internal error: oper %d\n", cmd->sccsoper); 325 exit(EX_SOFTWARE); 326 } 327 } 328 /* 329 ** LOOKUP -- look up an SCCS command name. 330 ** 331 ** Parameters: 332 ** name -- the name of the command to look up. 333 ** 334 ** Returns: 335 ** ptr to command descriptor for this command. 336 ** NULL if no such entry. 337 ** 338 ** Side Effects: 339 ** none. 340 */ 341 342 struct sccsprog * 343 lookup(name) 344 char *name; 345 { 346 register struct sccsprog *cmd; 347 348 for (cmd = SccsProg; cmd->sccsname != NULL; cmd++) 349 { 350 if (strcmp(cmd->sccsname, name) == 0) 351 return (cmd); 352 } 353 return (NULL); 354 } 355 356 357 xcommand(argv, forkflag, arg0) 358 char **argv; 359 bool forkflag; 360 char *arg0; 361 { 362 register char **av; 363 char *newargv[1000]; 364 register char **np; 365 366 np = newargv; 367 for (av = &arg0; *av != NULL; av++) 368 *np++ = *av; 369 for (av = argv; *av != NULL; av++) 370 *np++ = *av; 371 *np = NULL; 372 command(newargv, forkflag); 373 } 374 375 callprog(progpath, flags, argv, forkflag) 376 char *progpath; 377 short flags; 378 char **argv; 379 bool forkflag; 380 { 381 register char *p; 382 register char **av; 383 extern char *makefile(); 384 register int i; 385 auto int st; 386 register char **nav; 387 388 if (*argv == NULL) 389 return (-1); 390 391 /* 392 ** Fork if appropriate. 393 */ 394 395 if (forkflag) 396 { 397 # ifdef DEBUG 398 if (Debug) 399 printf("Forking\n"); 400 # endif 401 i = fork(); 402 if (i < 0) 403 { 404 fprintf(stderr, "Sccs: cannot fork"); 405 exit(EX_OSERR); 406 } 407 else if (i > 0) 408 { 409 wait(&st); 410 return (st); 411 } 412 } 413 414 /* 415 ** Build new argument vector. 416 */ 417 418 /* copy program filename arguments and flags */ 419 nav = &argv[1]; 420 av = argv; 421 while ((p = *++av) != NULL) 422 { 423 if (!bitset(NO_SDOT, flags) && *p != '-') 424 *nav = makefile(p); 425 else 426 *nav = p; 427 if (*nav != NULL) 428 nav++; 429 } 430 *nav = NULL; 431 432 /* 433 ** Set protection as appropriate. 434 */ 435 436 if (bitset(REALUSER, flags)) 437 setuid(getuid()); 438 439 /* 440 ** Call real SCCS program. 441 */ 442 443 execv(progpath, argv); 444 fprintf(stderr, "Sccs: cannot execute "); 445 perror(progpath); 446 exit(EX_UNAVAILABLE); 447 } 448 /* 449 ** MAKEFILE -- make filename of SCCS file 450 ** 451 ** If the name passed is already the name of an SCCS file, 452 ** just return it. Otherwise, munge the name into the name 453 ** of the actual SCCS file. 454 ** 455 ** There are cases when it is not clear what you want to 456 ** do. For example, if SccsPath is an absolute pathname 457 ** and the name given is also an absolute pathname, we go 458 ** for SccsPath (& only use the last component of the name 459 ** passed) -- this is important for security reasons (if 460 ** sccs is being used as a setuid front end), but not 461 ** particularly intuitive. 462 ** 463 ** Parameters: 464 ** name -- the file name to be munged. 465 ** 466 ** Returns: 467 ** The pathname of the sccs file. 468 ** NULL on error. 469 ** 470 ** Side Effects: 471 ** none. 472 */ 473 474 char * 475 makefile(name) 476 char *name; 477 { 478 register char *p; 479 register char c; 480 char buf[512]; 481 struct stat stbuf; 482 extern char *malloc(); 483 extern char *rindex(); 484 extern bool safepath(); 485 extern bool isdir(); 486 register char *q; 487 488 p = rindex(name, '/'); 489 if (p == NULL) 490 p = name; 491 else 492 p++; 493 494 /* 495 ** Check to see that the path is "safe", i.e., that we 496 ** are not letting some nasty person use the setuid part 497 ** of this program to look at or munge some presumably 498 ** hidden files. 499 */ 500 501 if (SccsDir[0] == '/' && !safepath(name)) 502 return (NULL); 503 504 /* 505 ** Create the base pathname. 506 */ 507 508 if (SccsDir[0] != '\0' && name[0] != '/' && strncmp(name, "./", 2) != 0) 509 { 510 strcpy(buf, SccsDir); 511 strcat(buf, "/"); 512 } 513 else 514 strcpy(buf, ""); 515 strncat(buf, name, p - name); 516 q = &buf[strlen(buf)]; 517 strcpy(q, p); 518 if (strncmp(p, "s.", 2) != 0 && !isdir(buf)) 519 { 520 strcpy(q, SccsPath); 521 strcat(buf, "/s."); 522 strcat(buf, p); 523 } 524 525 if (strcmp(buf, name) == 0) 526 p = name; 527 else 528 { 529 p = malloc(strlen(buf) + 1); 530 if (p == NULL) 531 { 532 perror("Sccs: no mem"); 533 exit(EX_OSERR); 534 } 535 strcpy(p, buf); 536 } 537 return (p); 538 } 539 /* 540 ** ISDIR -- return true if the argument is a directory. 541 ** 542 ** Parameters: 543 ** name -- the pathname of the file to check. 544 ** 545 ** Returns: 546 ** TRUE if 'name' is a directory, FALSE otherwise. 547 ** 548 ** Side Effects: 549 ** none. 550 */ 551 552 bool 553 isdir(name) 554 char *name; 555 { 556 struct stat stbuf; 557 558 return (stat(name, &stbuf) >= 0 && (stbuf.st_mode & S_IFMT) == S_IFDIR); 559 } 560 /* 561 ** SAFEPATH -- determine whether a pathname is "safe" 562 ** 563 ** "Safe" pathnames only allow you to get deeper into the 564 ** directory structure, i.e., full pathnames and ".." are 565 ** not allowed. 566 ** 567 ** Parameters: 568 ** p -- the name to check. 569 ** 570 ** Returns: 571 ** TRUE -- if the path is safe. 572 ** FALSE -- if the path is not safe. 573 ** 574 ** Side Effects: 575 ** Prints a message if the path is not safe. 576 */ 577 578 bool 579 safepath(p) 580 register char *p; 581 { 582 extern char *index(); 583 584 if (*p != '/') 585 { 586 while (strncmp(p, "../", 3) != 0 && strcmp(p, "..") != 0) 587 { 588 p = index(p, '/'); 589 if (p == NULL) 590 return (TRUE); 591 p++; 592 } 593 } 594 595 printf("You may not use full pathnames or \"..\"\n"); 596 return (FALSE); 597 } 598 /* 599 ** CLEAN -- clean out recreatable files 600 ** 601 ** Any file for which an "s." file exists but no "p." file 602 ** exists in the current directory is purged. 603 ** 604 ** Parameters: 605 ** tells whether this came from a "clean", "info", or 606 ** "check" command. 607 ** 608 ** Returns: 609 ** none. 610 ** 611 ** Side Effects: 612 ** Removes files in the current directory. 613 ** Prints information regarding files being edited. 614 ** Exits if a "check" command. 615 */ 616 617 clean(mode) 618 int mode; 619 { 620 struct direct dir; 621 struct stat stbuf; 622 char buf[100]; 623 char pline[120]; 624 register FILE *dirfd; 625 register char *basefile; 626 bool gotedit; 627 FILE *pfp; 628 629 dirfd = fopen(SccsPath, "r"); 630 if (dirfd == NULL) 631 { 632 fprintf(stderr, "Sccs: cannot open %s\n", SccsPath); 633 return; 634 } 635 636 /* 637 ** Scan the SCCS directory looking for s. files. 638 */ 639 640 gotedit = FALSE; 641 while (fread(&dir, sizeof dir, 1, dirfd) != NULL) 642 { 643 if (dir.d_ino == 0 || strncmp(dir.d_name, "s.", 2) != 0) 644 continue; 645 646 /* got an s. file -- see if the p. file exists */ 647 strcpy(buf, SccsPath); 648 strcat(buf, "/p."); 649 basefile = &buf[strlen(buf)]; 650 strncpy(basefile, &dir.d_name[2], sizeof dir.d_name - 2); 651 basefile[sizeof dir.d_name - 2] = '\0'; 652 pfp = fopen(buf, "r"); 653 if (pfp != NULL) 654 { 655 while (fgets(pline, sizeof pline, pfp) != NULL) 656 printf("%12s: being edited: %s", basefile, pline); 657 fclose(pfp); 658 gotedit = TRUE; 659 continue; 660 } 661 662 /* the s. file exists and no p. file exists -- unlink the g-file */ 663 if (mode == CLEANC) 664 { 665 strncpy(buf, &dir.d_name[2], sizeof dir.d_name - 2); 666 buf[sizeof dir.d_name - 2] = '\0'; 667 unlink(buf); 668 } 669 } 670 671 fclose(dirfd); 672 if (!gotedit && mode == INFOC) 673 printf("Nothing being edited\n"); 674 if (mode == CHECKC) 675 exit(gotedit); 676 } 677 /* 678 ** UNEDIT -- unedit a file 679 ** 680 ** Checks to see that the current user is actually editting 681 ** the file and arranges that s/he is not editting it. 682 ** 683 ** Parameters: 684 ** fn -- the name of the file to be unedited. 685 ** 686 ** Returns: 687 ** TRUE -- if the file was successfully unedited. 688 ** FALSE -- if the file was not unedited for some 689 ** reason. 690 ** 691 ** Side Effects: 692 ** fn is removed 693 ** entries are removed from pfile. 694 */ 695 696 bool 697 unedit(fn) 698 char *fn; 699 { 700 register FILE *pfp; 701 char *pfn; 702 static char tfn[] = "/tmp/sccsXXXXX"; 703 FILE *tfp; 704 register char *p; 705 register char *q; 706 bool delete = FALSE; 707 bool others = FALSE; 708 char *myname; 709 extern char *getlogin(); 710 struct pfile *pent; 711 extern struct pfile *getpfile(); 712 char buf[120]; 713 # ifdef UIDUSER 714 struct passwd *pw; 715 extern struct passwd *getpwuid(); 716 # endif UIDUSER 717 718 /* make "s." filename & find the trailing component */ 719 pfn = makefile(fn); 720 if (pfn == NULL) 721 return (FALSE); 722 q = rindex(pfn, '/'); 723 if (q == NULL) 724 q = &pfn[-1]; 725 if (q[1] != 's' || q[2] != '.') 726 { 727 fprintf(stderr, "Sccs: bad file name \"%s\"\n", fn); 728 return (FALSE); 729 } 730 731 /* turn "s." into "p." */ 732 *++q = 'p'; 733 734 pfp = fopen(pfn, "r"); 735 if (pfp == NULL) 736 { 737 printf("%12s: not being edited\n", fn); 738 return (FALSE); 739 } 740 741 /* 742 ** Copy p-file to temp file, doing deletions as needed. 743 */ 744 745 mktemp(tfn); 746 tfp = fopen(tfn, "w"); 747 if (tfp == NULL) 748 { 749 fprintf(stderr, "Sccs: cannot create \"%s\"\n", tfn); 750 exit(EX_OSERR); 751 } 752 753 # ifdef UIDUSER 754 pw = getpwuid(getuid()); 755 if (pw == NULL) 756 { 757 fprintf(stderr, "Sccs: who are you?\n"); 758 exit(EX_OSERR); 759 } 760 myname = pw->pw_name; 761 # else 762 myname = getlogin(); 763 # endif UIDUSER 764 while ((pent = getpfile(pfp)) != NULL) 765 { 766 if (strcmp(pent->p_user, myname) == 0) 767 { 768 /* a match */ 769 delete++; 770 } 771 else 772 { 773 fprintf(tfp, "%s %s %s %s %s\n", pent->p_osid, 774 pent->p_nsid, pent->p_user, pent->p_date, 775 pent->p_time); 776 others++; 777 } 778 } 779 780 /* do final cleanup */ 781 if (others) 782 { 783 if (freopen(tfn, "r", tfp) == NULL) 784 { 785 fprintf(stderr, "Sccs: cannot reopen \"%s\"\n", tfn); 786 exit(EX_OSERR); 787 } 788 if (freopen(pfn, "w", pfp) == NULL) 789 { 790 fprintf(stderr, "Sccs: cannot create \"%s\"\n", pfn); 791 return (FALSE); 792 } 793 while (fgets(buf, sizeof buf, tfp) != NULL) 794 fputs(buf, pfp); 795 } 796 else 797 { 798 unlink(pfn); 799 } 800 fclose(tfp); 801 fclose(pfp); 802 unlink(tfn); 803 804 if (delete) 805 { 806 unlink(fn); 807 printf("%12s: removed\n", fn); 808 return (TRUE); 809 } 810 else 811 { 812 printf("%12s: not being edited by you\n", fn); 813 return (FALSE); 814 } 815 } 816 /* 817 ** GETPFILE -- get an entry from the p-file 818 ** 819 ** Parameters: 820 ** pfp -- p-file file pointer 821 ** 822 ** Returns: 823 ** pointer to p-file struct for next entry 824 ** NULL on EOF or error 825 ** 826 ** Side Effects: 827 ** Each call wipes out results of previous call. 828 */ 829 830 struct pfile * 831 getpfile(pfp) 832 FILE *pfp; 833 { 834 static struct pfile ent; 835 static char buf[120]; 836 register char *p; 837 extern char *nextfield(); 838 839 if (fgets(buf, sizeof buf, pfp) == NULL) 840 return (NULL); 841 842 ent.p_osid = p = buf; 843 ent.p_nsid = p = nextfield(p); 844 ent.p_user = p = nextfield(p); 845 ent.p_date = p = nextfield(p); 846 ent.p_time = p = nextfield(p); 847 if (p == NULL || nextfield(p) != NULL) 848 return (NULL); 849 850 return (&ent); 851 } 852 853 854 char * 855 nextfield(p) 856 register char *p; 857 { 858 if (p == NULL || *p == '\0') 859 return (NULL); 860 while (*p != ' ' && *p != '\n' && *p != '\0') 861 p++; 862 if (*p == '\n' || *p == '\0') 863 { 864 *p = '\0'; 865 return (NULL); 866 } 867 *p++ = '\0'; 868 return (p); 869 } 870