1 2 /* 3 * \file save.c 4 * 5 * This module's routines will take the currently set options and 6 * store them into an ".rc" file for re-interpretation the next 7 * time the invoking program is run. 8 * 9 * @addtogroup autoopts 10 * @{ 11 */ 12 /* 13 * This file is part of AutoOpts, a companion to AutoGen. 14 * AutoOpts is free software. 15 * AutoOpts is Copyright (C) 1992-2015 by Bruce Korb - all rights reserved 16 * 17 * AutoOpts is available under any one of two licenses. The license 18 * in use must be one of these two and the choice is under the control 19 * of the user of the license. 20 * 21 * The GNU Lesser General Public License, version 3 or later 22 * See the files "COPYING.lgplv3" and "COPYING.gplv3" 23 * 24 * The Modified Berkeley Software Distribution License 25 * See the file "COPYING.mbsd" 26 * 27 * These files have the following sha256 sums: 28 * 29 * 8584710e9b04216a394078dc156b781d0b47e1729104d666658aecef8ee32e95 COPYING.gplv3 30 * 4379e7444a0e2ce2b12dd6f5a52a27a4d02d39d247901d3285c88cf0d37f477b COPYING.lgplv3 31 * 13aa749a5b0a454917a944ed8fffc530b784f5ead522b1aacaf4ec8aa55a6239 COPYING.mbsd 32 */ 33 34 /* = = = START-STATIC-FORWARD = = = */ 35 static char const * 36 find_dir_name(tOptions * opts, int * p_free); 37 38 static char const * 39 find_file_name(tOptions * opts, int * p_free_name); 40 41 static void 42 prt_entry(FILE * fp, tOptDesc * od, char const * l_arg); 43 44 static void 45 prt_value(FILE * fp, int depth, tOptDesc * pOD, tOptionValue const * ovp); 46 47 static void 48 prt_string(FILE * fp, char const * name, char const * pz); 49 50 static void 51 prt_val_list(FILE * fp, char const * name, tArgList * al); 52 53 static void 54 prt_nested(FILE * fp, tOptDesc * p); 55 56 static FILE * 57 open_sv_file(tOptions * opts); 58 59 static void 60 prt_no_arg_opt(FILE * fp, tOptDesc * p, tOptDesc * pOD); 61 62 static void 63 prt_str_arg(FILE * fp, tOptDesc * pOD); 64 65 static void 66 prt_enum_arg(FILE * fp, tOptDesc * od); 67 68 static void 69 prt_set_arg(FILE * fp, tOptDesc * od); 70 71 static void 72 prt_file_arg(FILE * fp, tOptDesc * od, tOptions * opts); 73 /* = = = END-STATIC-FORWARD = = = */ 74 75 /** 76 */ 77 static char const * 78 find_dir_name(tOptions * opts, int * p_free) 79 { 80 char const * pzDir; 81 82 if ( (opts->specOptIdx.save_opts == NO_EQUIVALENT) 83 || (opts->specOptIdx.save_opts == 0)) 84 return NULL; 85 86 pzDir = opts->pOptDesc[ opts->specOptIdx.save_opts ].optArg.argString; 87 if ((pzDir != NULL) && (*pzDir != NUL)) 88 return pzDir; 89 90 /* 91 * This function only works if there is a directory where 92 * we can stash the RC (INI) file. 93 */ 94 { 95 char const * const * papz = opts->papzHomeList; 96 if (papz == NULL) 97 return NULL; 98 99 while (papz[1] != NULL) papz++; 100 pzDir = *papz; 101 } 102 103 /* 104 * IF it does not require deciphering an env value, then just copy it 105 */ 106 if (*pzDir != '$') 107 return pzDir; 108 109 { 110 char const * pzEndDir = strchr(++pzDir, DIRCH); 111 char * pzFileName; 112 char * pzEnv; 113 114 if (pzEndDir != NULL) { 115 char z[ AO_NAME_SIZE ]; 116 if ((pzEndDir - pzDir) > AO_NAME_LIMIT ) 117 return NULL; 118 memcpy(z, pzDir, (size_t)(pzEndDir - pzDir)); 119 z[pzEndDir - pzDir] = NUL; 120 pzEnv = getenv(z); 121 } else { 122 123 /* 124 * Make sure we can get the env value (after stripping off 125 * any trailing directory or file names) 126 */ 127 pzEnv = getenv(pzDir); 128 } 129 130 if (pzEnv == NULL) { 131 fprintf(stderr, zsave_warn, opts->pzProgName); 132 fprintf(stderr, zNotDef, pzDir); 133 return NULL; 134 } 135 136 if (pzEndDir == NULL) 137 return pzEnv; 138 139 { 140 size_t sz = strlen(pzEnv) + strlen(pzEndDir) + 2; 141 pzFileName = (char *)AGALOC(sz, "dir name"); 142 } 143 144 if (pzFileName == NULL) 145 return NULL; 146 147 *p_free = 1; 148 /* 149 * Glue together the full name into the allocated memory. 150 * FIXME: We lose track of this memory. 151 */ 152 sprintf(pzFileName, "%s/%s", pzEnv, pzEndDir); 153 return pzFileName; 154 } 155 } 156 157 /** 158 */ 159 static char const * 160 find_file_name(tOptions * opts, int * p_free_name) 161 { 162 struct stat stBuf; 163 int free_dir_name = 0; 164 165 char const * pzDir = find_dir_name(opts, &free_dir_name); 166 if (pzDir == NULL) 167 return NULL; 168 169 /* 170 * See if we can find the specified directory. We use a once-only loop 171 * structure so we can bail out early. 172 */ 173 if (stat(pzDir, &stBuf) != 0) do { 174 char z[AG_PATH_MAX]; 175 char * dirchp; 176 177 /* 178 * IF we could not, check to see if we got a full 179 * path to a file name that has not been created yet. 180 */ 181 if (errno != ENOENT) { 182 bogus_name: 183 fprintf(stderr, zsave_warn, opts->pzProgName); 184 fprintf(stderr, zNoStat, errno, strerror(errno), pzDir); 185 if (free_dir_name) 186 AGFREE(pzDir); 187 return NULL; 188 } 189 190 /* 191 * Strip off the last component, stat the remaining string and 192 * that string must name a directory 193 */ 194 dirchp = strrchr(pzDir, DIRCH); 195 if (dirchp == NULL) { 196 stBuf.st_mode = S_IFREG; 197 break; /* found directory -- viz., "." */ 198 } 199 200 if ((size_t)(dirchp - pzDir) >= sizeof(z)) 201 goto bogus_name; 202 203 memcpy(z, pzDir, (size_t)(dirchp - pzDir)); 204 z[dirchp - pzDir] = NUL; 205 206 if ((stat(z, &stBuf) != 0) || ! S_ISDIR(stBuf.st_mode)) 207 goto bogus_name; 208 stBuf.st_mode = S_IFREG; /* file within this directory */ 209 } while (false); 210 211 /* 212 * IF what we found was a directory, 213 * THEN tack on the config file name 214 */ 215 if (S_ISDIR(stBuf.st_mode)) { 216 size_t sz = strlen(pzDir) + strlen(opts->pzRcName) + 2; 217 218 { 219 char * pzPath = (char *)AGALOC(sz, "file name"); 220 #ifdef HAVE_SNPRINTF 221 snprintf(pzPath, sz, "%s/%s", pzDir, opts->pzRcName); 222 #else 223 sprintf(pzPath, "%s/%s", pzDir, opts->pzRcName); 224 #endif 225 if (free_dir_name) 226 AGFREE(pzDir); 227 pzDir = pzPath; 228 free_dir_name = 1; 229 } 230 231 /* 232 * IF we cannot stat the object for any reason other than 233 * it does not exist, then we bail out 234 */ 235 if (stat(pzDir, &stBuf) != 0) { 236 if (errno != ENOENT) { 237 fprintf(stderr, zsave_warn, opts->pzProgName); 238 fprintf(stderr, zNoStat, errno, strerror(errno), 239 pzDir); 240 AGFREE(pzDir); 241 return NULL; 242 } 243 244 /* 245 * It does not exist yet, but it will be a regular file 246 */ 247 stBuf.st_mode = S_IFREG; 248 } 249 } 250 251 /* 252 * Make sure that whatever we ultimately found, that it either is 253 * or will soon be a file. 254 */ 255 if (! S_ISREG(stBuf.st_mode)) { 256 fprintf(stderr, zsave_warn, opts->pzProgName, pzDir); 257 if (free_dir_name) 258 AGFREE(pzDir); 259 return NULL; 260 } 261 262 /* 263 * Get rid of the old file 264 */ 265 unlink(pzDir); 266 *p_free_name = free_dir_name; 267 return pzDir; 268 } 269 270 /** 271 * print one option entry to the save file. 272 * 273 * @param[in] fp the file pointer for the save file 274 * @param[in] od the option descriptor to print 275 * @param[in] l_arg the last argument for the option 276 */ 277 static void 278 prt_entry(FILE * fp, tOptDesc * od, char const * l_arg) 279 { 280 int space_ct; 281 282 /* 283 * There is an argument. Pad the name so values line up. 284 * Not disabled *OR* this got equivalenced to another opt, 285 * then use current option name. 286 * Otherwise, there must be a disablement name. 287 */ 288 { 289 char const * pz = 290 (! DISABLED_OPT(od) || (od->optEquivIndex != NO_EQUIVALENT)) 291 ? od->pz_Name 292 : od->pz_DisableName; 293 space_ct = 17 - strlen(pz); 294 fputs(pz, fp); 295 } 296 297 if ( (l_arg == NULL) 298 && (OPTST_GET_ARGTYPE(od->fOptState) != OPARG_TYPE_NUMERIC)) 299 goto end_entry; 300 301 fputs(" = ", fp); 302 while (space_ct-- > 0) fputc(' ', fp); 303 304 /* 305 * IF the option is numeric only, 306 * THEN the char pointer is really the number 307 */ 308 if (OPTST_GET_ARGTYPE(od->fOptState) == OPARG_TYPE_NUMERIC) 309 fprintf(fp, "%d", (int)(intptr_t)l_arg); 310 311 else { 312 for (;;) { 313 char const * eol = strchr(l_arg, NL); 314 315 /* 316 * IF this is the last line 317 * THEN bail and print it 318 */ 319 if (eol == NULL) 320 break; 321 322 /* 323 * Print the continuation and the text from the current line 324 */ 325 (void)fwrite(l_arg, (size_t)(eol - l_arg), (size_t)1, fp); 326 l_arg = eol+1; /* advance the Last Arg pointer */ 327 fputs("\\\n", fp); 328 } 329 330 /* 331 * Terminate the entry 332 */ 333 fputs(l_arg, fp); 334 } 335 336 end_entry: 337 fputc(NL, fp); 338 } 339 340 /** 341 */ 342 static void 343 prt_value(FILE * fp, int depth, tOptDesc * pOD, tOptionValue const * ovp) 344 { 345 while (--depth >= 0) 346 putc(' ', fp), putc(' ', fp); 347 348 switch (ovp->valType) { 349 default: 350 case OPARG_TYPE_NONE: 351 fprintf(fp, NULL_ATR_FMT, ovp->pzName); 352 break; 353 354 case OPARG_TYPE_STRING: 355 prt_string(fp, ovp->pzName, ovp->v.strVal); 356 break; 357 358 case OPARG_TYPE_ENUMERATION: 359 case OPARG_TYPE_MEMBERSHIP: 360 if (pOD != NULL) { 361 uint32_t opt_state = pOD->fOptState; 362 uintptr_t val = pOD->optArg.argEnum; 363 char const * typ = (ovp->valType == OPARG_TYPE_ENUMERATION) 364 ? "keyword" : "set-membership"; 365 366 fprintf(fp, TYPE_ATR_FMT, ovp->pzName, typ); 367 368 /* 369 * This is a magic incantation that will convert the 370 * bit flag values back into a string suitable for printing. 371 */ 372 (*(pOD->pOptProc))(OPTPROC_RETURN_VALNAME, pOD ); 373 if (pOD->optArg.argString != NULL) { 374 fputs(pOD->optArg.argString, fp); 375 376 if (ovp->valType != OPARG_TYPE_ENUMERATION) { 377 /* 378 * set membership strings get allocated 379 */ 380 AGFREE(pOD->optArg.argString); 381 } 382 } 383 384 pOD->optArg.argEnum = val; 385 pOD->fOptState = opt_state; 386 fprintf(fp, END_XML_FMT, ovp->pzName); 387 break; 388 } 389 /* FALLTHROUGH */ 390 391 case OPARG_TYPE_NUMERIC: 392 fprintf(fp, NUMB_ATR_FMT, ovp->pzName, ovp->v.longVal); 393 break; 394 395 case OPARG_TYPE_BOOLEAN: 396 fprintf(fp, BOOL_ATR_FMT, ovp->pzName, 397 ovp->v.boolVal ? "true" : "false"); 398 break; 399 400 case OPARG_TYPE_HIERARCHY: 401 prt_val_list(fp, ovp->pzName, ovp->v.nestVal); 402 break; 403 } 404 } 405 406 /** 407 */ 408 static void 409 prt_string(FILE * fp, char const * name, char const * pz) 410 { 411 fprintf(fp, OPEN_XML_FMT, name); 412 for (;;) { 413 int ch = ((int)*(pz++)) & 0xFF; 414 415 switch (ch) { 416 case NUL: goto string_done; 417 418 case '&': 419 case '<': 420 case '>': 421 #if __GNUC__ >= 4 422 case 1 ... (' ' - 1): 423 case ('~' + 1) ... 0xFF: 424 #endif 425 emit_special_char(fp, ch); 426 break; 427 428 default: 429 #if __GNUC__ < 4 430 if ( ((ch >= 1) && (ch <= (' ' - 1))) 431 || ((ch >= ('~' + 1)) && (ch <= 0xFF)) ) { 432 emit_special_char(fp, ch); 433 break; 434 } 435 #endif 436 putc(ch, fp); 437 } 438 } string_done:; 439 fprintf(fp, END_XML_FMT, name); 440 } 441 442 /** 443 */ 444 static void 445 prt_val_list(FILE * fp, char const * name, tArgList * al) 446 { 447 static int depth = 1; 448 449 int sp_ct; 450 int opt_ct; 451 void ** opt_list; 452 453 if (al == NULL) 454 return; 455 opt_ct = al->useCt; 456 opt_list = VOIDP(al->apzArgs); 457 458 if (opt_ct <= 0) { 459 fprintf(fp, OPEN_CLOSE_FMT, name); 460 return; 461 } 462 463 fprintf(fp, NESTED_OPT_FMT, name); 464 465 depth++; 466 while (--opt_ct >= 0) { 467 tOptionValue const * ovp = *(opt_list++); 468 469 prt_value(fp, depth, NULL, ovp); 470 } 471 depth--; 472 473 for (sp_ct = depth; --sp_ct >= 0;) 474 putc(' ', fp), putc(' ', fp); 475 fprintf(fp, "</%s>\n", name); 476 } 477 478 /** 479 */ 480 static void 481 prt_nested(FILE * fp, tOptDesc * p) 482 { 483 int opt_ct; 484 tArgList * al = p->optCookie; 485 void ** opt_list; 486 487 if (al == NULL) 488 return; 489 490 opt_ct = al->useCt; 491 opt_list = VOIDP(al->apzArgs); 492 493 if (opt_ct <= 0) 494 return; 495 496 do { 497 tOptionValue const * base = *(opt_list++); 498 tOptionValue const * ovp = optionGetValue(base, NULL); 499 500 if (ovp == NULL) 501 continue; 502 503 fprintf(fp, NESTED_OPT_FMT, p->pz_Name); 504 505 do { 506 prt_value(fp, 1, p, ovp); 507 508 } while (ovp = optionNextValue(base, ovp), 509 ovp != NULL); 510 511 fprintf(fp, "</%s>\n", p->pz_Name); 512 } while (--opt_ct > 0); 513 } 514 515 /** 516 * open the file for saving option state. 517 * 518 * @param[in] opts the program options structure 519 * @returns the open file pointer. It may be NULL. 520 */ 521 static FILE * 522 open_sv_file(tOptions * opts) 523 { 524 FILE * fp; 525 526 { 527 int free_name = 0; 528 char const * pzFName = find_file_name(opts, &free_name); 529 if (pzFName == NULL) 530 return NULL; 531 532 fp = fopen(pzFName, "w" FOPEN_BINARY_FLAG); 533 if (fp == NULL) { 534 fprintf(stderr, zsave_warn, opts->pzProgName); 535 fprintf(stderr, zNoCreat, errno, strerror(errno), pzFName); 536 if (free_name) 537 AGFREE(pzFName); 538 return fp; 539 } 540 541 if (free_name) 542 AGFREE(pzFName); 543 } 544 545 fputs("# ", fp); 546 { 547 char const * e = strchr(opts->pzUsageTitle, NL); 548 if (e++ != NULL) 549 fwrite(opts->pzUsageTitle, 1, e - opts->pzUsageTitle, fp); 550 } 551 552 { 553 time_t cur_time = time(NULL); 554 char * time_str = ctime(&cur_time); 555 556 fprintf(fp, zPresetFile, time_str); 557 #ifdef HAVE_ALLOCATED_CTIME 558 /* 559 * The return values for ctime(), localtime(), and gmtime() 560 * normally point to static data that is overwritten by each call. 561 * The test to detect allocated ctime, so we leak the memory. 562 */ 563 AGFREE(time_str); 564 #endif 565 } 566 567 return fp; 568 } 569 570 /** 571 */ 572 static void 573 prt_no_arg_opt(FILE * fp, tOptDesc * p, tOptDesc * pOD) 574 { 575 /* 576 * The aliased to argument indicates whether or not the option 577 * is "disabled". However, the original option has the name 578 * string, so we get that there, not with "p". 579 */ 580 char const * pznm = 581 (DISABLED_OPT(p)) ? pOD->pz_DisableName : pOD->pz_Name; 582 /* 583 * If the option was disabled and the disablement name is NULL, 584 * then the disablement was caused by aliasing. 585 * Use the name as the string to emit. 586 */ 587 if (pznm == NULL) 588 pznm = pOD->pz_Name; 589 590 fprintf(fp, "%s\n", pznm); 591 } 592 593 /** 594 */ 595 static void 596 prt_str_arg(FILE * fp, tOptDesc * pOD) 597 { 598 if (pOD->fOptState & OPTST_STACKED) { 599 tArgList * pAL = (tArgList *)pOD->optCookie; 600 int uct = pAL->useCt; 601 char const ** ppz = pAL->apzArgs; 602 603 /* 604 * un-disable multiple copies of disabled options. 605 */ 606 if (uct > 1) 607 pOD->fOptState &= ~OPTST_DISABLED; 608 609 while (uct-- > 0) 610 prt_entry(fp, pOD, *(ppz++)); 611 } else { 612 prt_entry(fp, pOD, pOD->optArg.argString); 613 } 614 } 615 616 /** 617 * print the string value of an enumeration. 618 * 619 * @param[in] fp the file pointer to write to 620 * @param[in] od the option descriptor with the enumerated value 621 */ 622 static void 623 prt_enum_arg(FILE * fp, tOptDesc * od) 624 { 625 uintptr_t val = od->optArg.argEnum; 626 627 /* 628 * This is a magic incantation that will convert the 629 * bit flag values back into a string suitable for printing. 630 */ 631 (*(od->pOptProc))(OPTPROC_RETURN_VALNAME, od); 632 prt_entry(fp, od, VOIDP(od->optArg.argString)); 633 634 od->optArg.argEnum = val; 635 } 636 637 /** 638 * Print the bits set in a bit mask option. 639 * We call the option handling function with a magic value for 640 * the options pointer and it allocates and fills in the string. 641 * We print that with a call to prt_entry(). 642 * 643 * @param[in] fp the file pointer to write to 644 * @param[in] od the option descriptor with a bit mask value type 645 */ 646 static void 647 prt_set_arg(FILE * fp, tOptDesc * od) 648 { 649 char * list = optionMemberList(od); 650 size_t len = strlen(list); 651 char * buf = (char *)AGALOC(len + 3, "dir name"); 652 *buf= '='; 653 memcpy(buf+1, list, len + 1); 654 prt_entry(fp, od, buf); 655 AGFREE(buf); 656 AGFREE(list); 657 } 658 659 /** 660 * figure out what the option file name argument is. 661 * If one can be found, call prt_entry() to emit it. 662 * 663 * @param[in] fp the file pointer to write to. 664 * @param[in] od the option descriptor with a bit mask value type 665 * @param[in] opts the program options descriptor 666 */ 667 static void 668 prt_file_arg(FILE * fp, tOptDesc * od, tOptions * opts) 669 { 670 /* 671 * If the cookie is not NULL, then it has the file name, period. 672 * Otherwise, if we have a non-NULL string argument, then.... 673 */ 674 if (od->optCookie != NULL) 675 prt_entry(fp, od, od->optCookie); 676 677 else if (HAS_originalOptArgArray(opts)) { 678 char const * orig = 679 opts->originalOptArgArray[od->optIndex].argString; 680 681 if (od->optArg.argString == orig) 682 return; 683 684 prt_entry(fp, od, od->optArg.argString); 685 } 686 } 687 688 /*=export_func optionSaveFile 689 * 690 * what: saves the option state to a file 691 * 692 * arg: tOptions *, opts, program options descriptor 693 * 694 * doc: 695 * 696 * This routine will save the state of option processing to a file. The name 697 * of that file can be specified with the argument to the @code{--save-opts} 698 * option, or by appending the @code{rcfile} attribute to the last 699 * @code{homerc} attribute. If no @code{rcfile} attribute was specified, it 700 * will default to @code{.@i{programname}rc}. If you wish to specify another 701 * file, you should invoke the @code{SET_OPT_SAVE_OPTS(@i{filename})} macro. 702 * 703 * The recommend usage is as follows: 704 * @example 705 * optionProcess(&progOptions, argc, argv); 706 * if (i_want_a_non_standard_place_for_this) 707 * SET_OPT_SAVE_OPTS("myfilename"); 708 * optionSaveFile(&progOptions); 709 * @end example 710 * 711 * err: 712 * 713 * If no @code{homerc} file was specified, this routine will silently return 714 * and do nothing. If the output file cannot be created or updated, a message 715 * will be printed to @code{stderr} and the routine will return. 716 =*/ 717 void 718 optionSaveFile(tOptions * opts) 719 { 720 tOptDesc * od; 721 int ct; 722 FILE * fp = open_sv_file(opts); 723 724 if (fp == NULL) 725 return; 726 727 /* 728 * FOR each of the defined options, ... 729 */ 730 ct = opts->presetOptCt; 731 od = opts->pOptDesc; 732 do { 733 tOptDesc * p; 734 735 /* 736 * IF the option has not been defined 737 * OR it does not take an initialization value 738 * OR it is equivalenced to another option 739 * THEN continue (ignore it) 740 * 741 * Equivalenced options get picked up when the equivalenced-to 742 * option is processed. 743 */ 744 if (UNUSED_OPT(od)) 745 continue; 746 747 if ((od->fOptState & OPTST_DO_NOT_SAVE_MASK) != 0) 748 continue; 749 750 if ( (od->optEquivIndex != NO_EQUIVALENT) 751 && (od->optEquivIndex != od->optIndex)) 752 continue; 753 754 /* 755 * The option argument data are found at the equivalenced-to option, 756 * but the actual option argument type comes from the original 757 * option descriptor. Be careful! 758 */ 759 p = ((od->fOptState & OPTST_EQUIVALENCE) != 0) 760 ? (opts->pOptDesc + od->optActualIndex) : od; 761 762 switch (OPTST_GET_ARGTYPE(od->fOptState)) { 763 case OPARG_TYPE_NONE: 764 prt_no_arg_opt(fp, p, od); 765 break; 766 767 case OPARG_TYPE_NUMERIC: 768 prt_entry(fp, p, VOIDP(p->optArg.argInt)); 769 break; 770 771 case OPARG_TYPE_STRING: 772 prt_str_arg(fp, p); 773 break; 774 775 case OPARG_TYPE_ENUMERATION: 776 prt_enum_arg(fp, p); 777 break; 778 779 case OPARG_TYPE_MEMBERSHIP: 780 prt_set_arg(fp, p); 781 break; 782 783 case OPARG_TYPE_BOOLEAN: 784 prt_entry(fp, p, p->optArg.argBool ? "true" : "false"); 785 break; 786 787 case OPARG_TYPE_HIERARCHY: 788 prt_nested(fp, p); 789 break; 790 791 case OPARG_TYPE_FILE: 792 prt_file_arg(fp, p, opts); 793 break; 794 795 default: 796 break; /* cannot handle - skip it */ 797 } 798 } while (od++, (--ct > 0)); 799 800 fclose(fp); 801 } 802 /** @} 803 * 804 * Local Variables: 805 * mode: C 806 * c-file-style: "stroustrup" 807 * indent-tabs-mode: nil 808 * End: 809 * end of autoopts/save.c */ 810