Hi all,
I was thrilled to find that the quiz for ed(1) commands, `function
ed-command` (as referenced in The UNIX Programming Enviroment), existed
in OpenBSD, but found that there was a bug where ~half of my correct
answers were not accepted.
Looking deeper, there is a bug in quiz(6) for datfiles with multi-line
answers.
Specifically, the following quiz.db line:
foo:\
bar
Is parsed into "foo:bar\n", which made it impossible to get right (since
the comparison was expecting a newline). This is a result of a bug in
quiz.c's appdstr(qp->q_text, lp, len), which copies the trailing newline
after "bar" (in lp) into q_text.
In appdstr, there was logic to strip the newline in the case of multiple
backslashes:
if (*(mp - 2) == '\\')
mp--;
*mp = '\0';
For example (foo:\\\nbar\\\nbaz\n) would have the second newline stripped
when appending bar to foo; since bar\\\n has a \\ before the newline, the
above code would roll back mp and truncate the \n. However, when baz is
appended, the check fails to find a \\ and the \n is kept, resulting in
"foo:barbaz\n".
The simplest fix for the bug is to modify this check to strip the newline
unconditionally:
if (*(mp - 1) == '\n')
mp--;
*mp = '\0';
That said, it seemed cleaner to modify the code to use getline(3) over
fgetln(3), which guarantees each iteration of the loop is a single line
and null-terminated, enabling the newline truncation in the loop and
allowing the use of strlcpy(3). This patch implements that fix.
One can verify the bug(fix) by:
$ cat <<EOF > /tmp/qtest
foo1:bar
foo2:\\
bar
foo3:\\
ba\\
r
EOF
$ echo "/tmp/qtest:test:lines" > /tmp/qindex
# answer 'bar' to each question
$ quiz -i /tmp/qindex test lines # fails
$ /usr/obj/games/quiz/quiz -i /tmp/qindex test lines # succeeds
Any and all feedback welcome!
Thanks,
Alex
diff --git games/quiz/quiz.c games/quiz/quiz.c
index 073c1700719..c70dabe617a 100644
--- games/quiz/quiz.c
+++ games/quiz/quiz.c
@@ -110,7 +110,8 @@ get_file(const char *file)
{
FILE *fp;
QE *qp;
- size_t len;
+ ssize_t len;
+ size_t size;
char *lp;
if ((fp = fopen(file, "r")) == NULL)
@@ -123,9 +124,11 @@ get_file(const char *file)
*/
qp = &qlist;
qsize = 0;
- while ((lp = fgetln(fp, &len)) != NULL) {
+ lp = NULL;
+ size = 0;
+ while ((len = getline(&lp, &size, fp)) != -1) {
if (lp[len - 1] == '\n')
- --len;
+ lp[len - 1] = '\0';
if (qp->q_text && qp->q_text[0] != '\0' &&
qp->q_text[strlen(qp->q_text) - 1] == '\\')
qp->q_text = appdstr(qp->q_text, lp, len);
@@ -135,14 +138,15 @@ get_file(const char *file)
qp = qp->q_next;
if ((qp->q_text = malloc(len + 1)) == NULL)
errx(1, "malloc");
- /* lp may not be zero-terminated; cannot use strlcpy */
- strncpy(qp->q_text, lp, len);
- qp->q_text[len] = '\0';
+ strlcpy(qp->q_text, lp, len + 1);
qp->q_asked = qp->q_answered = FALSE;
qp->q_next = NULL;
++qsize;
}
}
+ free(lp);
+ if (ferror(fp))
+ err(1, "getline");
(void)fclose(fp);
}
@@ -334,11 +338,9 @@ appdstr(char *s, const char *tp, size_t len)
if (*(mp - 1) == '\\')
--mp;
- while ((ch = *mp++ = *tp++) && ch != '\n')
+ /* tp guaranteed null-terminated, copy in full */
+ while ((ch = *mp++ = *tp++) != '\0')
;
- if (*(mp - 2) == '\\')
- mp--;
- *mp = '\0';
free(s);
return (m);