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