Backport the :end-of-capability fix
[emacs.git] / lib-src / update-game-score.c
blob09f2d4ff094d1426be438fdba6ce627e95650d2c
1 /* update-game-score.c --- Update a score file
3 Copyright (C) 2002-2015 Free Software Foundation, Inc.
5 Author: Colin Walters <walters@debian.org>
7 This file is part of GNU Emacs.
9 GNU Emacs is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
14 GNU Emacs is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. */
23 /* This program allows a game to securely and atomically update a
24 score file. It should be installed setuid, owned by an appropriate
25 user like `games'.
27 Alternatively, it can be compiled without HAVE_SHARED_GAME_DIR
28 defined, and in that case it will store scores in the user's home
29 directory (it should NOT be setuid).
31 Created 2002/03/22.
34 #include <config.h>
36 #include <unistd.h>
37 #include <errno.h>
38 #include <inttypes.h>
39 #include <limits.h>
40 #include <stdbool.h>
41 #include <string.h>
42 #include <stdlib.h>
43 #include <stdio.h>
44 #include <time.h>
45 #include <pwd.h>
46 #include <ctype.h>
47 #include <fcntl.h>
48 #include <sys/stat.h>
49 #include <getopt.h>
51 #ifdef WINDOWSNT
52 #include "ntlib.h"
53 #endif
55 #ifndef min
56 # define min(a,b) ((a) < (b) ? (a) : (b))
57 #endif
59 #define MAX_ATTEMPTS 5
60 #define MAX_DATA_LEN 1024
62 static _Noreturn void
63 usage (int err)
65 fprintf (stdout, "Usage: update-game-score [-m MAX] [-r] [-d DIR] game/scorefile SCORE DATA\n");
66 fprintf (stdout, " update-game-score -h\n");
67 fprintf (stdout, " -h\t\tDisplay this help.\n");
68 fprintf (stdout, " -m MAX\t\tLimit the maximum number of scores to MAX.\n");
69 fprintf (stdout, " -r\t\tSort the scores in increasing order.\n");
70 fprintf (stdout, " -d DIR\t\tStore scores in DIR (only if not setuid).\n");
71 exit (err);
74 static int lock_file (const char *filename, void **state);
75 static int unlock_file (const char *filename, void *state);
77 struct score_entry
79 intmax_t score;
80 char *username;
81 char *data;
84 #define MAX_SCORES min (PTRDIFF_MAX, SIZE_MAX / sizeof (struct score_entry))
86 static int read_scores (const char *filename, struct score_entry **scores,
87 ptrdiff_t *count, ptrdiff_t *alloc);
88 static int push_score (struct score_entry **scores, ptrdiff_t *count,
89 ptrdiff_t *size, struct score_entry const *newscore);
90 static void sort_scores (struct score_entry *scores, ptrdiff_t count,
91 bool reverse);
92 static int write_scores (const char *filename,
93 const struct score_entry *scores, ptrdiff_t count);
95 static _Noreturn void
96 lose (const char *msg)
98 fprintf (stderr, "%s\n", msg);
99 exit (EXIT_FAILURE);
102 static _Noreturn void
103 lose_syserr (const char *msg)
105 fprintf (stderr, "%s: %s\n", msg, strerror (errno));
106 exit (EXIT_FAILURE);
109 static char *
110 get_user_id (void)
112 struct passwd *buf = getpwuid (getuid ());
113 if (!buf || strchr (buf->pw_name, ' ') || strchr (buf->pw_name, '\n'))
115 intmax_t uid = getuid ();
116 char *name = malloc (sizeof uid * CHAR_BIT / 3 + 4);
117 if (name)
118 sprintf (name, "%"PRIdMAX, uid);
119 return name;
121 return buf->pw_name;
124 static const char *
125 get_prefix (bool running_suid, const char *user_prefix)
127 if (!running_suid && user_prefix == NULL)
128 lose ("Not using a shared game directory, and no prefix given.");
129 if (running_suid)
131 #ifdef HAVE_SHARED_GAME_DIR
132 return HAVE_SHARED_GAME_DIR;
133 #else
134 lose ("This program was compiled without HAVE_SHARED_GAME_DIR,\n and should not be suid.");
135 #endif
137 return user_prefix;
141 main (int argc, char **argv)
143 int c;
144 bool running_suid;
145 void *lockstate;
146 char *scorefile;
147 char *nl;
148 const char *prefix, *user_prefix = NULL;
149 struct stat buf;
150 struct score_entry *scores;
151 struct score_entry newscore;
152 bool reverse = false;
153 ptrdiff_t scorecount, scorealloc;
154 ptrdiff_t max_scores = MAX_SCORES;
156 srand (time (0));
158 while ((c = getopt (argc, argv, "hrm:d:")) != -1)
159 switch (c)
161 case 'h':
162 usage (EXIT_SUCCESS);
163 break;
164 case 'd':
165 user_prefix = optarg;
166 break;
167 case 'r':
168 reverse = 1;
169 break;
170 case 'm':
172 intmax_t m = strtoimax (optarg, 0, 10);
173 if (m < 0)
174 usage (EXIT_FAILURE);
175 max_scores = min (m, MAX_SCORES);
177 break;
178 default:
179 usage (EXIT_FAILURE);
182 if (argc - optind != 3)
183 usage (EXIT_FAILURE);
185 running_suid = (getuid () != geteuid ());
187 prefix = get_prefix (running_suid, user_prefix);
189 scorefile = malloc (strlen (prefix) + strlen (argv[optind]) + 2);
190 if (!scorefile)
191 lose_syserr ("Couldn't allocate score file");
193 strcpy (scorefile, prefix);
194 strcat (scorefile, "/");
195 strcat (scorefile, argv[optind]);
197 newscore.score = strtoimax (argv[optind + 1], 0, 10);
199 newscore.data = argv[optind + 2];
200 if (strlen (newscore.data) > MAX_DATA_LEN)
201 newscore.data[MAX_DATA_LEN] = '\0';
202 nl = strchr (newscore.data, '\n');
203 if (nl)
204 *nl = '\0';
206 newscore.username = get_user_id ();
207 if (! newscore.username)
208 lose_syserr ("Couldn't determine user id");
210 if (stat (scorefile, &buf) < 0)
211 lose_syserr ("Failed to access scores file");
213 if (lock_file (scorefile, &lockstate) < 0)
214 lose_syserr ("Failed to lock scores file");
216 if (read_scores (scorefile, &scores, &scorecount, &scorealloc) < 0)
218 unlock_file (scorefile, lockstate);
219 lose_syserr ("Failed to read scores file");
221 if (push_score (&scores, &scorecount, &scorealloc, &newscore) < 0)
223 unlock_file (scorefile, lockstate);
224 lose_syserr ("Failed to add score");
226 sort_scores (scores, scorecount, reverse);
227 /* Limit the number of scores. If we're using reverse sorting, then
228 also increment the beginning of the array, to skip over the
229 *smallest* scores. Otherwise, just decrementing the number of
230 scores suffices, since the smallest is at the end. */
231 if (scorecount > max_scores)
233 if (reverse)
234 scores += scorecount - max_scores;
235 scorecount = max_scores;
237 if (write_scores (scorefile, scores, scorecount) < 0)
239 unlock_file (scorefile, lockstate);
240 lose_syserr ("Failed to write scores file");
242 if (unlock_file (scorefile, lockstate) < 0)
243 lose_syserr ("Failed to unlock scores file");
244 exit (EXIT_SUCCESS);
247 static int
248 read_score (FILE *f, struct score_entry *score)
250 int c;
251 if ((c = getc (f)) != EOF)
252 ungetc (c, f);
253 if (feof (f))
254 return 1;
255 for (score->score = 0; (c = getc (f)) != EOF && isdigit (c); )
257 if (INTMAX_MAX / 10 < score->score)
258 return -1;
259 score->score *= 10;
260 if (INTMAX_MAX - (c - '0') < score->score)
261 return -1;
262 score->score += c - '0';
264 while ((c = getc (f)) != EOF
265 && isspace (c))
267 if (c == EOF)
268 return -1;
269 ungetc (c, f);
270 #ifdef HAVE_GETDELIM
272 size_t count = 0;
273 score->username = 0;
274 if (getdelim (&score->username, &count, ' ', f) < 1
275 || score->username == NULL)
276 return -1;
277 /* Trim the space */
278 score->username[strlen (score->username)-1] = '\0';
280 #else
282 ptrdiff_t unameread = 0;
283 ptrdiff_t unamelen = 30;
284 char *username = malloc (unamelen);
285 if (!username)
286 return -1;
288 while ((c = getc (f)) != EOF && c != ' ')
290 if (unameread >= unamelen - 1)
292 ptrdiff_t unamelen_max = min (PTRDIFF_MAX, SIZE_MAX);
293 if (unamelen <= unamelen_max / 2)
294 unamelen *= 2;
295 else if (unamelen < unamelen_max)
296 unamelen = unamelen_max;
297 else
299 errno = ENOMEM;
300 return -1;
302 username = realloc (username, unamelen);
303 if (!username)
304 return -1;
306 username[unameread] = c;
307 unameread++;
309 if (c == EOF)
310 return -1;
311 username[unameread] = '\0';
312 score->username = username;
314 #endif
315 #ifdef HAVE_GETLINE
316 score->data = NULL;
317 errno = 0;
319 size_t len;
320 if (getline (&score->data, &len, f) < 0)
321 return -1;
322 score->data[strlen (score->data)-1] = '\0';
324 #else
326 ptrdiff_t cur = 0;
327 ptrdiff_t len = 16;
328 char *buf = malloc (len);
329 if (!buf)
330 return -1;
331 while ((c = getc (f)) != EOF
332 && c != '\n')
334 if (cur >= len-1)
336 if (min (PTRDIFF_MAX, SIZE_MAX) / 2 < len)
338 errno = ENOMEM;
339 return -1;
341 if (!(buf = realloc (buf, len *= 2)))
342 return -1;
344 buf[cur] = c;
345 cur++;
347 score->data = buf;
348 score->data[cur] = '\0';
350 #endif
351 return 0;
354 static int
355 read_scores (const char *filename, struct score_entry **scores,
356 ptrdiff_t *count, ptrdiff_t *alloc)
358 int readval = -1;
359 ptrdiff_t scorecount = 0;
360 ptrdiff_t cursize = 0;
361 struct score_entry *ret = 0;
362 struct score_entry entry;
363 FILE *f = fopen (filename, "r");
364 int retval = -1;
365 if (!f)
366 return -1;
367 while ((readval = read_score (f, &entry)) == 0)
368 if (push_score (&ret, &scorecount, &cursize, &entry) < 0)
369 return -1;
370 if (readval > 0 && fclose (f) == 0)
372 *count = scorecount;
373 *alloc = cursize;
374 *scores = ret;
375 retval = 0;
377 return retval;
380 static int
381 score_compare (const void *a, const void *b)
383 const struct score_entry *sa = (const struct score_entry *) a;
384 const struct score_entry *sb = (const struct score_entry *) b;
385 return (sb->score > sa->score) - (sb->score < sa->score);
388 static int
389 score_compare_reverse (const void *a, const void *b)
391 return score_compare (b, a);
395 push_score (struct score_entry **scores, ptrdiff_t *count, ptrdiff_t *size,
396 struct score_entry const *newscore)
398 struct score_entry *newscores = *scores;
399 if (*count == *size)
401 ptrdiff_t newsize = *size;
402 if (newsize <= 0)
403 newsize = 1;
404 else if (newsize <= MAX_SCORES / 2)
405 newsize *= 2;
406 else if (newsize < MAX_SCORES)
407 newsize = MAX_SCORES;
408 else
410 errno = ENOMEM;
411 return -1;
413 newscores = realloc (newscores, sizeof *newscores * newsize);
414 if (!newscores)
415 return -1;
416 *scores = newscores;
417 *size = newsize;
419 newscores[*count] = *newscore;
420 (*count) += 1;
421 return 0;
424 static void
425 sort_scores (struct score_entry *scores, ptrdiff_t count, bool reverse)
427 qsort (scores, count, sizeof *scores,
428 reverse ? score_compare_reverse : score_compare);
431 static int
432 write_scores (const char *filename, const struct score_entry *scores,
433 ptrdiff_t count)
435 int fd;
436 FILE *f;
437 ptrdiff_t i;
438 char *tempfile = malloc (strlen (filename) + strlen (".tempXXXXXX") + 1);
439 if (!tempfile)
440 return -1;
441 strcpy (tempfile, filename);
442 strcat (tempfile, ".tempXXXXXX");
443 fd = mkostemp (tempfile, 0);
444 if (fd < 0)
445 return -1;
446 #ifndef DOS_NT
447 if (fchmod (fd, 0644) != 0)
448 return -1;
449 #endif
450 f = fdopen (fd, "w");
451 if (! f)
452 return -1;
453 for (i = 0; i < count; i++)
454 if (fprintf (f, "%"PRIdMAX" %s %s\n",
455 scores[i].score, scores[i].username, scores[i].data)
456 < 0)
457 return -1;
458 if (fclose (f) != 0)
459 return -1;
460 if (rename (tempfile, filename) != 0)
461 return -1;
462 #ifdef DOS_NT
463 if (chmod (filename, 0644) < 0)
464 return -1;
465 #endif
466 return 0;
469 static int
470 lock_file (const char *filename, void **state)
472 int fd;
473 struct stat buf;
474 int attempts = 0;
475 const char *lockext = ".lockfile";
476 char *lockpath = malloc (strlen (filename) + strlen (lockext) + 60);
477 if (!lockpath)
478 return -1;
479 strcpy (lockpath, filename);
480 strcat (lockpath, lockext);
481 *state = lockpath;
482 trylock:
483 attempts++;
484 /* If the lock is over an hour old, delete it. */
485 if (stat (lockpath, &buf) == 0
486 && 60 * 60 < time (0) - buf.st_ctime)
487 unlink (lockpath);
488 fd = open (lockpath, O_CREAT | O_EXCL, 0600);
489 if (fd < 0)
491 if (errno == EEXIST)
493 /* Break the lock; we won't corrupt the file, but we might
494 lose some scores. */
495 if (attempts > MAX_ATTEMPTS)
497 unlink (lockpath);
498 attempts = 0;
500 sleep ((rand () % 2)+1);
501 goto trylock;
503 else
504 return -1;
506 close (fd);
507 return 0;
510 static int
511 unlock_file (const char *filename, void *state)
513 char *lockpath = (char *) state;
514 int saved_errno = errno;
515 int ret = unlink (lockpath);
516 int unlink_errno = errno;
517 free (lockpath);
518 errno = ret < 0 ? unlink_errno : saved_errno;
519 return ret;
522 /* update-game-score.c ends here */