1 /* update-game-score.c --- Update a score file
3 Copyright (C) 2002-2014 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
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).
56 # define min(a,b) ((a) < (b) ? (a) : (b))
59 #define MAX_ATTEMPTS 5
60 #define MAX_DATA_LEN 1024
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");
74 static int lock_file (const char *filename
, void **state
);
75 static int unlock_file (const char *filename
, void *state
);
83 #define MAX_SCORES min (PTRDIFF_MAX, SIZE_MAX / sizeof (struct score_entry))
85 static int read_scores (const char *filename
, struct score_entry
**scores
,
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
,
91 static int write_scores (const char *filename
,
92 const struct score_entry
*scores
, ptrdiff_t count
);
95 lose (const char *msg
)
97 fprintf (stderr
, "%s\n", msg
);
101 static _Noreturn
void
102 lose_syserr (const char *msg
)
104 fprintf (stderr
, "%s: %s\n", msg
,
105 errno
? strerror (errno
) : "Invalid data in score file");
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);
118 sprintf (name
, "%"PRIdMAX
, uid
);
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.");
131 #ifdef HAVE_SHARED_GAME_DIR
132 return HAVE_SHARED_GAME_DIR
;
134 lose ("This program was compiled without HAVE_SHARED_GAME_DIR,\n and should not be suid.");
141 normalize_integer (char *num
)
145 while (*num
!= '\n' && isspace (*num
))
148 num
+= neg
|| *num
== '-';
152 while (*++num
== '0')
158 for (p
= num
; '0' <= *p
&& *p
<= '9'; p
++)
173 main (int argc
, char **argv
)
179 char *end
, *nl
, *user
, *data
;
180 const char *prefix
, *user_prefix
= NULL
;
181 struct score_entry
*scores
;
182 struct score_entry newscore
;
183 bool reverse
= false;
184 ptrdiff_t scorecount
, scorealloc
;
185 ptrdiff_t max_scores
= MAX_SCORES
;
189 while ((c
= getopt (argc
, argv
, "hrm:d:")) != -1)
193 usage (EXIT_SUCCESS
);
196 user_prefix
= optarg
;
203 intmax_t m
= strtoimax (optarg
, &end
, 10);
204 if (optarg
== end
|| *end
|| m
< 0)
205 usage (EXIT_FAILURE
);
206 max_scores
= min (m
, MAX_SCORES
);
210 usage (EXIT_FAILURE
);
213 if (argc
- optind
!= 3)
214 usage (EXIT_FAILURE
);
216 running_suid
= (getuid () != geteuid ());
218 prefix
= get_prefix (running_suid
, user_prefix
);
220 scorefile
= malloc (strlen (prefix
) + strlen (argv
[optind
]) + 2);
222 lose_syserr ("Couldn't allocate score file");
224 strcpy (scorefile
, prefix
);
225 strcat (scorefile
, "/");
226 strcat (scorefile
, argv
[optind
]);
228 newscore
.score
= normalize_integer (argv
[optind
+ 1]);
229 if (! newscore
.score
)
231 fprintf (stderr
, "%s: Invalid score\n", argv
[optind
+ 1]);
235 user
= get_user_id ();
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');
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");
249 if (lock_file (scorefile
, &lockstate
) < 0)
250 lose_syserr ("Failed to lock scores file");
252 if (read_scores (scorefile
, &scores
, &scorecount
, &scorealloc
) < 0)
254 unlock_file (scorefile
, lockstate
);
255 lose_syserr ("Failed to read scores file");
257 if (push_score (&scores
, &scorecount
, &scorealloc
, &newscore
) < 0)
259 unlock_file (scorefile
, lockstate
);
260 lose_syserr ("Failed to add score");
262 sort_scores (scores
, scorecount
, reverse
);
263 /* Limit the number of scores. If we're using reverse sorting, then
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. */
267 if (scorecount
> max_scores
)
270 scores
+= scorecount
- max_scores
;
271 scorecount
= max_scores
;
273 if (write_scores (scorefile
, scores
, scorecount
) < 0)
275 unlock_file (scorefile
, lockstate
);
276 lose_syserr ("Failed to write scores file");
278 if (unlock_file (scorefile
, lockstate
) < 0)
279 lose_syserr ("Failed to unlock scores file");
284 read_score (char *p
, struct score_entry
*score
)
291 score
->user_data
= p
;
292 p
= strchr (p
, '\n');
300 read_scores (const char *filename
, struct score_entry
**scores
,
301 ptrdiff_t *count
, ptrdiff_t *alloc
)
304 ptrdiff_t filesize
, nread
;
306 FILE *f
= fopen (filename
, "r");
309 if (fstat (fileno (f
), &st
) != 0)
311 if (! (0 <= st
.st_size
&& st
.st_size
< min (PTRDIFF_MAX
, SIZE_MAX
)))
316 filesize
= st
.st_size
;
317 filedata
= malloc (filesize
+ 1);
320 nread
= fread (filedata
, 1, filesize
+ 1, f
);
321 if (filesize
< nread
)
326 if (nread
< filesize
)
328 if (ferror (f
) || fclose (f
) != 0)
330 filedata
[filesize
] = 0;
331 if (strlen (filedata
) != filesize
)
339 for (p
= filedata
; p
< filedata
+ filesize
; )
341 struct score_entry entry
;
342 p
= read_score (p
, &entry
);
348 if (push_score (scores
, count
, alloc
, &entry
) < 0)
355 score_compare (const void *a
, const void *b
)
357 const struct score_entry
*sa
= (const struct score_entry
*) a
;
358 const struct score_entry
*sb
= (const struct score_entry
*) b
;
359 char *sca
= sa
->score
;
360 char *scb
= sb
->score
;
362 bool nega
= *sca
== '-';
363 bool negb
= *scb
== '-';
364 int diff
= nega
- negb
;
376 return lenb
< lena
? -1 : 1;
377 return strcmp (scb
, sca
);
381 score_compare_reverse (const void *a
, const void *b
)
383 return score_compare (b
, a
);
387 push_score (struct score_entry
**scores
, ptrdiff_t *count
, ptrdiff_t *size
,
388 struct score_entry
const *newscore
)
390 struct score_entry
*newscores
= *scores
;
393 ptrdiff_t newsize
= *size
;
396 else if (newsize
<= MAX_SCORES
/ 2)
398 else if (newsize
< MAX_SCORES
)
399 newsize
= MAX_SCORES
;
405 newscores
= realloc (newscores
, sizeof *newscores
* newsize
);
411 newscores
[*count
] = *newscore
;
417 sort_scores (struct score_entry
*scores
, ptrdiff_t count
, bool reverse
)
419 qsort (scores
, count
, sizeof *scores
,
420 reverse
? score_compare_reverse
: score_compare
);
424 write_scores (const char *filename
, const struct score_entry
*scores
,
430 char *tempfile
= malloc (strlen (filename
) + strlen (".tempXXXXXX") + 1);
433 strcpy (tempfile
, filename
);
434 strcat (tempfile
, ".tempXXXXXX");
435 fd
= mkostemp (tempfile
, 0);
439 if (fchmod (fd
, 0644) != 0)
442 f
= fdopen (fd
, "w");
445 for (i
= 0; i
< count
; i
++)
446 if (fprintf (f
, "%s %s\n", scores
[i
].score
, scores
[i
].user_data
) < 0)
450 if (rename (tempfile
, filename
) != 0)
456 lock_file (const char *filename
, void **state
)
461 const char *lockext
= ".lockfile";
462 char *lockpath
= malloc (strlen (filename
) + strlen (lockext
) + 60);
465 strcpy (lockpath
, filename
);
466 strcat (lockpath
, lockext
);
469 while ((fd
= open (lockpath
, O_CREAT
| O_EXCL
, 0600)) < 0)
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
))
481 if (unlink (lockpath
) != 0 && errno
!= ENOENT
)
486 sleep ((rand () & 1) + 1);
494 unlock_file (const char *filename
, void *state
)
496 char *lockpath
= (char *) state
;
497 int saved_errno
= errno
;
498 int ret
= unlink (lockpath
);
499 int unlink_errno
= errno
;
501 errno
= ret
< 0 ? unlink_errno
: saved_errno
;
505 /* update-game-score.c ends here */