1 /* Copyright (C) 1993, 1992 Nathan Sidwell */ 2 /* RCS $Id: scoring.c,v 4.14 1995/12/21 15:55:04 nathan Exp $ */ 3 /*{{{ file locking problems*/ 4 /* 5 * USELOCKFILE file locking as suggested by 6 * Daniel Edward Lovinger <del+@cmu.edu> 7 * With lockf (or flock), we just use the kernel's locking stuff to lock the 8 * entire score file while we read, or update it. But some distributed 9 * file systems don't support it and some are broken (SunOS 4.1). 10 * USELOCKFILE uses uses open(O_CREAT | O_EXCL) to create 11 * a lock file in the same directory as the xmris high score file, with 12 * the name "xmris.lock". 13 * The personal score files are either in the score file directory 14 * with names "xmris-<name>", or in the user's home directory with name 15 * ".xmris.scores". 16 * In order to work correctly, if xmris is set_euid'd to get the access 17 * permissions to the high score directory, we keep juggling 18 * the effective user id between the set_euid'd one and the real uid. 19 * This ensures that xmris can open the display on servers which use 20 * magic cookie and access control (like openwindows), and that the 21 * personal file has the correct attributes when created in the user's 22 * home directory. 23 * 24 * Some systems have flock (BSD), and some have lockf (SYSV). 25 */ 26 /*}}}*/ 27 #include "xmris.h" 28 /*{{{ other includes*/ 29 #ifdef TRANSPUTER 30 #include <iocntrl.h> 31 #else 32 #include <pwd.h> 33 #include <sys/stat.h> 34 #ifdef USELOCKFILE 35 #include <fcntl.h> 36 #endif /* USELOCKFILE */ 37 #endif /* TRANSPUTER */ 38 /*}}}*/ 39 /*{{{ file locking*/ 40 #ifndef SYSV 41 #define lock_file(stream) flock(fileno(stream), LOCK_EX) 42 #define unlock_file(stream) flock(fileno(stream), LOCK_UN) 43 #else 44 #define lock_file(stream) lockf(fileno(stream), F_LOCK, 0L) 45 #define unlock_file(stream) lockf(fileno(stream), F_ULOCK, 0L) 46 #endif /* SYSV */ 47 /*}}}*/ 48 /*{{{ static*/ 49 static CONST char date_formats[4] = "DMY"; 50 static char *score_file = NULL; /* high score file name */ 51 static char *personal_file = NULL; /* personal in high score dir */ 52 static char *personal_home = NULL; /* personal in home dir */ 53 static int personal_make = 2; 54 static uid_t personal_uid = -1; /* uid for personal file */ 55 static char date_format[4] = " "; 56 static char *alternate = NULL; /* alternative name */ 57 #ifdef USELOCKFILE 58 static char *locking_file = NULL; /* lock file name */ 59 static unsigned locks; /* number of locks open */ 60 #endif /* USELOCKFILE */ 61 static HIGH_SCORE *CONST tables[] = 62 {scoring.high, scoring.today, scoring.personal}; 63 /*}}}*/ 64 /*{{{ prototypes*/ 65 static unsigned expire PROTOARG((time_t, time_t)); 66 static unsigned file_changed PROTOARG((char CONST *, uid_t)); 67 static FILE *get_lock PROTOARG((char CONST *, unsigned, uid_t)); 68 static VOIDFUNC get_unlock PROTOARG((FILE *)); 69 static unsigned insert_personal PROTOARG((HIGH_SCORE CONST *)); 70 static unsigned insert_score 71 PROTOARG((HIGH_SCORE *, HIGH_SCORE *, unsigned)); 72 static VOIDFUNC load_check_expire_insert PROTOARG((unsigned)); 73 static VOIDFUNC make_unique PROTOARG((HIGH_SCORE *)); 74 static unsigned merge_personal PROTOARG((FILE *)); 75 static unsigned merge_scores PROTOARG((FILE *)); 76 static unsigned long read_line 77 PROTOARG((FILE *, HIGH_SCORE *, unsigned long)); 78 static VOIDFUNC remove_scores PROTOARG((HIGH_SCORE *, unsigned)); 79 static VOIDFUNC retire_scores PROTOARG((void)); 80 static VOIDFUNC write_personal PROTOARG((FILE *)); 81 static VOIDFUNC write_scores PROTOARG((FILE *)); 82 static unsigned long write_table 83 PROTOARG((FILE *, HIGH_SCORE *, unsigned long)); 84 /*}}}*/ 85 /*{{{ void check_scores()*/ 86 extern VOIDFUNC check_scores FUNCARGVOID 87 { 88 retire_scores(); 89 if((score_file && file_changed(score_file, effective_uid)) || 90 (!personal_make && personal_file && 91 file_changed(personal_file, personal_uid))) 92 load_check_expire_insert(0); 93 return; 94 } 95 /*}}}*/ 96 /*{{{ unsigned expire(now, then)*/ 97 static unsigned expire 98 FUNCARG((now, then), 99 time_t now 100 ARGSEP time_t then 101 ) 102 /* 103 * check to see if the score is now too old 104 */ 105 { 106 struct tm *ptr; 107 int now_day, then_day; 108 int now_hour, then_hour; 109 110 ptr = localtime(&now); 111 now_day = ptr->tm_yday; 112 now_hour = ptr->tm_hour; 113 ptr = localtime(&then); 114 then_day = ptr->tm_yday; 115 then_hour = ptr->tm_hour; 116 return !(now_day == then_day || (now_day == then_day + 1 && 117 then_hour >= 21 && now_hour < 12)); 118 } 119 /*}}}*/ 120 /*{{{ void file_changed(name, uid)*/ 121 static unsigned file_changed 122 FUNCARG((name, uid), 123 char CONST *name 124 ARGSEP uid_t uid 125 ) 126 /* 127 * check if a score file has been changed since last I looked, 128 * so that we pick up the new scores 129 */ 130 { 131 #ifdef TRANSPUTER 132 return 1; /* assume that it has changed */ 133 #else 134 static time_t last_time[2]; 135 struct stat buffer; 136 unsigned changed; 137 138 assert(name); 139 if(uid != current_uid) 140 set_euid((current_uid = uid)); 141 if(!stat(name, &buffer)) 142 { 143 changed = buffer.st_mtime != last_time[name == score_file]; 144 last_time[name == score_file] = buffer.st_mtime; 145 } 146 else 147 changed = 0; 148 if(real_uid != current_uid) 149 set_euid((current_uid = real_uid)); 150 return changed; 151 #endif /* TRANSPUTER */ 152 } 153 /*}}}*/ 154 /*{{{ void get_lock(name, flag, uid)*/ 155 static FILE *get_lock 156 FUNCARG((name, flag, uid), 157 char CONST *name 158 ARGSEP unsigned flag 159 ARGSEP uid_t uid 160 ) 161 /* 162 * open and locks a high score file 163 * flag & 1 == 0 -> "r+" 164 * flag & 1 != 0 -> "w+" 165 * flag & 2 inhibit error message 166 * flag & 4 && effective_uid == real_uid set chmod 666 167 * uid required to access 168 */ 169 { 170 FILE *stream; 171 172 #ifdef TRANSPUTER 173 if(locking_file && !locks) 174 /*{{{ attempt file lock*/ 175 { 176 unsigned count; 177 FILE *lock; 178 179 for(count = 3; count; count--) 180 { 181 lock = fopen(locking_file, "r"); 182 if(lock) 183 { 184 fclose(lock); 185 sleep(1); 186 } 187 else 188 { 189 lock = fopen(locking_file, "w"); 190 if(lock) 191 fclose(lock); 192 else 193 perror(locking_file); 194 break; 195 } 196 } 197 } 198 /*}}}*/ 199 #else 200 #ifdef USELOCKFILE 201 if(locking_file && !locks) 202 /*{{{ attempt exclusive file lock*/ 203 { 204 unsigned count; 205 int filed; 206 207 for(count = 3; count;) 208 { 209 if(current_uid != effective_uid) 210 set_euid((current_uid = effective_uid)); 211 filed = open(locking_file, O_CREAT | O_EXCL, 0660); 212 if(filed >= 0) 213 break; 214 if(errno == EINTR) 215 continue; 216 else if(errno == EEXIST) 217 { 218 sleep(1); 219 if(!file_changed(name, uid)) 220 count--; 221 } 222 else 223 { 224 perror(locking_file); 225 break; 226 } 227 } 228 if(filed >= 0) 229 close(filed); 230 } 231 /*}}}*/ 232 #endif /* USELOCKFILE */ 233 if(uid != current_uid) 234 set_euid((current_uid = uid)); 235 #endif /* TRANSPUTER */ 236 stream = fopen(name, flag & 1 ? "w+" : "r+"); 237 if(!stream && !(flag & 2)) 238 perror(name); 239 #ifdef USELOCKFILE 240 if(stream) 241 locks++; 242 else if(locking_file && !locks) 243 { 244 #ifndef TRANSPUTER 245 if(current_uid != effective_uid) 246 set_euid((current_uid = effective_uid)); 247 #endif /* TRANSPUTER */ 248 unlink(locking_file); 249 } 250 #else 251 if(stream) 252 /*{{{ get lock on the file*/ 253 while(lock_file(stream)) 254 { 255 if(errno == EINTR) 256 continue; 257 } 258 /*}}}*/ 259 #endif /* USELOCKFILE */ 260 #ifndef TRANSPUTER 261 if(stream && flag & 4 && effective_uid == real_uid) 262 chmod(name, 0660); /* not everyone has fchmod */ 263 if(current_uid != real_uid) 264 set_euid((current_uid = real_uid)); 265 #endif /* TRANSPUTER */ 266 return stream; 267 } 268 /*}}}*/ 269 /*{{{ void get_unlock(stream)*/ 270 static VOIDFUNC get_unlock 271 FUNCARG((stream), 272 FILE *stream 273 ) 274 /* 275 * unlock and close the high score file 276 */ 277 { 278 fflush(stream); 279 #ifdef USELOCKFILE 280 fclose(stream); 281 locks--; 282 if(locking_file && locks) 283 { 284 #ifndef TRANSPUTER 285 if(current_uid != effective_uid) 286 set_euid((current_uid = effective_uid)); 287 #endif 288 unlink(locking_file); 289 #ifndef TRANSPUTER 290 if(current_uid != real_uid) 291 set_euid((current_uid = real_uid)); 292 #endif /* TRANSPUTER */ 293 } 294 #else 295 rewind(stream); 296 unlock_file(stream); 297 fclose(stream); 298 #endif /* USELOCKFILE */ 299 return; 300 } 301 /*}}}*/ 302 /*{{{ void high_score(score, screen, msec)*/ 303 extern VOIDFUNC high_score 304 FUNCARG((score, screen, msec), 305 unsigned long score 306 ARGSEP unsigned screen 307 ARGSEP unsigned long msec 308 ) 309 /* 310 * are we worthy of imortallity? 311 */ 312 { 313 scoring.mine.score = score; 314 scoring.mine.screen = screen; 315 scoring.mine.elapsed = (unsigned)(msec / 1000); 316 scoring.mine.stamp = time((time_t *)NULL); 317 scoring.mine.marker = 0; 318 retire_scores(); 319 if(score && (score >= scoring.today[HIGH_SCORES - 1].score || 320 score >= scoring.personal[HIGH_SCORES - 1].score || 321 (score >= SCORE_THRESHOLD && 322 score >= scoring.high[HIGH_SCORES - 1].score))) 323 load_check_expire_insert(1); 324 return; 325 } 326 /*}}}*/ 327 /*{{{ void init_scores()*/ 328 extern VOIDFUNC init_scores FUNCARGVOID 329 /* 330 * check that we have the wherewithall to deal with 331 * high scores 332 */ 333 { 334 char CONST *user; 335 char CONST *home; 336 char *reallife; 337 size_t dirlen; 338 339 user = NULL; 340 home = NULL; 341 reallife = NULL; 342 #ifndef TRANSPUTER 343 /*{{{ read passwd information*/ 344 { 345 struct passwd *ptr; 346 347 ptr = getpwuid(real_uid); 348 if(ptr) 349 { 350 user = ptr->pw_name; 351 home = ptr->pw_dir; 352 if(user && ptr->pw_gecos) 353 /*{{{ get the realname*/ 354 { 355 /* attempt to extract a realname from the gecos field. 356 * sometimes this has more than the name in, and sometimes 357 * the name is surname,firstname, 358 * we convert to firstname surname, and strip off 359 * extra fields after the comma. 360 * if the thing before the first comma has spaces in it, then 361 * we assume that's the name. 362 */ 363 char *sptr; 364 char *cptr; 365 366 reallife = ptr->pw_gecos; 367 cptr = strchr(reallife, ','); 368 sptr = strchr(reallife, ' '); 369 if(cptr && (!sptr || sptr > cptr)) 370 { 371 sptr = cptr + 1; 372 cptr = strchr(cptr + 1, ','); 373 } 374 else 375 sptr = NULL; 376 if(cptr) 377 *cptr = 0; 378 if(!*reallife) 379 reallife = NULL; 380 else if(sptr) 381 { 382 if(!cptr) 383 cptr = reallife + strlen(reallife); 384 if(cptr[-1] != ' ') 385 { 386 cptr[0] = ' '; 387 cptr[1] = 0; 388 } 389 while(*sptr == ' ') 390 sptr++; 391 while(sptr != reallife) 392 { 393 char c; 394 395 c = *reallife; 396 for(cptr = reallife; cptr[1]; cptr++) 397 cptr[0] = cptr[1]; 398 sptr--; 399 *cptr = c; 400 } 401 while(*cptr == ' ' || *cptr == ',') 402 *cptr-- = 0; 403 } 404 } 405 /*}}}*/ 406 } 407 } 408 /*}}}*/ 409 #endif 410 scoring.mine.score = 0; 411 /*{{{ set username*/ 412 { 413 if(!user) 414 user = getenv("LOGNAME"); 415 if(!user) 416 user = getenv("USER"); 417 if(!user) 418 user = "Unknown"; 419 if(data.username == False && reallife) 420 { 421 strncpy(scoring.mine.name, reallife, NAME_LEN); 422 reallife = (char *)user; 423 } 424 else 425 strncpy(scoring.mine.name, user, NAME_LEN); 426 scoring.mine.name[NAME_LEN] = 0; 427 if(reallife) 428 { 429 /* not everyone has strdup */ 430 alternate = malloc(strlen(reallife) + 1); 431 if(alternate) 432 strcpy(alternate, reallife); 433 } 434 if(data.username == False) 435 scoring.alternate = alternate; 436 } 437 /*}}}*/ 438 dirlen = data.dir ? strlen(data.dir) : 0; 439 if(dirlen && data.dir[dirlen - 1] == '/') 440 dirlen--; 441 #ifdef USELOCKFILE 442 /*{{{ lock file?*/ 443 if(dirlen) 444 { 445 locking_file = malloc(dirlen + 12); 446 if(locking_file) 447 { 448 strcpy(locking_file, data.dir); 449 strcpy(&locking_file[dirlen], "/xmris.lock"); 450 } 451 } 452 /*}}}*/ 453 #endif /* USELOCKFILE */ 454 /*{{{ score directory?*/ 455 if(dirlen) 456 { 457 size_t nlen; 458 459 score_file = malloc(dirlen + 14); 460 if(score_file) 461 { 462 strcpy(score_file, data.dir); 463 strcpy(&score_file[dirlen], "/xmris.score"); 464 } 465 nlen = strlen(user); 466 personal_file = malloc(dirlen + 8 + nlen); 467 if(personal_file) 468 { 469 strcpy(personal_file, data.dir); 470 strcpy(&personal_file[dirlen], "/xmris-"); 471 strcpy(&personal_file[dirlen + 7], user); 472 } 473 } 474 /*}}}*/ 475 /*{{{ personal file*/ 476 { 477 if(!home) 478 home = getenv("HOME"); 479 if(home) 480 { 481 size_t length; 482 483 length = strlen(home); 484 personal_home = malloc(length + 15); 485 if(personal_home) 486 { 487 strcpy(personal_home, home); 488 strcpy(&personal_home[length], "/.xmris.score"); 489 } 490 } 491 } 492 /*}}}*/ 493 personal_uid = effective_uid; 494 if(data.expire || data.format || data.remove) 495 data.scores = True; 496 load_check_expire_insert(0); 497 return; 498 } 499 /*}}}*/ 500 /*{{{ unsigned insert_personal(sptr)*/ 501 static unsigned insert_personal 502 FUNCARG((sptr), 503 HIGH_SCORE CONST *sptr 504 ) 505 /* 506 * inserts a score into the personal file 507 */ 508 { 509 HIGH_SCORE new; 510 char CONST *ptr; 511 512 memcpy(&new, sptr, sizeof(new)); 513 ptr = ctime(&new.stamp); 514 memcpy(&new.name[0], &ptr[11], 5); 515 memcpy(&new.name[5], &ptr[7], 3); 516 memcpy(&new.name[8], &ptr[3], 5); 517 memcpy(&new.name[13], &ptr[22], 2); 518 new.name[15] = 0; 519 return insert_score(scoring.personal, &new, 0); 520 } 521 /*}}}*/ 522 /*{{{ unsigned insert_score(sptr, iptr, multiple)*/ 523 static unsigned insert_score 524 FUNCARG((sptr, iptr, multiple), 525 HIGH_SCORE *sptr /* table */ 526 ARGSEP HIGH_SCORE *iptr /* new score */ 527 ARGSEP unsigned multiple /* multiple entries */ 528 ) 529 /* 530 * inserts a score into a high score table 531 */ 532 { 533 unsigned ix; 534 unsigned inserted; 535 536 inserted = 0; 537 iptr->marker = 0; 538 if(iptr->score) 539 { 540 for(ix = HIGH_SCORES; ix--; sptr++) 541 if(sptr->score > iptr->score) 542 { 543 if(!multiple && !strcmp(sptr->name, iptr->name)) 544 break; 545 } 546 else if(sptr->score < iptr->score || sptr->stamp < iptr->stamp) 547 { 548 HIGH_SCORE *eptr; 549 550 for(eptr = sptr; ix--; eptr++) 551 if(!eptr->score) 552 { 553 if(ix) 554 eptr[1].score = 0; 555 break; 556 } 557 else if(!multiple && !strcmp(eptr->name, iptr->name)) 558 break; 559 for(; eptr != sptr; eptr--) 560 memcpy(eptr, eptr - 1, sizeof(HIGH_SCORE)); 561 memcpy(sptr, iptr, sizeof(HIGH_SCORE)); 562 inserted = 1; 563 break; 564 } 565 else 566 { 567 strcpy(sptr->name, iptr->name); 568 break; 569 } 570 } 571 return inserted; 572 } 573 /*}}}*/ 574 /*{{{ void load_check_expire_insert(insert)*/ 575 static VOIDFUNC load_check_expire_insert 576 FUNCARG((insert), 577 unsigned insert 578 ) 579 /* this does most of the high score file manipulation 580 * it loads up the high score files 581 * checks the integrity of the personal score vs high score 582 * optionally inserts a new score into the tables 583 * optionally expires scores 584 * write the files back 585 */ 586 { 587 FILE *score_stream; 588 FILE *personal_stream; 589 FILE *home_stream; 590 unsigned do_score, do_personal; 591 592 score_stream = personal_stream = home_stream = NULL; 593 do_score = do_personal = 0; 594 /*{{{ score_file?*/ 595 if(score_file) 596 { 597 score_stream = get_lock(score_file, 2, effective_uid); 598 if(score_stream) 599 do_score = merge_scores(score_stream); 600 else 601 do_score = 1; 602 } 603 /*}}}*/ 604 /*{{{ personal_file?*/ 605 if(personal_file) 606 { 607 personal_stream = get_lock(personal_file, personal_make, personal_uid); 608 if(personal_stream) 609 { 610 personal_make = 0; 611 if(personal_home && !unlink(personal_home)) 612 fprintf(stderr, "Two personal score files, '%s' removed\n", 613 personal_home); 614 personal_home = NULL; 615 do_personal = merge_personal(personal_stream); 616 } 617 } 618 /*}}}*/ 619 /*{{{ personal_home?*/ 620 if(personal_home) 621 { 622 home_stream = get_lock(personal_home, 2, real_uid); 623 if(home_stream) 624 { 625 merge_personal(home_stream); 626 if(!personal_file) 627 { 628 personal_file = personal_home; 629 personal_home = NULL; 630 personal_make = 0; 631 personal_uid = real_uid; 632 } 633 else 634 do_personal = 1; 635 } 636 } 637 /*}}}*/ 638 /*{{{ check alternate*/ 639 if(alternate) 640 { 641 unsigned count; 642 unsigned ix; 643 HIGH_SCORE *sptr; 644 645 for(ix = 2; ix--;) 646 for(sptr = tables[ix], count = HIGH_SCORES; count-- && sptr->score; 647 sptr++) 648 if(!strcmp(sptr->name, alternate)) 649 { 650 strcpy(sptr->name, scoring.mine.name); 651 do_score = 1; 652 } 653 } 654 /*}}}*/ 655 /*{{{ check personal*/ 656 { 657 unsigned count; 658 HIGH_SCORE *sptr; 659 HIGH_SCORE *hptr; 660 661 for(count = HIGH_SCORES, hptr = scoring.high; 662 count-- && hptr->score; hptr++) 663 if(!strcmp(hptr->name, scoring.mine.name)) 664 { 665 if(hptr->score > scoring.personal[0].score) 666 do_personal |= insert_personal(hptr); 667 break; 668 } 669 if(hptr - scoring.high == HIGH_SCORES || !hptr->score) 670 hptr = NULL; 671 for(count = HIGH_SCORES, sptr = scoring.personal; 672 count-- && sptr->score; sptr++) 673 if(sptr->score >= SCORE_THRESHOLD && 674 score_file && (!hptr || sptr->score > hptr->score)) 675 { 676 do_personal = 1; 677 sptr->marker = 1; 678 } 679 remove_scores(scoring.personal, 1); 680 } 681 /*}}}*/ 682 /* expire person? */ 683 if(effective_uid != real_uid) 684 { 685 if(data.remove || data.format) 686 fprintf(stderr, "Not owner"); 687 data.remove = data.format = NULL; 688 } 689 else 690 { 691 /*{{{ set date format?*/ 692 if(data.format) 693 { 694 unsigned ix; 695 696 for(ix = 0; ix != 3; ix++) 697 if(!data.format[ix] || !strchr(date_formats, data.format[ix]) || 698 strchr(&data.format[ix + 1], data.format[ix])) 699 break; 700 if(data.format[3] || ix != 3) 701 fprintf(stderr, "Invalid format '%s'", data.format); 702 else 703 { 704 strcpy(date_format, data.format); 705 do_score = 1; 706 } 707 data.format = NULL; 708 } 709 /*}}}*/ 710 if(data.remove) 711 { 712 unsigned found; 713 unsigned index; 714 char CONST *truename; 715 char CONST *home; 716 unsigned personal; 717 718 personal = 0; 719 truename = home = NULL; 720 #ifndef TRANSPUTER 721 /*{{{ find name & home*/ 722 { 723 struct passwd *ptr; 724 725 ptr = getpwnam(data.remove); 726 if(ptr) 727 { 728 home = ptr->pw_dir; 729 truename = ptr->pw_gecos; 730 } 731 } 732 /*}}}*/ 733 #endif /* TRANSPUTER */ 734 /*{{{ search*/ 735 for(found = index = 0; index != 2; index++) 736 { 737 HIGH_SCORE *sptr; 738 unsigned count; 739 740 for(sptr = tables[index], count = HIGH_SCORES; count--; sptr++) 741 if(!sptr->score) 742 break; 743 else if(!strcmp(sptr->name, data.remove) || 744 (truename && !strcmp(sptr->name, truename))) 745 { 746 sptr->marker = 1; 747 remove_scores(tables[index], 1); 748 found = 1; 749 break; 750 } 751 } 752 /*}}}*/ 753 do_score |= found; 754 /*{{{ try personal files too*/ 755 { 756 /*{{{ score directory?*/ 757 if(data.dir) 758 { 759 size_t dirlen; 760 char *file; 761 762 dirlen = strlen(data.dir); 763 file = malloc(dirlen + 8 + strlen(data.remove)); 764 if(file) 765 { 766 strcpy(file, data.dir); 767 strcpy(&file[dirlen], "/xmris-"); 768 strcpy(&file[dirlen + 7], data.remove); 769 if(!unlink(file)) 770 { 771 personal = 1; 772 found = 1; 773 } 774 free(file); 775 } 776 } 777 /*}}}*/ 778 /*{{{ home directory?*/ 779 { 780 if(home) 781 { 782 char *file; 783 size_t length; 784 785 length = strlen(home); 786 file = malloc(length + 15); 787 if(file) 788 { 789 strcpy(file, home); 790 strcpy(&file[length], "/.xmris.score"); 791 if(!unlink(file)) 792 { 793 personal = 1; 794 found = 1; 795 } 796 free(file); 797 } 798 } 799 } 800 /*}}}*/ 801 } 802 /*}}}*/ 803 if(found) 804 printf("Removed '%s'%s\n", data.remove, 805 personal ? "" : ", no personal score table"); 806 else 807 printf("No entry or personal score table for '%s'\n", data.remove); 808 data.remove = NULL; 809 } 810 } 811 /*{{{ expire date?*/ 812 if(data.expire) 813 { 814 /* valid date separators are ' ' ':' '-' '/' 815 * day month year in any order, 816 * month can be ascii 817 * year can be 2 or 4 digits, 00-80 -> 2000-2080 818 * 81-99 -> 1981-1999 819 * uses date_format to resolve ambiguity 820 * or give n{years|months|weeks|days|hours|minutes|seconds}, 821 * to expire those older/younger than n days/weeks ago 822 * a leading sign specifies older ('+') or younger ('-'). default is older 823 */ 824 /*{{{ month names*/ 825 static char CONST *CONST months[] = 826 { 827 "January", "February", "March", "April", 828 "May", "June", "July", "August", 829 "September", "October", "November", "December", 830 NULL 831 }; 832 /*}}}*/ 833 /*{{{ typedef struct Scale*/ 834 typedef struct Scale 835 { 836 char CONST *name; 837 unsigned long factor; 838 } SCALE; 839 /*}}}*/ 840 /*{{{ time scales*/ 841 static SCALE CONST scales[] = 842 { 843 {"years", (365 * 4 + 1) * 6 * 60 * 60}, 844 {"months", 61 * 12 * 60 * 60}, 845 {"weeks", 7 * 24 * 60 * 60}, 846 {"days", 24 * 60 * 60}, 847 {"hours", 60 * 60}, 848 {"minutes", 60}, 849 {"seconds", 1}, 850 {NULL} 851 }; 852 /*}}}*/ 853 unsigned long seconds; 854 int values[3]; 855 int date[3]; 856 unsigned ix; 857 char CONST *ptr; 858 int error; 859 char format[4]; 860 int rel; 861 int after; 862 863 after = 1; 864 for(ix = 3; ix--;) 865 { 866 date[ix] = values[ix] = -1; 867 format[ix] = ' '; 868 } 869 format[3] = 0; 870 error = 0; 871 ptr = data.expire; 872 after = (*ptr == '-') - (*ptr == '+'); 873 if(after) 874 ptr++; 875 /*{{{ try relative*/ 876 { 877 char *tmp; 878 879 seconds = strtol(ptr, &tmp, 10); 880 if(*tmp) 881 { 882 SCALE CONST *sptr; 883 size_t length; 884 885 length = strlen(tmp); 886 for(sptr = scales; sptr->name; sptr++) 887 if(!strncmp(tmp, sptr->name, length)) 888 { 889 seconds *= sptr->factor; 890 tmp += length; 891 break; 892 } 893 } 894 else 895 seconds *= 24 * 60 * 60; 896 rel = !*tmp; 897 if(rel) 898 { 899 ptr = tmp; 900 after = after > 0; 901 } 902 } 903 /*}}}*/ 904 if(!rel) 905 { 906 int amb; 907 908 after = after < 0; 909 /*{{{ parse the string*/ 910 for(ix = 0; ix != 3; ix++) 911 { 912 size_t length; 913 int type; 914 int value; 915 916 length = 0; 917 value = type = -1; 918 if(isdigit(*ptr)) 919 /*{{{ number*/ 920 { 921 char *eptr; 922 923 value = (int)strtol(ptr, &eptr, 10); 924 length = eptr - ptr; 925 if(value > 99) 926 type = 2; 927 else if(value > 89) 928 { 929 type = 2; 930 value += 1900; 931 } 932 else if(value > 31 || !value) 933 { 934 type = 2; 935 value += 2000; 936 } 937 } 938 /*}}}*/ 939 else if(isalpha(*ptr)) 940 /*{{{ month*/ 941 { 942 char CONST * CONST *mptr; 943 944 while(isalpha(ptr[length])) 945 length++; 946 for(mptr = months; *mptr; mptr++) 947 if(length <= strlen(*mptr)) 948 { 949 size_t ix; 950 951 for(ix = 0; ix != length; ix++) 952 if(tolower((*mptr)[ix]) != tolower(ptr[length])) 953 break; 954 if(ix == length) 955 break; 956 } 957 if(*mptr) 958 { 959 type = 1; 960 value = mptr - months + 1; 961 } 962 else 963 error = 1; 964 } 965 /*}}}*/ 966 else 967 error = 1; 968 if(type < 0) 969 values[ix] = value; 970 else if(date[type] >= 0) 971 error = 1; 972 else 973 { 974 date[type] = value; 975 format[ix] = date_formats[type]; 976 } 977 ptr += length; 978 if(*ptr && !isalnum(*ptr) && ix != 2) 979 ptr++; 980 } 981 /*}}}*/ 982 amb = strchr(format, ' ') != NULL; 983 /*{{{ disambiguate*/ 984 { 985 int flag; 986 987 for(flag = 0; flag != 2; flag++) 988 { 989 /*{{{ use format*/ 990 for(ix = 0; ix != 3; ix++) 991 if(values[ix] > 0) 992 { 993 if(format[ix] == ' ' && 994 !strchr(format, date_format[ix])) 995 format[ix] = date_format[ix]; 996 switch(format[ix]) 997 { 998 case 'D': 999 date[0] = values[ix]; 1000 values[ix] = -1; 1001 break; 1002 case 'M': 1003 if(values[0] > 12) 1004 format[ix] = ' '; 1005 else 1006 { 1007 date[1] = values[ix]; 1008 values[ix] = -1; 1009 } 1010 break; 1011 case 'Y': 1012 date[2] = values[ix] + 1900; 1013 if(date[2] < 1990) 1014 date[2] += 100; 1015 values[ix] = -1; 1016 break; 1017 default: 1018 format[ix] = ' '; 1019 } 1020 } 1021 /*}}}*/ 1022 if(!flag) 1023 { 1024 char *ptr; 1025 1026 ptr = strchr(format, ' '); 1027 if(ptr && !strchr(ptr + 1, ' ')) 1028 { 1029 int temp; 1030 1031 for(temp = ' ', ix = 0; ix != 3; ix++) 1032 temp += date_formats[ix] - format[ix]; 1033 *ptr = temp; 1034 } 1035 else 1036 break; 1037 } 1038 } 1039 } 1040 /*}}}*/ 1041 if(amb && !strchr(format, ' ')) 1042 { 1043 char reply[5]; 1044 1045 printf("Ambiguous date '%s': accept %d %s %d y/n(y)?", 1046 data.expire, date[0], months[date[1] - 1], date[2]); 1047 fflush(stdout); 1048 if(!fgets(reply, sizeof(reply), stdin)) 1049 error = 1; 1050 else if(*reply != '\n' && uppercase(*reply) != 'Y') 1051 error = 1; 1052 } 1053 } 1054 error |= *ptr; 1055 if((!rel && strchr(format, ' ')) || error) 1056 fprintf(stderr, "Bad or ambiguous date '%s'\n", data.expire); 1057 else 1058 { 1059 time_t expire; 1060 unsigned changed; 1061 1062 changed = 0; 1063 if(rel) 1064 { 1065 expire = time((time_t *)NULL); 1066 expire -= seconds; 1067 } 1068 else 1069 { 1070 struct tm date_tm; 1071 1072 date_tm.tm_sec = 0; 1073 date_tm.tm_min = 0; 1074 date_tm.tm_hour = 0; 1075 date_tm.tm_yday = -1; 1076 date_tm.tm_mday = date[0]; 1077 date_tm.tm_mon = date[1] - 1; 1078 date_tm.tm_year = date[2] - 1900; 1079 expire = mktime(&date_tm); 1080 } 1081 if(expire == -1) 1082 fprintf(stderr, "Not a valid date\n"); 1083 else 1084 /*{{{ expire them*/ 1085 { 1086 unsigned ix; 1087 unsigned found; 1088 1089 for(found = 0, ix = 3; ix--;) 1090 { 1091 HIGH_SCORE *sptr; 1092 unsigned count; 1093 1094 for(sptr = tables[ix], count = HIGH_SCORES; 1095 count-- && sptr->score; sptr++) 1096 if(((ix == 2) || !strcmp(sptr->name, scoring.mine.name) || 1097 (alternate && !strcmp(sptr->name, alternate))) && 1098 (after ? expire < sptr->stamp : expire > sptr->stamp)) 1099 { 1100 sptr->marker = 1; 1101 found |= 1 << ix; 1102 } 1103 remove_scores(tables[ix], 1); 1104 } 1105 do_personal |= found & 4; 1106 /*{{{ write high_scores?*/ 1107 if(found & 3 || changed) 1108 { 1109 if(found & 1 && 1110 scoring.personal[0].score >= SCORE_THRESHOLD) 1111 { 1112 HIGH_SCORE score; 1113 1114 make_unique(&scoring.personal[0]); 1115 memcpy(&score, &scoring.personal[0], sizeof(score)); 1116 scoring.personal->stamp = score.stamp; 1117 strcpy(score.name, scoring.mine.name); 1118 insert_score(scoring.high, &score, 0); 1119 } 1120 do_score = 1; 1121 } 1122 /*}}}*/ 1123 } 1124 /*}}}*/ 1125 } 1126 data.expire = NULL; 1127 } 1128 /*}}}*/ 1129 /*{{{ insert?*/ 1130 if(insert) 1131 { 1132 HIGH_SCORE *inserted; 1133 1134 make_unique(&scoring.mine); 1135 inserted = NULL; 1136 if(insert_personal(&scoring.mine)) 1137 { 1138 inserted = scoring.personal; 1139 do_personal = 1; 1140 } 1141 if(insert_score(scoring.today, &scoring.mine, !score_file)) 1142 { 1143 inserted = scoring.today; 1144 do_score = 1; 1145 } 1146 if(scoring.mine.score >= SCORE_THRESHOLD && 1147 insert_score(scoring.high, &scoring.mine, !score_file)) 1148 { 1149 inserted = scoring.high; 1150 do_score = 1; 1151 } 1152 if(inserted) 1153 scoring.display = inserted; 1154 } 1155 /*}}}*/ 1156 /*{{{ write_score?*/ 1157 if(do_score) 1158 { 1159 if(!score_stream && score_file) 1160 { 1161 score_stream = get_lock(score_file, 5, effective_uid); 1162 if(!score_stream) 1163 score_file = NULL; 1164 } 1165 if(score_stream) 1166 write_scores(score_stream); 1167 } 1168 /*}}}*/ 1169 /*{{{ write personal?*/ 1170 if(do_personal) 1171 { 1172 if(!personal_stream && score_stream && personal_file) 1173 personal_stream = get_lock(personal_file, 3, personal_uid); 1174 if(personal_stream) 1175 { 1176 write_personal(personal_stream); 1177 if(personal_home) 1178 unlink(personal_home); 1179 personal_home = NULL; 1180 } 1181 else 1182 { 1183 if(!home_stream) 1184 home_stream = get_lock(personal_home, 1, real_uid); 1185 if(home_stream) 1186 write_personal(home_stream); 1187 else 1188 personal_home = NULL; 1189 personal_uid = real_uid; 1190 personal_file = personal_home; 1191 personal_home = NULL; 1192 } 1193 personal_make = 0; 1194 } 1195 /*}}}*/ 1196 if(personal_stream) 1197 get_unlock(personal_stream); 1198 if(home_stream) 1199 get_unlock(home_stream); 1200 if(score_stream) 1201 get_unlock(score_stream); 1202 if(do_score && score_file) 1203 file_changed(score_file, effective_uid); 1204 if(do_personal && personal_file) 1205 file_changed(personal_file, personal_uid); 1206 return; 1207 } 1208 /*}}}*/ 1209 /*{{{ void make_unique(sptr)*/ 1210 static VOIDFUNC make_unique 1211 FUNCARG((hptr), 1212 HIGH_SCORE *hptr 1213 ) 1214 /* makes sure that the score has a unique stamp 1215 * compared to the high and today score tables 1216 */ 1217 { 1218 HIGH_SCORE *sptr; 1219 unsigned ix; 1220 unsigned count; 1221 unsigned retry; 1222 1223 do 1224 { 1225 retry = 0; 1226 for(ix = 2; ix--;) 1227 for(sptr = tables[ix], count = HIGH_SCORES; 1228 count-- && sptr->score; sptr++) 1229 if(sptr->score == hptr->score && sptr->stamp == hptr->stamp) 1230 { 1231 hptr->stamp++; 1232 retry = 1; 1233 } 1234 } 1235 while(retry); 1236 return; 1237 } 1238 /*}}}*/ 1239 /*{{{ unsigned merge_personal(stream)*/ 1240 static unsigned merge_personal 1241 FUNCARG((stream), 1242 FILE *stream 1243 ) 1244 /* 1245 * loads the personal score file into the personal score 1246 * table. If the score file is corrupted, then we lose the 1247 * merged entries otherwise we lose the non-loaded entries. 1248 * The file must have been locked appropriately. 1249 * returns non-zero if the file is unreadable 1250 */ 1251 { 1252 unsigned long check; 1253 unsigned failed; 1254 HIGH_SCORE new; 1255 1256 clearerr(stream); 1257 rewind(stream); 1258 check = 0; 1259 scoring.personal[0].score = 0; 1260 /*{{{ read file*/ 1261 while(1) 1262 { 1263 check = read_line(stream, &new, check); 1264 if(!new.score) 1265 break; 1266 insert_score(scoring.personal, &new, 0); 1267 } 1268 /*}}}*/ 1269 failed = ferror(stream) || check != new.stamp; 1270 if(failed) 1271 { 1272 fprintf(stderr, "%s:Your personal score file '%s' has been corrupted.\n", 1273 myname, personal_file); 1274 scoring.personal[0].score = 0; 1275 } 1276 return failed; 1277 } 1278 /*}}}*/ 1279 /*{{{ unsigned merge_scores(stream)*/ 1280 static unsigned merge_scores 1281 FUNCARG((stream), 1282 FILE *stream 1283 ) 1284 /* 1285 * merges the high score file into the current high score 1286 * table. If the score file is corrupted, then we lose the 1287 * merged entries. The file must have been locked appropriately. 1288 * returns non-zero if the file is unreadable 1289 */ 1290 { 1291 unsigned long check; 1292 unsigned failed; 1293 time_t now; 1294 HIGH_SCORE new; 1295 1296 clearerr(stream); 1297 rewind(stream); 1298 now = time((time_t *)NULL); 1299 scoring.high[0].score = 0; 1300 scoring.today[0].score = 0; 1301 check = 0; 1302 /*{{{ read file*/ 1303 while(1) 1304 { 1305 check = read_line(stream, &new, check); 1306 if(!new.score) 1307 break; 1308 if(new.score >= SCORE_THRESHOLD) 1309 insert_score(scoring.high, &new, 0); 1310 if(!expire(now, new.stamp)) 1311 insert_score(scoring.today, &new, 0); 1312 } 1313 /*}}}*/ 1314 failed = ferror(stream) || check != new.stamp; 1315 if(failed) 1316 { 1317 fprintf(stderr, "%s:The high score file '%s' has been corrupted.\n", 1318 myname, score_file); 1319 scoring.high[0].score = 0; 1320 scoring.today[0].score = 0; 1321 } 1322 return failed; 1323 } 1324 /*}}}*/ 1325 /*{{{ unsigned long read_line(stream, sptr, check)*/ 1326 static unsigned long read_line 1327 FUNCARG((stream, sptr, check), 1328 FILE *stream 1329 ARGSEP HIGH_SCORE *sptr 1330 ARGSEP unsigned long check 1331 ) 1332 { 1333 char line[NAME_LEN + 40]; 1334 1335 if(!fgets(line, sizeof(line), stream)) 1336 sptr->score = sptr->stamp = 0; 1337 else if(line[0] == '+') 1338 { 1339 char *ptr; 1340 size_t index; 1341 1342 sptr->score = 0; 1343 sptr->stamp = strtol(&line[1], &ptr, 10); 1344 while(*ptr == ' ') 1345 ptr++; 1346 for(index = 0; isalpha(*ptr) && index < 3; ptr++, index++) 1347 date_format[index] = *ptr; 1348 if(*ptr != '\n') 1349 sptr->stamp = 0; 1350 } 1351 else 1352 { 1353 size_t length; 1354 char *name; 1355 char *ptr; 1356 1357 for(ptr = line; *ptr; ptr++) 1358 check += *(unsigned char *)ptr; 1359 sptr->stamp = strtol(line, &ptr, 10); 1360 while(*ptr == ' ') 1361 ptr++; 1362 sptr->score = strtol(ptr, &ptr, 10) / SCORE_ROUND * SCORE_ROUND; 1363 while(*ptr == ' ') 1364 ptr++; 1365 sptr->screen = (unsigned)strtol(ptr, &ptr, 10); 1366 while(*ptr == ' ') 1367 ptr++; 1368 sptr->elapsed = (unsigned)strtol(ptr, &name, 10); 1369 if(name == ptr) 1370 sptr->elapsed = 0; 1371 else 1372 for(ptr = name; *ptr == ' ';) 1373 ptr++; 1374 name = ptr; 1375 length = strlen(ptr) - 1; 1376 if(!sptr->score || !sptr->screen || 1377 ptr[length] != '\n' || length > NAME_LEN) 1378 sptr->score = 0; 1379 name[length] = 0; 1380 strcpy(sptr->name, name); 1381 sptr->marker = 1; 1382 } 1383 return check; 1384 } 1385 /*}}}*/ 1386 /*{{{ static VOIDFUNC remove_scores(base, flag)*/ 1387 static VOIDFUNC remove_scores 1388 FUNCARG((base, flag), 1389 HIGH_SCORE *base 1390 ARGSEP unsigned flag 1391 ) 1392 /* 1393 * throw out the marked scores, 'cos they're from a corrupt score file 1394 * or just too darn old 1395 */ 1396 { 1397 HIGH_SCORE *ptr; 1398 HIGH_SCORE *dest; 1399 unsigned ix; 1400 1401 for(ptr = base, ix = HIGH_SCORES; ix--; ptr++) 1402 { 1403 if(flag && ptr->marker) 1404 ptr->score = 0; 1405 ptr->marker = 0; 1406 } 1407 for(ptr = dest = base, ix = HIGH_SCORES; ix--; ptr++) 1408 if(!ptr->score) 1409 /* EMPTY */; 1410 else if(ptr == dest) 1411 dest++; 1412 else 1413 { 1414 memcpy(dest, ptr, sizeof(HIGH_SCORE)); 1415 dest++; 1416 } 1417 for(;dest != ptr; dest++) 1418 dest->score = 0; 1419 return; 1420 } 1421 /*}}}*/ 1422 /*{{{ static VOIDFUNC retire_scores()*/ 1423 static VOIDFUNC retire_scores FUNCARGVOID 1424 /* 1425 * gracefully retire the wrinkly scores 1426 */ 1427 { 1428 HIGH_SCORE *ptr; 1429 unsigned count; 1430 time_t now; 1431 1432 now = time((time_t *)NULL); 1433 for(ptr = scoring.today, count = 0; count != HIGH_SCORES && ptr->score; 1434 ptr++, count ++) 1435 ptr->marker = expire(now, ptr->stamp); 1436 remove_scores(scoring.today, 1); 1437 return; 1438 } 1439 /*}}}*/ 1440 /*{{{ void write_personal(stream)*/ 1441 static VOIDFUNC write_personal 1442 FUNCARG((stream), 1443 FILE *stream 1444 ) 1445 /* 1446 * writes out the personal score table to the file. 1447 * the file must have been locked appropriately. 1448 */ 1449 { 1450 unsigned long check; 1451 1452 clearerr(stream); 1453 rewind(stream); 1454 check = 0; 1455 check = write_table(stream, scoring.personal, check); 1456 if(ferror(stream)) 1457 { 1458 clearerr(stream); 1459 rewind(stream); 1460 check = 0; 1461 } 1462 fprintf(stream, "+%lu\n", check); 1463 return; 1464 } 1465 /*}}}*/ 1466 /*{{{ void write_scores(stream)*/ 1467 static VOIDFUNC write_scores 1468 FUNCARG((stream), 1469 FILE *stream 1470 ) 1471 /* 1472 * writes out the high score table to the file. 1473 * the file must have been locked appropriately. 1474 */ 1475 { 1476 unsigned long check; 1477 1478 clearerr(stream); 1479 rewind(stream); 1480 check = 0; 1481 check = write_table(stream, scoring.high, check); 1482 check = write_table(stream, scoring.today, check); 1483 if(ferror(stream)) 1484 { 1485 clearerr(stream); 1486 rewind(stream); 1487 check = 0; 1488 } 1489 fprintf(stream, "+%lu %s\n", check, date_format); 1490 return; 1491 } 1492 /*}}}*/ 1493 /*{{{ unsigned long write_table(stream, sptr, check)*/ 1494 static unsigned long write_table 1495 FUNCARG((stream, sptr, check), 1496 FILE *stream 1497 ARGSEP HIGH_SCORE *sptr 1498 ARGSEP unsigned long check 1499 ) 1500 { 1501 unsigned ix; 1502 1503 for(ix = HIGH_SCORES; ix-- && sptr->score; sptr++) 1504 { 1505 char line[NAME_LEN + 40]; 1506 char *ptr; 1507 1508 sprintf(line, "%lu %lu %u %u %s\n", (unsigned long)sptr->stamp, 1509 sptr->score, sptr->screen, sptr->elapsed, sptr->name); 1510 fputs(line, stream); 1511 for(ptr = line; *ptr; ptr++) 1512 check += *(unsigned char *)ptr; 1513 } 1514 return check; 1515 } 1516 /*}}}*/ 1517