update-game-score.c 10.5 KB
Newer Older
Colin Walters's avatar
Colin Walters committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/* update-game-score.c --- Update a score file
   Copyright (C) 2002 Free Software Foundation, Inc.

This file is part of GNU Emacs.

GNU Emacs is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

GNU Emacs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Emacs; see the file COPYING.  If not, write to
the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.  */

/* This program is allows a game to securely and atomically update a
Colin Walters's avatar
Colin Walters committed
22 23 24 25 26 27
   score file.  It should be installed setuid, owned by an appropriate
   user like `games'.

   Alternatively, it can be compiled without HAVE_SHARED_GAME_DIR
   defined, and in that case it will store scores in the user's home
   directory (it should NOT be setuid).
Colin Walters's avatar
Colin Walters committed
28 29 30 31 32 33 34 35 36 37 38 39

   Created 2002/03/22, by Colin Walters <walters@debian.org>
*/

#define _GNU_SOURCE

#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
Colin Walters's avatar
Colin Walters committed
40
#include <pwd.h>
Colin Walters's avatar
Colin Walters committed
41 42
#include <ctype.h>
#include <fcntl.h>
Colin Walters's avatar
Colin Walters committed
43
#include <stdarg.h>
Colin Walters's avatar
Colin Walters committed
44
#include <sys/stat.h>
Colin Walters's avatar
Colin Walters committed
45
#include <config.h>
Colin Walters's avatar
Colin Walters committed
46 47

#define MAX_ATTEMPTS 5
Colin Walters's avatar
Colin Walters committed
48 49
#define MAX_SCORES 200
#define MAX_DATA_LEN 1024
50 51 52 53

#ifdef HAVE_SHARED_GAME_DIR
#define SCORE_FILE_PREFIX HAVE_SHARED_GAME_DIR
#else
Colin Walters's avatar
Colin Walters committed
54 55 56 57 58
#define SCORE_FILE_PREFIX "~/.emacs.d/games"
#endif

#if !defined (__GNUC__) || __GNUC__ < 2
#define __attribute__(x) 
59
#endif
Colin Walters's avatar
Colin Walters committed
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

int
usage(int err)
{
  fprintf(stdout, "Usage: update-game-score [-m MAX ] [ -r ] game/scorefile SCORE DATA\n");
  fprintf(stdout, "       update-game-score -h\n");
  fprintf(stdout, " -h\t\tDisplay this help.\n");
  fprintf(stdout, " -m MAX\t\tLimit the maximum number of scores to MAX.\n");
  fprintf(stdout, " -r\t\tSort the scores in increasing order.\n");
  exit(err);
}

int
lock_file(const char *filename, void **state);
int
unlock_file(const char *filename, void *state);

struct score_entry
{
  long score;
Colin Walters's avatar
Colin Walters committed
80
  char *username;
Colin Walters's avatar
Colin Walters committed
81 82 83 84 85 86 87 88
  char *data;
};

int
read_scores(const char *filename, struct score_entry **scores,
	    int *count);
int
push_score(struct score_entry **scores, int *count,
Colin Walters's avatar
Colin Walters committed
89
	   int newscore, char *username, char *newdata);
Colin Walters's avatar
Colin Walters committed
90 91 92 93 94 95
void
sort_scores(struct score_entry *scores, int count, int reverse);
int
write_scores(const char *filename, const struct score_entry *scores,
	     int count);

Colin Walters's avatar
Colin Walters committed
96
char *
97
get_user_id(struct passwd *buf)
Colin Walters's avatar
Colin Walters committed
98 99 100 101 102 103
{
  char *name;
  if (!buf)
    {
      int count = 1;
      int uid = (int) getuid();
Colin Walters's avatar
Colin Walters committed
104 105
      int tuid = uid;
      while (tuid /= 10)
Colin Walters's avatar
Colin Walters committed
106 107 108 109 110 111 112 113
	count++;
      name = malloc(count+1);
      sprintf(name, "%d", uid);
      return name;
    }
  return buf->pw_name;
}

114 115 116 117 118 119 120 121
char *
get_home_dir(struct passwd *buf)
{
  if (!buf)
    return NULL;
  return buf->pw_dir;
}

Colin Walters's avatar
Colin Walters committed
122 123 124 125 126 127 128 129 130 131 132 133
void lose(const char *msg, ...)
     __attribute__ ((format (printf,1,0), noreturn));

