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