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
);
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
,
92 static int write_scores (const char *filename
,
93 const struct score_entry
*scores
, ptrdiff_t count
);
96 lose (const char *msg
)
98 fprintf (stderr
, "%s\n", msg
);
102 static _Noreturn
void
103 lose_syserr (const char *msg
)
105 fprintf (stderr
, "%s: %s\n", msg
, strerror (errno
));
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 main (int argc
, char **argv
)
148 const char *prefix
, *user_prefix
= NULL
;
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
;
158 while ((c
= getopt (argc
, argv
, "hrm:d:")) != -1)
162 usage (EXIT_SUCCESS
);
165 user_prefix
= optarg
;
172 intmax_t m
= strtoimax (optarg
, 0, 10);
174 usage (EXIT_FAILURE
);
175 max_scores
= min (m
, MAX_SCORES
);
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);
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');
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
)
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");
248 read_score (FILE *f
, struct score_entry
*score
)
251 if ((c
= getc (f
)) != EOF
)
255 for (score
->score
= 0; (c
= getc (f
)) != EOF
&& isdigit (c
); )
257 if (INTMAX_MAX
/ 10 < score
->score
)
260 if (INTMAX_MAX
- (c
- '0') < score
->score
)
262 score
->score
+= c
- '0';
264 while ((c
= getc (f
)) != EOF
274 if (getdelim (&score
->username
, &count
, ' ', f
) < 1
275 || score
->username
== NULL
)
278 score
->username
[strlen (score
->username
)-1] = '\0';
282 ptrdiff_t unameread
= 0;
283 ptrdiff_t unamelen
= 30;
284 char *username
= malloc (unamelen
);
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)
295 else if (unamelen
< unamelen_max
)
296 unamelen
= unamelen_max
;
302 username
= realloc (username
, unamelen
);
306 username
[unameread
] = c
;
311 username
[unameread
] = '\0';
312 score
->username
= username
;
320 if (getline (&score
->data
, &len
, f
) < 0)
322 score
->data
[strlen (score
->data
)-1] = '\0';
328 char *buf
= malloc (len
);
331 while ((c
= getc (f
)) != EOF
336 if (min (PTRDIFF_MAX
, SIZE_MAX
) / 2 < len
)
341 if (!(buf
= realloc (buf
, len
*= 2)))
348 score
->data
[cur
] = '\0';
355 read_scores (const char *filename
, struct score_entry
**scores
,
356 ptrdiff_t *count
, ptrdiff_t *alloc
)
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");
367 while ((readval
= read_score (f
, &entry
)) == 0)
368 if (push_score (&ret
, &scorecount
, &cursize
, &entry
) < 0)
370 if (readval
> 0 && fclose (f
) == 0)
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
);
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
;
401 ptrdiff_t newsize
= *size
;
404 else if (newsize
<= MAX_SCORES
/ 2)
406 else if (newsize
< MAX_SCORES
)
407 newsize
= MAX_SCORES
;
413 newscores
= realloc (newscores
, sizeof *newscores
* newsize
);
419 newscores
[*count
] = *newscore
;
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
);
432 write_scores (const char *filename
, const struct score_entry
*scores
,
438 char *tempfile
= malloc (strlen (filename
) + strlen (".tempXXXXXX") + 1);
441 strcpy (tempfile
, filename
);
442 strcat (tempfile
, ".tempXXXXXX");
443 fd
= mkostemp (tempfile
, 0);
447 if (fchmod (fd
, 0644) != 0)
450 f
= fdopen (fd
, "w");
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
)
460 if (rename (tempfile
, filename
) != 0)
463 if (chmod (filename
, 0644) < 0)
470 lock_file (const char *filename
, void **state
)
475 const char *lockext
= ".lockfile";
476 char *lockpath
= malloc (strlen (filename
) + strlen (lockext
) + 60);
479 strcpy (lockpath
, filename
);
480 strcat (lockpath
, lockext
);
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
)
488 fd
= open (lockpath
, O_CREAT
| O_EXCL
, 0600);
493 /* Break the lock; we won't corrupt the file, but we might
495 if (attempts
> MAX_ATTEMPTS
)
500 sleep ((rand () % 2)+1);
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
;
518 errno
= ret
< 0 ? unlink_errno
: saved_errno
;
522 /* update-game-score.c ends here */