Home | History | Annotate | Line # | Download | only in util
      1 /*	$NetBSD: readlline.c,v 1.3 2025/02/25 19:15:52 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	readlline 3
      6 /* SUMMARY
      7 /*	read logical line
      8 /* SYNOPSIS
      9 /*	#include <readlline.h>
     10 /*
     11 /*	VSTRING	*readllines(buf, fp, lineno, first_line)
     12 /*	VSTRING	*buf;
     13 /*	VSTREAM	*fp;
     14 /*	int	*lineno;
     15 /*	int	*first_line;
     16 /*
     17 /*	VSTRING	*readlline(buf, fp, lineno)
     18 /*	VSTRING	*buf;
     19 /*	VSTREAM	*fp;
     20 /*	int	*lineno;
     21 /* DESCRIPTION
     22 /*	readllines() reads one logical line from the named stream.
     23 /* .IP "blank lines and comments"
     24 /*	Empty lines and whitespace-only lines are ignored, as
     25 /*	are lines whose first non-whitespace character is a `#'.
     26 /* .IP "multi-line text"
     27 /*	A logical line starts with non-whitespace text. A line that
     28 /*	starts with whitespace continues a logical line.
     29 /* .PP
     30 /*	The result value is the input buffer argument or a null pointer
     31 /*	when no input is found.
     32 /*
     33 /*	readlline() is a backwards-compatibility wrapper.
     34 /*
     35 /*	Arguments:
     36 /* .IP buf
     37 /*	A variable-length buffer for input. The result is null terminated.
     38 /* .IP fp
     39 /*	Handle to an open stream.
     40 /* .IP lineno
     41 /*	A null pointer, or a pointer to an integer that is incremented
     42 /*	after reading a physical line.
     43 /* .IP first_line
     44 /*	A null pointer, or a pointer to an integer that will contain
     45 /*	the line number of the first non-blank, non-comment line
     46 /*	in the result logical line.
     47 /* DIAGNOSTICS
     48 /*	Warning: a continuation line that does not continue preceding text.
     49 /*	The invalid input is ignored, to avoid complicating caller code.
     50 /* SECURITY
     51 /* .ad
     52 /* .fi
     53 /*	readlline() imposes no logical line length limit therefore it
     54 /*	should be used for reading trusted information only.
     55 /* LICENSE
     56 /* .ad
     57 /* .fi
     58 /*	The Secure Mailer license must be distributed with this software.
     59 /* AUTHOR(S)
     60 /*	Wietse Venema
     61 /*	IBM T.J. Watson Research
     62 /*	P.O. Box 704
     63 /*	Yorktown Heights, NY 10598, USA
     64 /*--*/
     65 
     66 /* System library. */
     67 
     68 #include <sys_defs.h>
     69 #include <ctype.h>
     70 
     71 /* Utility library. */
     72 
     73 #include "msg.h"
     74 #include "vstream.h"
     75 #include "vstring.h"
     76 #include "readlline.h"
     77 
     78 #define STR(x) vstring_str(x)
     79 #define LEN(x) VSTRING_LEN(x)
     80 #define END(x) vstring_end(x)
     81 
     82 /* readllines - read one logical line */
     83 
     84 VSTRING *readllines(VSTRING *buf, VSTREAM *fp, int *lineno, int *first_line)
     85 {
     86     int     ch;
     87     int     next;
     88     ssize_t start;
     89     char   *cp;
     90     int     my_lineno = 0, my_first_line, got_null = 0;
     91 
     92     VSTRING_RESET(buf);
     93 
     94     if (lineno == 0)
     95 	lineno = &my_lineno;
     96     if (first_line == 0)
     97 	first_line = &my_first_line;
     98 
     99     /*
    100      * Ignore comment lines, all whitespace lines, and empty lines. Terminate
    101      * at EOF or at the beginning of the next logical line.
    102      */
    103     for (;;) {
    104 	/* Read one line, possibly not newline terminated. */
    105 	start = LEN(buf);
    106 	while ((ch = VSTREAM_GETC(fp)) != VSTREAM_EOF && ch != '\n') {
    107 	    VSTRING_ADDCH(buf, ch);
    108 	    if (ch == 0)
    109 		got_null = 1;
    110 	}
    111 	if (ch == '\n' || LEN(buf) > start)
    112 	    *lineno += 1;
    113 	/* Ignore comment line, all whitespace line, or empty line. */
    114 	for (cp = STR(buf) + start; cp < END(buf) && ISSPACE(*cp); cp++)
    115 	     /* void */ ;
    116 	if (cp == END(buf) || *cp == '#')
    117 	    vstring_truncate(buf, start);
    118 	if (start == 0)
    119 	    *first_line = *lineno;
    120 	/* Terminate at EOF or at the beginning of the next logical line. */
    121 	if (ch == VSTREAM_EOF)
    122 	    break;
    123 	if (LEN(buf) > 0) {
    124 	    if ((next = VSTREAM_GETC(fp)) != VSTREAM_EOF)
    125 		vstream_ungetc(fp, next);
    126 	    if (next != '#' && !ISSPACE(next))
    127 		break;
    128 	}
    129     }
    130     VSTRING_TERMINATE(buf);
    131 
    132     /*
    133      * This code does not care about embedded null bytes, but callers do.
    134      */
    135     if (got_null) {
    136 	const char *why = "text after null byte may be ignored";
    137 
    138 	if (*first_line == *lineno)
    139 	    msg_warn("%s, line %d: %s",
    140 		     VSTREAM_PATH(fp), *lineno, why);
    141 	else
    142 	    msg_warn("%s, line %d-%d: %s",
    143 		     VSTREAM_PATH(fp), *first_line, *lineno, why);
    144     }
    145 
    146     /*
    147      * Invalid input: continuing text without preceding text. Allowing this
    148      * would complicate "postconf -e", which implements its own multi-line
    149      * parsing routine. Do not abort, just warn, so that critical programs
    150      * like postmap do not leave behind a truncated table.
    151      */
    152     if (LEN(buf) > 0 && ISSPACE(*STR(buf))) {
    153 	msg_warn("%s: logical line must not start with whitespace: \"%.30s%s\"",
    154 		 VSTREAM_PATH(fp), STR(buf),
    155 		 LEN(buf) > 30 ? "..." : "");
    156 	return (readllines(buf, fp, lineno, first_line));
    157     }
    158 
    159     /*
    160      * Done.
    161      */
    162     return (LEN(buf) > 0 ? buf : 0);
    163 }
    164 
    165  /*
    166   * Stand-alone test program.
    167   */
    168 #ifdef TEST
    169 #include <stdlib.h>
    170 #include <string.h>
    171 #include <msg.h>
    172 #include <msg_vstream.h>
    173 #include <stringops.h>
    174 #include <vstream.h>
    175 #include <vstring.h>
    176 
    177  /*
    178   * Test cases. Note: the input and exp_output fields are converted with
    179   * unescape(). Embedded null bytes must be specified as \\0.
    180   */
    181 struct testcase {
    182     const char *name;
    183     const char *input;
    184     const char *exp_output;
    185     int     exp_first_line;
    186     int     exp_last_line;
    187 };
    188 
    189 static const struct testcase testcases[] = {
    190     {"leading space before non-comment",
    191 	" abcde\nfghij\n",
    192 	"fghij",
    193 	2, 2
    194 	/* Expect "logical line must not start with whitespace" */
    195     },
    196     {"leading space before leading comment",
    197 	" #abcde\nfghij\n",
    198 	"fghij",
    199 	2, 2
    200     },
    201     {"leading #comment at beginning of line",
    202 	"#abc\ndef",
    203 	"def",
    204 	2, 2,
    205     },
    206     {"empty line before non-comment",
    207 	"\nabc\n",
    208 	"abc",
    209 	2, 2,
    210     },
    211     {"whitespace line before non-comment",
    212 	" \nabc\n",
    213 	"abc",
    214 	2, 2,
    215     },
    216     {"missing newline at end of non-comment",
    217 	"abc def",
    218 	"abc def",
    219 	1, 1,
    220     },
    221     {"missing newline at end of comment",
    222 	"#abc def",
    223 	"",
    224 	1, 1,
    225     },
    226     {"embedded null, single-line",
    227 	"abc\\0def",
    228 	"abc\\0def",
    229 	1, 1,
    230 	/* Expect "line 1: text after null byte may be ignored" */
    231     },
    232     {"embedded null, multiline",
    233 	"abc\\0\n def",
    234 	"abc\\0 def",
    235 	1, 2,
    236 	/* Expect "line 1-2: text after null byte may be ignored" */
    237     },
    238     {"embedded null in comment",
    239 	"#abc\\0\ndef",
    240 	"def",
    241 	2, 2,
    242 	/* Expect "line 2: text after null byte may be ignored" */
    243     },
    244     {"multiline input",
    245 	"abc\n def\n",
    246 	"abc def",
    247 	1, 2,
    248     },
    249     {"multiline input with embedded #comment after space",
    250 	"abc\n #def\n ghi",
    251 	"abc ghi",
    252 	1, 3,
    253     },
    254     {"multiline input with embedded #comment flush left",
    255 	"abc\n#def\n ghi",
    256 	"abc ghi",
    257 	1, 3,
    258     },
    259     {"multiline input with embedded whitespace line",
    260 	"abc\n \n ghi",
    261 	"abc ghi",
    262 	1, 3,
    263     },
    264     {"multiline input with embedded empty line",
    265 	"abc\n\n ghi",
    266 	"abc ghi",
    267 	1, 3,
    268     },
    269     {"multiline input with embedded #comment after space",
    270 	"abc\n #def\n",
    271 	"abc",
    272 	1, 2,
    273     },
    274     {"multiline input with embedded #comment flush left",
    275 	"abc\n#def\n",
    276 	"abc",
    277 	1, 2,
    278     },
    279     {"empty line at end of file",
    280 	"\n",
    281 	"",
    282 	1, 1,
    283     },
    284     {"whitespace line at end of file",
    285 	"\n \n",
    286 	"",
    287 	2, 2,
    288     },
    289     {"whitespace at end of file",
    290 	"abc\n ",
    291 	"abc",
    292 	1, 2,
    293     },
    294 };
    295 
    296 int     main(int argc, char **argv)
    297 {
    298     const struct testcase *tp;
    299     VSTRING *inp_buf = vstring_alloc(100);
    300     VSTRING *exp_buf = vstring_alloc(100);
    301     VSTRING *out_buf = vstring_alloc(100);
    302     VSTRING *esc_buf = vstring_alloc(100);
    303     VSTREAM *fp;
    304     int     last_line;
    305     int     first_line;
    306     int     pass;
    307     int     fail;
    308 
    309 #define NUM_TESTS       sizeof(testcases)/sizeof(testcases[0])
    310 
    311     msg_vstream_init(basename(argv[0]), VSTREAM_ERR);
    312     util_utf8_enable = 1;
    313 
    314     for (pass = fail = 0, tp = testcases; tp < testcases + NUM_TESTS; tp++) {
    315 	int     ok = 0;
    316 
    317 	vstream_fprintf(VSTREAM_ERR, "RUN  %s\n", tp->name);
    318 	unescape(inp_buf, tp->input);
    319 	unescape(exp_buf, tp->exp_output);
    320 	if ((fp = vstream_memopen(inp_buf, O_RDONLY)) == 0)
    321 	    msg_panic("open memory stream for reading: %m");
    322 	vstream_control(fp, CA_VSTREAM_CTL_PATH("memory buffer"),
    323 			CA_VSTREAM_CTL_END);
    324 	last_line = 0;
    325 	if (readllines(out_buf, fp, &last_line, &first_line) == 0) {
    326 	    VSTRING_RESET(out_buf);
    327 	    VSTRING_TERMINATE(out_buf);
    328 	}
    329 	if (LEN(out_buf) != LEN(exp_buf)) {
    330 	    msg_warn("unexpected output length, got: %ld, want: %ld",
    331 		     (long) LEN(out_buf), (long) LEN(exp_buf));
    332 	} else if (memcmp(STR(out_buf), STR(exp_buf), LEN(out_buf)) != 0) {
    333 	    msg_warn("unexpected output: got: >%s<, want: >%s<",
    334 		     STR(escape(esc_buf, STR(out_buf), LEN(out_buf))),
    335 		     tp->exp_output);
    336 	} else if (first_line != tp->exp_first_line) {
    337 	    msg_warn("unexpected first_line: got: %d, want: %d",
    338 		     first_line, tp->exp_first_line);
    339 	} else if (last_line != tp->exp_last_line) {
    340 	    msg_warn("unexpected last_line: got: %d, want: %d",
    341 		     last_line, tp->exp_last_line);
    342 	} else {
    343 	    vstream_fprintf(VSTREAM_ERR, "got and want: >%s<\n",
    344 			    tp->exp_output);
    345 	    ok = 1;
    346 	}
    347 	if (ok) {
    348 	    vstream_fprintf(VSTREAM_ERR, "PASS %s\n", tp->name);
    349 	    pass++;
    350 	} else {
    351 	    vstream_fprintf(VSTREAM_ERR, "FAIL %s\n", tp->name);
    352 	    fail++;
    353 	}
    354 	vstream_fclose(fp);
    355     }
    356     vstring_free(inp_buf);
    357     vstring_free(exp_buf);
    358     vstring_free(out_buf);
    359     vstring_free(esc_buf);
    360 
    361     msg_info("PASS=%d FAIL=%d", pass, fail);
    362     return (fail > 0);
    363 }
    364 
    365 #endif
    366