void lose(const char *msg, ...)
{
    va_list ap;
    va_start(ap, msg);
    vfprintf(stderr, msg, ap);
    va_end(ap);
    exit(1);
}

Colin Walters's avatar
Colin Walters committed
134 135 136 137 138
int
main(int argc, char **argv)
{
  int c;
  void *lockstate;
139
  char *scorefile, *prefix;
Colin Walters's avatar
Colin Walters committed
140 141
  struct stat buf;
  struct score_entry *scores;
Colin Walters's avatar
Colin Walters committed
142
  int newscore, scorecount, reverse = 0, max = MAX_SCORES;
Colin Walters's avatar
Colin Walters committed
143
  char *newdata;
144
  struct passwd *passwdbuf;
Colin Walters's avatar
Colin Walters committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158

  srand(time(0));

  while ((c = getopt(argc, argv, "hrm:")) != -1)
    switch (c)
      {
      case 'h':
	usage(0);
	break;
      case 'r':
	reverse = 1;
	break;
      case 'm':
	max = atoi(optarg);
Colin Walters's avatar
Colin Walters committed
159 160
	if (max > MAX_SCORES)
	  max = MAX_SCORES;
Colin Walters's avatar
Colin Walters committed
161 162 163 164 165 166 167
	break;
      default:
	usage(1);
      }

  if (optind+3 != argc)
    usage(1);
168 169 170

  passwdbuf = getpwuid(getuid());

Colin Walters's avatar
Colin Walters committed
171
  if (!strncmp(SCORE_FILE_PREFIX, "~", 1))
172
    {
Colin Walters's avatar
Colin Walters committed
173 174 175 176 177 178 179
      char *homedir = get_home_dir(passwdbuf);
      if (!homedir)
	lose("Unable to determine home directory\n");
      prefix = malloc(strlen(homedir) + strlen(SCORE_FILE_PREFIX) + 1);
      strcpy(prefix, homedir);
      /* Skip over the '~'. */
      strcat(prefix, SCORE_FILE_PREFIX+1);
180 181
    }
  else
Colin Walters's avatar
Colin Walters committed
182 183 184 185 186 187
    prefix = strdup(SCORE_FILE_PREFIX);

  if (!prefix)
    lose("Couldn't create score file name: %s\n", strerror(errno));

  scorefile = malloc(strlen(prefix) + strlen(argv[optind]) + 2);
Colin Walters's avatar
Colin Walters committed
188
  if (!scorefile)
Colin Walters's avatar
Colin Walters committed
189 190
    lose("Couldn't create score file name: %s\n", strerror(errno));

191
  strcpy(scorefile, prefix);
Colin Walters's avatar
Colin Walters committed
192 193
  free(prefix);
  strcat(scorefile, "/");
Colin Walters's avatar
Colin Walters committed
194 195 196
  strcat(scorefile, argv[optind]);
  newscore = atoi(argv[optind+1]);
  newdata = argv[optind+2];
Colin Walters's avatar
Colin Walters committed
197 198
  if (strlen(newdata) > MAX_DATA_LEN)
    newdata[MAX_DATA_LEN] = '\0';
Colin Walters's avatar
Colin Walters committed
199 200
  
  if (stat(scorefile, &buf) < 0)
Colin Walters's avatar
Colin Walters committed
201 202
    lose("Failed to access scores file \"%s\": %s\n", scorefile,
	 strerror(errno));
Colin Walters's avatar
Colin Walters committed
203
  if (lock_file(scorefile, &lockstate) < 0)
Colin Walters's avatar
Colin Walters committed
204 205
      lose("Failed to lock scores file \"%s\": %s\n",
	   scorefile, strerror(errno));
Colin Walters's avatar
Colin Walters committed
206 207
  if (read_scores(scorefile, &scores, &scorecount) < 0)
    {
Colin Walters's avatar
Colin Walters committed
208 209 210
      unlock_file(scorefile, lockstate);
      lose("Failed to read scores file \"%s\": %s\n", scorefile,
	   strerror(errno));
Colin Walters's avatar
Colin Walters committed
211
    }
212
  push_score(&scores, &scorecount, newscore, get_user_id(passwdbuf), newdata);
Colin Walters's avatar
Colin Walters committed
213 214 215 216 217 218 219 220
  /* Limit the number of scores.  If we're using reverse sorting, then
     we should increment the beginning of the array, to skip over the
     *smallest* scores.  Otherwise, we just decrement the number of
     scores, since the smallest will be at the end. */
  if (scorecount > MAX_SCORES)
    scorecount -= (scorecount - MAX_SCORES);
    if (reverse)
      scores += (scorecount - MAX_SCORES);
Colin Walters's avatar
Colin Walters committed
221 222 223
  sort_scores(scores, scorecount, reverse);
  if (write_scores(scorefile, scores, scorecount) < 0)
    {
Colin Walters's avatar
Colin Walters committed
224 225 226
      unlock_file(scorefile, lockstate);
      lose("Failed to write scores file \"%s\": %s\n", scorefile,
	   strerror(errno));
Colin Walters's avatar
Colin Walters committed
227 228 229 230 231 232 233 234 235 236 237 238
    }
  unlock_file(scorefile, lockstate);
  exit(0);
}

