Commit | Line | Data |
---|---|---|
2f1de3dd | 1 | /* update-game-score.c --- Update a score file |
294981c7 | 2 | |
ba318903 | 3 | Copyright (C) 2002-2014 Free Software Foundation, Inc. |
294981c7 GM |
4 | |
5 | Author: Colin Walters <walters@debian.org> | |
2f1de3dd CW |
6 | |
7 | This file is part of GNU Emacs. | |
8 | ||
294981c7 | 9 | GNU Emacs is free software: you can redistribute it and/or modify |
2f1de3dd | 10 | it under the terms of the GNU General Public License as published by |
294981c7 GM |
11 | the Free Software Foundation, either version 3 of the License, or |
12 | (at your option) any later version. | |
2f1de3dd CW |
13 | |
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. | |
18 | ||
19 | You should have received a copy of the GNU General Public License | |
294981c7 GM |
20 | along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. */ |
21 | ||
2f1de3dd | 22 | |
89cc1591 | 23 | /* This program allows a game to securely and atomically update a |
8eec804f CW |
24 | score file. It should be installed setuid, owned by an appropriate |
25 | user like `games'. | |
26 | ||
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). | |
2f1de3dd | 30 | |
294981c7 | 31 | Created 2002/03/22. |
2f1de3dd CW |
32 | */ |
33 | ||
c42d6dbd EZ |
34 | #include <config.h> |
35 | ||
2f1de3dd CW |
36 | #include <unistd.h> |
37 | #include <errno.h> | |
d70efef4 | 38 | #include <inttypes.h> |
0c6d656d | 39 | #include <limits.h> |
d70efef4 | 40 | #include <stdbool.h> |
2f1de3dd CW |
41 | #include <string.h> |
42 | #include <stdlib.h> | |
43 | #include <stdio.h> | |
44 | #include <time.h> | |
cd553ffb | 45 | #include <pwd.h> |
2f1de3dd CW |
46 | #include <ctype.h> |
47 | #include <fcntl.h> | |
48 | #include <sys/stat.h> | |
15142f27 | 49 | #include <getopt.h> |
5b400482 | 50 | |
7c4026b6 EZ |
51 | #ifdef WINDOWSNT |
52 | #include "ntlib.h" | |
53 | #endif | |
54 | ||
d70efef4 PE |
55 | #ifndef min |
56 | # define min(a,b) ((a) < (b) ? (a) : (b)) | |
57 | #endif | |
58 | ||
2f1de3dd | 59 | #define MAX_ATTEMPTS 5 |
8eec804f | 60 | #define MAX_DATA_LEN 1024 |
5795b420 | 61 | |
845ca893 | 62 | static _Noreturn void |
873fbd0b | 63 | usage (int err) |
2f1de3dd | 64 | { |
ec3b5374 | 65 | fprintf (stdout, "Usage: update-game-score [-m MAX] [-r] [-d DIR] game/scorefile SCORE DATA\n"); |
a5d107f3 RS |
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); | |
2f1de3dd CW |
72 | } |
73 | ||
b23b5a5b PE |
74 | static int lock_file (const char *filename, void **state); |
75 | static int unlock_file (const char *filename, void *state); | |
2f1de3dd CW |
76 | |
77 | struct score_entry | |
78 | { | |
60fc70a8 PE |
79 | char *score; |
80 | char *user_data; | |
2f1de3dd CW |
81 | }; |
82 | ||
d70efef4 PE |
83 | #define MAX_SCORES min (PTRDIFF_MAX, SIZE_MAX / sizeof (struct score_entry)) |
84 | ||
b23b5a5b | 85 | static int read_scores (const char *filename, struct score_entry **scores, |
d70efef4 PE |
86 | ptrdiff_t *count, ptrdiff_t *alloc); |
87 | static int push_score (struct score_entry **scores, ptrdiff_t *count, | |
88 | ptrdiff_t *size, struct score_entry const *newscore); | |
89 | static void sort_scores (struct score_entry *scores, ptrdiff_t count, | |
90 | bool reverse); | |
b23b5a5b | 91 | static int write_scores (const char *filename, |
d70efef4 | 92 | const struct score_entry *scores, ptrdiff_t count); |
cf398788 | 93 | |
845ca893 | 94 | static _Noreturn void |
873fbd0b | 95 | lose (const char *msg) |
cf398788 | 96 | { |
a5d107f3 | 97 | fprintf (stderr, "%s\n", msg); |
65396510 | 98 | exit (EXIT_FAILURE); |
cf398788 | 99 | } |
2f1de3dd | 100 | |
845ca893 | 101 | static _Noreturn void |
873fbd0b | 102 | lose_syserr (const char *msg) |
d99fabf0 | 103 | { |
60fc70a8 PE |
104 | fprintf (stderr, "%s: %s\n", msg, |
105 | errno ? strerror (errno) : "Invalid data in score file"); | |
65396510 | 106 | exit (EXIT_FAILURE); |
d99fabf0 CW |
107 | } |
108 | ||
b23b5a5b | 109 | static char * |
f57e2426 | 110 | get_user_id (void) |
cd553ffb | 111 | { |
a5d107f3 | 112 | struct passwd *buf = getpwuid (getuid ()); |
d70efef4 | 113 | if (!buf || strchr (buf->pw_name, ' ') || strchr (buf->pw_name, '\n')) |
cd553ffb | 114 | { |
d70efef4 PE |
115 | intmax_t uid = getuid (); |
116 | char *name = malloc (sizeof uid * CHAR_BIT / 3 + 4); | |
0c6d656d | 117 | if (name) |
d70efef4 | 118 | sprintf (name, "%"PRIdMAX, uid); |
cd553ffb CW |
119 | return name; |
120 | } | |
121 | return buf->pw_name; | |
122 | } | |
123 | ||
b23b5a5b | 124 | static const char * |
d70efef4 | 125 | get_prefix (bool running_suid, const char *user_prefix) |
8eec804f | 126 | { |
d99fabf0 | 127 | if (!running_suid && user_prefix == NULL) |
a5d107f3 | 128 | lose ("Not using a shared game directory, and no prefix given."); |
d99fabf0 CW |
129 | if (running_suid) |
130 | { | |
131 | #ifdef HAVE_SHARED_GAME_DIR | |
132 | return HAVE_SHARED_GAME_DIR; | |
133 | #else | |
a5d107f3 | 134 | lose ("This program was compiled without HAVE_SHARED_GAME_DIR,\n and should not be suid."); |
d99fabf0 CW |
135 | #endif |
136 | } | |
137 | return user_prefix; | |
8eec804f CW |
138 | } |
139 | ||
60fc70a8 PE |
140 | static char * |
141 | normalize_integer (char *num) | |
142 | { | |
143 | bool neg; | |
144 | char *p; | |
145 | while (*num != '\n' && isspace (*num)) | |
146 | num++; | |
147 | neg = *num == '-'; | |
148 | num += neg || *num == '-'; | |
149 | ||
150 | if (*num == '0') | |
151 | { | |
152 | while (*++num == '0') | |
153 | continue; | |
154 | neg &= !!*num; | |
155 | num -= !*num; | |
156 | } | |
157 | ||
158 | for (p = num; '0' <= *p && *p <= '9'; p++) | |
159 | continue; | |
160 | ||
161 | if (*p || p == num) | |
162 | { | |
163 | errno = 0; | |
164 | return 0; | |
165 | } | |
166 | ||
167 | if (neg) | |
168 | *--num = '-'; | |
169 | return num; | |
170 | } | |
171 | ||
2f1de3dd | 172 | int |
873fbd0b | 173 | main (int argc, char **argv) |
2f1de3dd | 174 | { |
d70efef4 PE |
175 | int c; |
176 | bool running_suid; | |
2f1de3dd | 177 | void *lockstate; |
d70efef4 | 178 | char *scorefile; |
60fc70a8 | 179 | char *end, *nl, *user, *data; |
988e88ab | 180 | const char *prefix, *user_prefix = NULL; |
2f1de3dd | 181 | struct score_entry *scores; |
d70efef4 PE |
182 | struct score_entry newscore; |
183 | bool reverse = false; | |
184 | ptrdiff_t scorecount, scorealloc; | |
185 | ptrdiff_t max_scores = MAX_SCORES; | |
2f1de3dd | 186 | |
a5d107f3 | 187 | srand (time (0)); |
2f1de3dd | 188 | |
a5d107f3 | 189 | while ((c = getopt (argc, argv, "hrm:d:")) != -1) |
2f1de3dd CW |
190 | switch (c) |
191 | { | |
192 | case 'h': | |
65396510 | 193 | usage (EXIT_SUCCESS); |
2f1de3dd | 194 | break; |
d99fabf0 CW |
195 | case 'd': |
196 | user_prefix = optarg; | |
197 | break; | |
2f1de3dd CW |
198 | case 'r': |
199 | reverse = 1; | |
200 | break; | |
201 | case 'm': | |
d70efef4 | 202 | { |
60fc70a8 PE |
203 | intmax_t m = strtoimax (optarg, &end, 10); |
204 | if (optarg == end || *end || m < 0) | |
d70efef4 PE |
205 | usage (EXIT_FAILURE); |
206 | max_scores = min (m, MAX_SCORES); | |
207 | } | |
2f1de3dd CW |
208 | break; |
209 | default: | |
65396510 | 210 | usage (EXIT_FAILURE); |
2f1de3dd CW |
211 | } |
212 | ||
d70efef4 | 213 | if (argc - optind != 3) |
65396510 | 214 | usage (EXIT_FAILURE); |
5795b420 | 215 | |
a5d107f3 | 216 | running_suid = (getuid () != geteuid ()); |
5795b420 | 217 | |
a5d107f3 | 218 | prefix = get_prefix (running_suid, user_prefix); |
8eec804f | 219 | |
a5d107f3 | 220 | scorefile = malloc (strlen (prefix) + strlen (argv[optind]) + 2); |
2f1de3dd | 221 | if (!scorefile) |
a5d107f3 | 222 | lose_syserr ("Couldn't allocate score file"); |
8eec804f | 223 | |
a5d107f3 RS |
224 | strcpy (scorefile, prefix); |
225 | strcat (scorefile, "/"); | |
226 | strcat (scorefile, argv[optind]); | |
d99fabf0 | 227 | |
60fc70a8 PE |
228 | newscore.score = normalize_integer (argv[optind + 1]); |
229 | if (! newscore.score) | |
230 | { | |
231 | fprintf (stderr, "%s: Invalid score\n", argv[optind + 1]); | |
232 | return EXIT_FAILURE; | |
233 | } | |
d70efef4 | 234 | |
60fc70a8 PE |
235 | user = get_user_id (); |
236 | if (! user) | |
237 | lose_syserr ("Couldn't determine user id"); | |
238 | data = argv[optind + 2]; | |
239 | if (strlen (data) > MAX_DATA_LEN) | |
240 | data[MAX_DATA_LEN] = '\0'; | |
241 | nl = strchr (data, '\n'); | |
d70efef4 PE |
242 | if (nl) |
243 | *nl = '\0'; | |
60fc70a8 PE |
244 | newscore.user_data = malloc (strlen (user) + 1 + strlen (data) + 1); |
245 | if (! newscore.user_data | |
246 | || sprintf (newscore.user_data, "%s %s", user, data) < 0) | |
247 | lose_syserr ("Memory exhausted"); | |
177c0ea7 | 248 | |
a5d107f3 RS |
249 | if (lock_file (scorefile, &lockstate) < 0) |
250 | lose_syserr ("Failed to lock scores file"); | |
177c0ea7 | 251 | |
d70efef4 | 252 | if (read_scores (scorefile, &scores, &scorecount, &scorealloc) < 0) |
2f1de3dd | 253 | { |
a5d107f3 RS |
254 | unlock_file (scorefile, lockstate); |
255 | lose_syserr ("Failed to read scores file"); | |
2f1de3dd | 256 | } |
d70efef4 PE |
257 | if (push_score (&scores, &scorecount, &scorealloc, &newscore) < 0) |
258 | { | |
259 | unlock_file (scorefile, lockstate); | |
260 | lose_syserr ("Failed to add score"); | |
261 | } | |
24e9e996 | 262 | sort_scores (scores, scorecount, reverse); |
8eec804f | 263 | /* Limit the number of scores. If we're using reverse sorting, then |
f0d80d43 PE |
264 | also increment the beginning of the array, to skip over the |
265 | *smallest* scores. Otherwise, just decrementing the number of | |
266 | scores suffices, since the smallest is at the end. */ | |
d70efef4 | 267 | if (scorecount > max_scores) |
f0d80d43 PE |
268 | { |
269 | if (reverse) | |
d70efef4 PE |
270 | scores += scorecount - max_scores; |
271 | scorecount = max_scores; | |
f0d80d43 | 272 | } |
a5d107f3 | 273 | if (write_scores (scorefile, scores, scorecount) < 0) |
2f1de3dd | 274 | { |
a5d107f3 RS |
275 | unlock_file (scorefile, lockstate); |
276 | lose_syserr ("Failed to write scores file"); | |
2f1de3dd | 277 | } |
d70efef4 PE |
278 | if (unlock_file (scorefile, lockstate) < 0) |
279 | lose_syserr ("Failed to unlock scores file"); | |
65396510 | 280 | exit (EXIT_SUCCESS); |
2f1de3dd CW |
281 | } |
282 | ||
60fc70a8 PE |
283 | static char * |
284 | read_score (char *p, struct score_entry *score) | |
2f1de3dd | 285 | { |
60fc70a8 PE |
286 | score->score = p; |
287 | p = strchr (p, ' '); | |
288 | if (!p) | |
289 | return p; | |
290 | *p++ = 0; | |
291 | score->user_data = p; | |
292 | p = strchr (p, '\n'); | |
293 | if (!p) | |
294 | return p; | |
295 | *p++ = 0; | |
296 | return p; | |
2f1de3dd CW |
297 | } |
298 | ||
b23b5a5b | 299 | static int |
d70efef4 PE |
300 | read_scores (const char *filename, struct score_entry **scores, |
301 | ptrdiff_t *count, ptrdiff_t *alloc) | |
2f1de3dd | 302 | { |
60fc70a8 PE |
303 | char *p, *filedata; |
304 | ptrdiff_t filesize, nread; | |
305 | struct stat st; | |
a5d107f3 | 306 | FILE *f = fopen (filename, "r"); |
177c0ea7 | 307 | if (!f) |
2f1de3dd | 308 | return -1; |
60fc70a8 PE |
309 | if (fstat (fileno (f), &st) != 0) |
310 | return -1; | |
311 | if (! (0 <= st.st_size && st.st_size < min (PTRDIFF_MAX, SIZE_MAX))) | |
312 | { | |
313 | errno = EOVERFLOW; | |
314 | return -1; | |
315 | } | |
316 | filesize = st.st_size; | |
317 | filedata = malloc (filesize + 1); | |
318 | if (! filedata) | |
319 | return -1; | |
320 | nread = fread (filedata, 1, filesize + 1, f); | |
321 | if (filesize < nread) | |
322 | { | |
323 | errno = 0; | |
d70efef4 | 324 | return -1; |
60fc70a8 PE |
325 | } |
326 | if (nread < filesize) | |
327 | filesize = nread; | |
328 | if (ferror (f) || fclose (f) != 0) | |
329 | return -1; | |
330 | filedata[filesize] = 0; | |
331 | if (strlen (filedata) != filesize) | |
98a428c1 | 332 | { |
60fc70a8 PE |
333 | errno = 0; |
334 | return -1; | |
98a428c1 | 335 | } |
60fc70a8 PE |
336 | |
337 | *scores = 0; | |
338 | *count = *alloc = 0; | |
339 | for (p = filedata; p < filedata + filesize; ) | |
340 | { | |
341 | struct score_entry entry; | |
342 | p = read_score (p, &entry); | |
343 | if (!p) | |
344 | { | |
345 | errno = 0; | |
346 | return -1; | |
347 | } | |
348 | if (push_score (scores, count, alloc, &entry) < 0) | |
349 | return -1; | |
350 | } | |
351 | return 0; | |
2f1de3dd CW |
352 | } |
353 | ||
b23b5a5b | 354 | static int |
873fbd0b | 355 | score_compare (const void *a, const void *b) |
2f1de3dd CW |
356 | { |
357 | const struct score_entry *sa = (const struct score_entry *) a; | |
358 | const struct score_entry *sb = (const struct score_entry *) b; | |
60fc70a8 PE |
359 | char *sca = sa->score; |
360 | char *scb = sb->score; | |
361 | size_t lena, lenb; | |
362 | bool nega = *sca == '-'; | |
363 | bool negb = *scb == '-'; | |
364 | int diff = nega - negb; | |
365 | if (diff) | |
366 | return diff; | |
367 | if (nega) | |
368 | { | |
369 | char *tmp = sca; | |
370 | sca = scb + 1; | |
371 | scb = tmp + 1; | |
372 | } | |
373 | lena = strlen (sca); | |
374 | lenb = strlen (scb); | |
375 | if (lena != lenb) | |
376 | return lenb < lena ? -1 : 1; | |
377 | return strcmp (scb, sca); | |
2f1de3dd CW |
378 | } |
379 | ||
b23b5a5b | 380 | static int |
873fbd0b | 381 | score_compare_reverse (const void *a, const void *b) |
2f1de3dd | 382 | { |
d70efef4 | 383 | return score_compare (b, a); |
2f1de3dd CW |
384 | } |
385 | ||
386 | int | |
d70efef4 PE |
387 | push_score (struct score_entry **scores, ptrdiff_t *count, ptrdiff_t *size, |
388 | struct score_entry const *newscore) | |
2f1de3dd | 389 | { |
d70efef4 PE |
390 | struct score_entry *newscores = *scores; |
391 | if (*count == *size) | |
392 | { | |
393 | ptrdiff_t newsize = *size; | |
394 | if (newsize <= 0) | |
395 | newsize = 1; | |
396 | else if (newsize <= MAX_SCORES / 2) | |
397 | newsize *= 2; | |
398 | else if (newsize < MAX_SCORES) | |
399 | newsize = MAX_SCORES; | |
400 | else | |
401 | { | |
402 | errno = ENOMEM; | |
403 | return -1; | |
404 | } | |
405 | newscores = realloc (newscores, sizeof *newscores * newsize); | |
406 | if (!newscores) | |
407 | return -1; | |
408 | *scores = newscores; | |
409 | *size = newsize; | |
410 | } | |
411 | newscores[*count] = *newscore; | |
2f1de3dd | 412 | (*count) += 1; |
2f1de3dd CW |
413 | return 0; |
414 | } | |
177c0ea7 | 415 | |
b23b5a5b | 416 | static void |
d70efef4 | 417 | sort_scores (struct score_entry *scores, ptrdiff_t count, bool reverse) |
2f1de3dd | 418 | { |
d70efef4 PE |
419 | qsort (scores, count, sizeof *scores, |
420 | reverse ? score_compare_reverse : score_compare); | |
2f1de3dd CW |
421 | } |
422 | ||
b23b5a5b | 423 | static int |
d70efef4 PE |
424 | write_scores (const char *filename, const struct score_entry *scores, |
425 | ptrdiff_t count) | |
2f1de3dd | 426 | { |
e0fdb694 | 427 | int fd; |
177c0ea7 | 428 | FILE *f; |
d70efef4 | 429 | ptrdiff_t i; |
a5d107f3 | 430 | char *tempfile = malloc (strlen (filename) + strlen (".tempXXXXXX") + 1); |
2f1de3dd CW |
431 | if (!tempfile) |
432 | return -1; | |
a5d107f3 RS |
433 | strcpy (tempfile, filename); |
434 | strcat (tempfile, ".tempXXXXXX"); | |
e0fdb694 PE |
435 | fd = mkostemp (tempfile, 0); |
436 | if (fd < 0) | |
437 | return -1; | |
bf6b4923 | 438 | #ifndef DOS_NT |
dc217d01 PE |
439 | if (fchmod (fd, 0644) != 0) |
440 | return -1; | |
7a49c9d6 | 441 | #endif |
e0fdb694 PE |
442 | f = fdopen (fd, "w"); |
443 | if (! f) | |
2f1de3dd CW |
444 | return -1; |
445 | for (i = 0; i < count; i++) | |
60fc70a8 | 446 | if (fprintf (f, "%s %s\n", scores[i].score, scores[i].user_data) < 0) |
2f1de3dd | 447 | return -1; |
dc217d01 | 448 | if (fclose (f) != 0) |
2f1de3dd | 449 | return -1; |
dc217d01 | 450 | if (rename (tempfile, filename) != 0) |
2f1de3dd | 451 | return -1; |
8eec804f | 452 | return 0; |
2f1de3dd | 453 | } |
177c0ea7 | 454 | |
b23b5a5b | 455 | static int |
873fbd0b | 456 | lock_file (const char *filename, void **state) |
2f1de3dd CW |
457 | { |
458 | int fd; | |
7c4f6873 | 459 | struct stat buf; |
2f1de3dd | 460 | int attempts = 0; |
988e88ab | 461 | const char *lockext = ".lockfile"; |
a5d107f3 | 462 | char *lockpath = malloc (strlen (filename) + strlen (lockext) + 60); |
2f1de3dd CW |
463 | if (!lockpath) |
464 | return -1; | |
a5d107f3 RS |
465 | strcpy (lockpath, filename); |
466 | strcat (lockpath, lockext); | |
2f1de3dd | 467 | *state = lockpath; |
60fc70a8 PE |
468 | |
469 | while ((fd = open (lockpath, O_CREAT | O_EXCL, 0600)) < 0) | |
2f1de3dd | 470 | { |
60fc70a8 PE |
471 | if (errno != EEXIST) |
472 | return -1; | |
473 | attempts++; | |
474 | ||
475 | /* Break the lock if it is over an hour old, or if we've tried | |
476 | more than MAX_ATTEMPTS times. We won't corrupt the file, but | |
477 | we might lose some scores. */ | |
478 | if (MAX_ATTEMPTS < attempts | |
479 | || (stat (lockpath, &buf) == 0 && 60 * 60 < time (0) - buf.st_ctime)) | |
2f1de3dd | 480 | { |
60fc70a8 PE |
481 | if (unlink (lockpath) != 0 && errno != ENOENT) |
482 | return -1; | |
483 | attempts = 0; | |
2f1de3dd | 484 | } |
60fc70a8 PE |
485 | |
486 | sleep ((rand () & 1) + 1); | |
2f1de3dd | 487 | } |
60fc70a8 | 488 | |
a5d107f3 | 489 | close (fd); |
2f1de3dd CW |
490 | return 0; |
491 | } | |
177c0ea7 | 492 | |
b23b5a5b | 493 | static int |
873fbd0b | 494 | unlock_file (const char *filename, void *state) |
2f1de3dd CW |
495 | { |
496 | char *lockpath = (char *) state; | |
2f1de3dd | 497 | int saved_errno = errno; |
d70efef4 PE |
498 | int ret = unlink (lockpath); |
499 | int unlink_errno = errno; | |
a5d107f3 | 500 | free (lockpath); |
d70efef4 | 501 | errno = ret < 0 ? unlink_errno : saved_errno; |
2f1de3dd CW |
502 | return ret; |
503 | } | |
ab5796a9 | 504 | |
65396510 | 505 | /* update-game-score.c ends here */ |