1236099Sdes/*-
2236099Sdes * Copyright (c) 2012 Dag-Erling Sm��rgrav
3236099Sdes * All rights reserved.
4236099Sdes *
5236099Sdes * Redistribution and use in source and binary forms, with or without
6236099Sdes * modification, are permitted provided that the following conditions
7236099Sdes * are met:
8236099Sdes * 1. Redistributions of source code must retain the above copyright
9255376Sdes *    notice, this list of conditions and the following disclaimer.
10236099Sdes * 2. Redistributions in binary form must reproduce the above copyright
11236099Sdes *    notice, this list of conditions and the following disclaimer in the
12236099Sdes *    documentation and/or other materials provided with the distribution.
13236099Sdes * 3. The name of the author may not be used to endorse or promote
14236099Sdes *    products derived from this software without specific prior written
15236099Sdes *    permission.
16236099Sdes *
17236099Sdes * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18236099Sdes * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19236099Sdes * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20236099Sdes * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21236099Sdes * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22236099Sdes * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23236099Sdes * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24236099Sdes * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25236099Sdes * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26236099Sdes * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27236099Sdes * SUCH DAMAGE.
28236099Sdes *
29255376Sdes * $Id: openpam_readword.c 648 2013-03-05 17:54:27Z des $
30236099Sdes */
31236099Sdes
32236099Sdes#ifdef HAVE_CONFIG_H
33236099Sdes# include "config.h"
34236099Sdes#endif
35236099Sdes
36236099Sdes#include <errno.h>
37236099Sdes#include <stdio.h>
38236099Sdes#include <stdlib.h>
39236099Sdes
40236099Sdes#include <security/pam_appl.h>
41236099Sdes
42236099Sdes#include "openpam_impl.h"
43236099Sdes#include "openpam_ctype.h"
44236099Sdes
45236099Sdes#define MIN_WORD_SIZE	32
46236099Sdes
47236099Sdes/*
48236099Sdes * OpenPAM extension
49236099Sdes *
50236099Sdes * Read a word from a file, respecting shell quoting rules.
51236099Sdes */
52236099Sdes
53236099Sdeschar *
54236099Sdesopenpam_readword(FILE *f, int *lineno, size_t *lenp)
55236099Sdes{
56236099Sdes	char *word;
57236099Sdes	size_t size, len;
58273326Sdes	int ch, escape, quote;
59236099Sdes	int serrno;
60236099Sdes
61236099Sdes	errno = 0;
62236099Sdes
63236099Sdes	/* skip initial whitespace */
64273326Sdes	escape = quote = 0;
65273326Sdes	while ((ch = getc(f)) != EOF) {
66273326Sdes		if (ch == '\n') {
67273326Sdes			/* either EOL or line continuation */
68273326Sdes			if (!escape)
69273326Sdes				break;
70273326Sdes			if (lineno != NULL)
71273326Sdes				++*lineno;
72273326Sdes			escape = 0;
73273326Sdes		} else if (escape) {
74273326Sdes			/* escaped something else */
75236099Sdes			break;
76273326Sdes		} else if (ch == '#') {
77273326Sdes			/* comment: until EOL, no continuation */
78273326Sdes			while ((ch = getc(f)) != EOF)
79273326Sdes				if (ch == '\n')
80273326Sdes					break;
81273326Sdes			break;
82273326Sdes		} else if (ch == '\\') {
83273326Sdes			escape = 1;
84273326Sdes		} else if (!is_ws(ch)) {
85273326Sdes			break;
86273326Sdes		}
87236099Sdes	}
88236099Sdes	if (ch == EOF)
89236099Sdes		return (NULL);
90236099Sdes	ungetc(ch, f);
91236099Sdes	if (ch == '\n')
92236099Sdes		return (NULL);
93236099Sdes
94236099Sdes	word = NULL;
95236099Sdes	size = len = 0;
96236099Sdes	while ((ch = fgetc(f)) != EOF && (!is_ws(ch) || quote || escape)) {
97236099Sdes		if (ch == '\\' && !escape && quote != '\'') {
98236099Sdes			/* escape next character */
99236099Sdes			escape = ch;
100236099Sdes		} else if ((ch == '\'' || ch == '"') && !quote && !escape) {
101236099Sdes			/* begin quote */
102236099Sdes			quote = ch;
103236099Sdes			/* edge case: empty quoted string */
104247809Sdes			if (openpam_straddch(&word, &size, &len, 0) != 0)
105236099Sdes				return (NULL);
106236099Sdes		} else if (ch == quote && !escape) {
107236099Sdes			/* end quote */
108236099Sdes			quote = 0;
109273326Sdes		} else if (ch == '\n' && escape) {
110236099Sdes			/* line continuation */
111236099Sdes			escape = 0;
112236099Sdes		} else {
113236099Sdes			if (escape && quote && ch != '\\' && ch != quote &&
114236099Sdes			    openpam_straddch(&word, &size, &len, '\\') != 0) {
115236099Sdes				free(word);
116236099Sdes				errno = ENOMEM;
117236099Sdes				return (NULL);
118236099Sdes			}
119236099Sdes			if (openpam_straddch(&word, &size, &len, ch) != 0) {
120236099Sdes				free(word);
121236099Sdes				errno = ENOMEM;
122236099Sdes				return (NULL);
123236099Sdes			}
124236099Sdes			escape = 0;
125236099Sdes		}
126236099Sdes		if (lineno != NULL && ch == '\n')
127236099Sdes			++*lineno;
128236099Sdes	}
129236099Sdes	if (ch == EOF && ferror(f)) {
130236099Sdes		serrno = errno;
131236099Sdes		free(word);
132236099Sdes		errno = serrno;
133236099Sdes		return (NULL);
134236099Sdes	}
135236099Sdes	if (ch == EOF && (escape || quote)) {
136236099Sdes		/* Missing escaped character or closing quote. */
137236099Sdes		openpam_log(PAM_LOG_ERROR, "unexpected end of file");
138236099Sdes		free(word);
139236099Sdes		errno = EINVAL;
140236099Sdes		return (NULL);
141236099Sdes	}
142236099Sdes	ungetc(ch, f);
143236099Sdes	if (lenp != NULL)
144236099Sdes		*lenp = len;
145236099Sdes	return (word);
146236099Sdes}
147236099Sdes
148236099Sdes/**
149236099Sdes * The =openpam_readword function reads the next word from a file, and
150236099Sdes * returns it in a NUL-terminated buffer allocated with =!malloc.
151236099Sdes *
152236099Sdes * A word is a sequence of non-whitespace characters.
153236099Sdes * However, whitespace characters can be included in a word if quoted or
154236099Sdes * escaped according to the following rules:
155236099Sdes *
156236099Sdes *  - An unescaped single or double quote introduces a quoted string,
157236099Sdes *    which ends when the same quote character is encountered a second
158236099Sdes *    time.
159236099Sdes *    The quotes themselves are stripped.
160236099Sdes *
161236099Sdes *  - Within a single- or double-quoted string, all whitespace characters,
162236099Sdes *    including the newline character, are preserved as-is.
163236099Sdes *
164236099Sdes *  - Outside a quoted string, a backslash escapes the next character,
165236099Sdes *    which is preserved as-is, unless that character is a newline, in
166236099Sdes *    which case it is discarded and reading continues at the beginning of
167236099Sdes *    the next line as if the backslash and newline had not been there.
168236099Sdes *    In all cases, the backslash itself is discarded.
169236099Sdes *
170236099Sdes *  - Within a single-quoted string, double quotes and backslashes are
171236099Sdes *    preserved as-is.
172236099Sdes *
173236099Sdes *  - Within a double-quoted string, a single quote is preserved as-is,
174236099Sdes *    and a backslash is preserved as-is unless used to escape a double
175236099Sdes *    quote.
176236099Sdes *
177236099Sdes * In addition, if the first non-whitespace character on the line is a
178236099Sdes * hash character (#), the rest of the line is discarded.
179236099Sdes * If a hash character occurs within a word, however, it is preserved
180236099Sdes * as-is.
181236099Sdes * A backslash at the end of a comment does cause line continuation.
182236099Sdes *
183236099Sdes * If =lineno is not =NULL, the integer variable it points to is
184236099Sdes * incremented every time a quoted or escaped newline character is read.
185236099Sdes *
186236099Sdes * If =lenp is not =NULL, the length of the word (after quotes and
187236099Sdes * backslashes have been removed) is stored in the variable it points to.
188236099Sdes *
189236099Sdes * RETURN VALUES
190236099Sdes *
191236099Sdes * If successful, the =openpam_readword function returns a pointer to a
192236099Sdes * dynamically allocated NUL-terminated string containing the first word
193236099Sdes * encountered on the line.
194236099Sdes *
195236099Sdes * The caller is responsible for releasing the returned buffer by passing
196236099Sdes * it to =!free.
197236099Sdes *
198236099Sdes * If =openpam_readword reaches the end of the line or file before any
199236099Sdes * characters are copied to the word, it returns =NULL.  In the former
200236099Sdes * case, the newline is pushed back to the file.
201236099Sdes *
202236099Sdes * If =openpam_readword reaches the end of the file while a quote or
203236099Sdes * backslash escape is in effect, it sets :errno to =EINVAL and returns
204236099Sdes * =NULL.
205236099Sdes *
206236099Sdes * IMPLEMENTATION NOTES
207236099Sdes *
208236099Sdes * The parsing rules are intended to be equivalent to the normal POSIX
209236099Sdes * shell quoting rules.
210236099Sdes * Any discrepancy is a bug and should be reported to the author along
211236099Sdes * with sample input that can be used to reproduce the error.
212236099Sdes *
213236099Sdes * >openpam_readline
214236099Sdes * >openpam_readlinev
215236099Sdes *
216236099Sdes * AUTHOR DES
217236099Sdes */
218