int
read_score(FILE *f, struct score_entry *score)
{
  int c;
  if (feof(f))
    return 1;
  while ((c = getc(f)) != EOF
Colin Walters's avatar
Colin Walters committed
239 240 241 242 243 244 245 246 247 248
	 && isdigit(c))
    {
      score->score *= 10;
      score->score += (c-48);
    }
  while ((c = getc(f)) != EOF
	 && isspace(c))
    ;
  if (c == EOF)
    return -1;
249
  ungetc(c, f);
Colin Walters's avatar
Colin Walters committed
250 251 252 253 254 255
#ifdef HAVE_GETDELIM
  {
    int count = 0;
    if (getdelim(&score->username, &count, ' ', f) < 1
	|| score->username == NULL)
      return -1;
Colin Walters's avatar
Colin Walters committed
256
  }
Colin Walters's avatar
Colin Walters committed
257 258 259 260
#else
  {
    int unameread = 0;
    int unamelen = 30;
261 262 263
    char *username = malloc(unamelen);
    if (!username)
      return -1;
Colin Walters's avatar
Colin Walters committed
264 265 266 267
    
    while ((c = getc(f)) != EOF
	   && !isspace(c))
      {
Colin Walters's avatar
Colin Walters committed
268 269 270
	if (unameread >= unamelen-1)
	  if (!(username = realloc(username, unamelen *= 2)))
	    return -1;
Colin Walters's avatar
Colin Walters committed
271 272 273
	username[unameread] = c;
	unameread++;
      }
274 275 276
    if (c == EOF)    
      return -1;
    username[unameread] = '\0';
Colin Walters's avatar
Colin Walters committed
277 278 279
    score->username = username;
  }
#endif
Colin Walters's avatar
Colin Walters committed
280 281 282 283 284 285 286
#ifdef HAVE_GETLINE
  score->data = NULL;
  errno = ESUCCES;
  {
    int len;
    if (getline(&score->data, &len, f) < 0)
      return -1;
287
    score->data[strlen(score->data)-1] = '\0';
Colin Walters's avatar
Colin Walters committed
288 289
  }
#else
Colin Walters's avatar
Colin Walters committed
290 291 292 293 294 295
  {
    int cur = 0;
    int len = 16;
    char *buf = malloc(len);
    if (!buf)
      return -1;
296 297
    while ((c = getc(f)) != EOF
	   && c != '\n')
Colin Walters's avatar
Colin Walters committed
298 299 300 301 302 303 304 305 306 307
      {
	if (cur >= len-1)
	  {
	    if (!(buf = realloc(buf, len *= 2)))
	      return -1;
	  }
	buf[cur] = c;
	cur++;
      }
    score->data = buf;
308
    score->data[cur] = '\0';
Colin Walters's avatar
Colin Walters committed
309
  }
Colin Walters's avatar
Colin Walters committed
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
#endif
  return 0;
}

int
read_scores(const char *filename, struct score_entry **scores,
	    int *count)
{
  int readval, scorecount, cursize;
  struct score_entry *ret;
  FILE *f = fopen(filename, "r");
  if (!f) 
    return -1;
  scorecount = 0;
  cursize = 16;
  ret = malloc(sizeof(struct score_entry) * cursize);
  if (!ret) 
    return -1;
  while ((readval = read_score(f, &ret[scorecount])) == 0)
    {
      /* We encoutered an error */
      if (readval < 0)
	return -1;
      scorecount++;
      if (scorecount >= cursize)
	{
	  ret = realloc(ret, cursize *= 2);
	  if (!ret)
	    return -1;
	}
    }
  *count = scorecount;
  *scores = ret;
  return 0;
}

int
score_compare(const void *a, const void *b)
{
  const struct score_entry *sa = (const struct score_entry *) a;
  const struct score_entry *sb = (const struct score_entry *) b;
  return (sb->score > sa->score) - (sb->score < sa->score);
}

int
score_compare_reverse(const void *a, const void *b)
{
  const struct score_entry *sa = (const struct score_entry *) a;
  const struct score_entry *sb = (const struct score_entry *) b;
  return (sa->score > sb->score) - (sa->score < sb->score);
}

int
push_score(struct score_entry **scores, int *count,
Colin Walters's avatar
Colin Walters committed
364
	   int newscore, char *username, char *newdata) 
Colin Walters's avatar
Colin Walters committed
365 366 367 368 369 370
{
 struct score_entry *newscores = realloc(*scores,
					 sizeof(struct score_entry) * ((*count) + 1));
  if (!newscores)
    return -1;
  newscores[*count].score = newscore;
Colin Walters's avatar
Colin Walters committed
371
  newscores[*count].username = username;
Colin Walters's avatar
Colin Walters committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
  newscores[*count].data = newdata;
  (*count) += 1;
  *scores = newscores;
  return 0;
}
  
void
sort_scores(struct score_entry *scores, int count, int reverse)
{
  qsort(scores, count, sizeof(struct score_entry),
	reverse ? score_compare_reverse : score_compare);
}

int
write_scores(const char *filename, const struct score_entry *scores,
	     int count)
{
  FILE *f;  
  int i;
  char *tempfile = malloc(strlen(filename) + strlen(".tempXXXXXX") + 1);
  if (!tempfile)
    return -1;
  strcpy(tempfile, filename);
  strcat(tempfile, ".tempXXXXXX");
Colin Walters's avatar
Colin Walters committed
396
#ifdef HAVE_MKSTEMP
Colin Walters's avatar
Colin Walters committed
397
  if (mkstemp(tempfile) < 0
Colin Walters's avatar
Colin Walters committed
398 399 400
#else
  if (mktemp(tempfile) != tempfile
#endif
Colin Walters's avatar
Colin Walters committed
401 402 403
      || !(f = fopen(tempfile, "w")))
    return -1;
  for (i = 0; i < count; i++)
Colin Walters's avatar
Colin Walters committed
404 405
    if (fprintf(f, "%ld %s %s\n", scores[i].score, scores[i].username,
		scores[i].data) < 0)
Colin Walters's avatar
Colin Walters committed
406 407
      return -1;
  fclose(f);
Colin Walters's avatar
Colin Walters committed
408
  if (rename(tempfile, filename) < 0)
Colin Walters's avatar
Colin Walters committed
409
    return -1;
Colin Walters's avatar
Colin Walters committed
410
  if (chmod(filename, 0644) < 0)
Colin Walters's avatar
Colin Walters committed
411
    return -1;
Colin Walters's avatar
Colin Walters committed
412
  return 0;
Colin Walters's avatar
Colin Walters committed
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
}

int
lock_file(const char *filename, void **state)
{
  int fd;
  int attempts = 0;
  char *lockext = ".lockfile";
  char *lockpath = malloc(strlen(filename) + strlen(lockext) + 60);
  if (!lockpath)
    return -1;
  strcpy(lockpath, filename);
  strcat(lockpath, lockext);
  *state = lockpath;
 trylock:
  attempts++;
  if ((fd = open(lockpath, O_CREAT | O_EXCL, 0600)) < 0)
    {
      if (errno == EEXIST)
	{
	  /* Break the lock; we won't corrupt the file, but we might
	     lose some scores. */
	  if (attempts > MAX_ATTEMPTS)
	    unlink(lockpath);
Colin Walters's avatar
Colin Walters committed
437
	  sleep((rand() % 2)+1);
Colin Walters's avatar
Colin Walters committed
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
	  goto trylock;
	}
      else
	return -1;
    }
  close(fd);
  return 0;
}

int
unlock_file(const char *filename, void *state)
{
  char *lockpath = (char *) state;
  int ret = unlink(lockpath);
  int saved_errno = errno;
  free(lockpath);
  errno = saved_errno;
  return ret;
}