1 /*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License (the "License").
6 * You may not use this file except in compliance with the License.
7 *
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9 * or http://www.opensolaris.org/os/licensing.
10 * See the License for the specific language governing permissions
11 * and limitations under the License.
12 *
13 * When distributing Covered Code, include this CDDL HEADER in each
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15 * If applicable, add the following below this CDDL HEADER, with the
16 * fields enclosed by brackets "[]" replaced with your own identifying
17 * information: Portions Copyright [yyyy] [name of copyright owner]
18 *
19 * CDDL HEADER END
20 */
21 /*
22 * Copyright 2008 Sun Microsystems, Inc. All rights reserved.
23 * Use is subject to license terms.
24 *
25 * Common code and structures used by name-service-switch "files" backends.
26 */
27
28 /*
29 * An implementation that used mmap() sensibly would be a wonderful thing,
30 * but this here is just yer standard fgets() thang.
31 */
32
33 #include "files_common.h"
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <ctype.h>
38 #include <fcntl.h>
39 #include <poll.h>
40 #include <unistd.h>
41 #include <sys/stat.h>
42 #include <sys/mman.h>
43
44 /*ARGSUSED*/
45 nss_status_t
_nss_files_setent(be,dummy)46 _nss_files_setent(be, dummy)
47 files_backend_ptr_t be;
48 void *dummy;
49 {
50 if (be->f == 0) {
51 if (be->filename == 0) {
52 /* Backend isn't initialized properly? */
53 return (NSS_UNAVAIL);
54 }
55 if ((be->f = fopen(be->filename, "rF")) == 0) {
56 return (NSS_UNAVAIL);
57 }
58 } else {
59 rewind(be->f);
60 }
61 return (NSS_SUCCESS);
62 }
63
64 /*ARGSUSED*/
65 nss_status_t
_nss_files_endent(be,dummy)66 _nss_files_endent(be, dummy)
67 files_backend_ptr_t be;
68 void *dummy;
69 {
70 if (be->f != 0) {
71 (void) fclose(be->f);
72 be->f = 0;
73 }
74 if (be->buf != 0) {
75 free(be->buf);
76 be->buf = 0;
77 }
78 return (NSS_SUCCESS);
79 }
80
81 /*
82 * This routine reads a line, including the processing of continuation
83 * characters. It always leaves (or inserts) \n\0 at the end of the line.
84 * It returns the length of the line read, excluding the \n\0. Who's idea
85 * was this?
86 * Returns -1 on EOF.
87 *
88 * Note that since each concurrent call to _nss_files_read_line has
89 * it's own FILE pointer, we can use getc_unlocked w/o difficulties,
90 * a substantial performance win.
91 */
92 int
_nss_files_read_line(f,buffer,buflen)93 _nss_files_read_line(f, buffer, buflen)
94 FILE *f;
95 char *buffer;
96 int buflen;
97 {
98 int linelen; /* 1st unused slot in buffer */
99 int c;
100
101 /*CONSTCOND*/
102 while (1) {
103 linelen = 0;
104 while (linelen < buflen - 1) { /* "- 1" saves room for \n\0 */
105 switch (c = getc_unlocked(f)) {
106 case EOF:
107 if (linelen == 0 ||
108 buffer[linelen - 1] == '\\') {
109 return (-1);
110 } else {
111 buffer[linelen ] = '\n';
112 buffer[linelen + 1] = '\0';
113 return (linelen);
114 }
115 case '\n':
116 if (linelen > 0 &&
117 buffer[linelen - 1] == '\\') {
118 --linelen; /* remove the '\\' */
119 } else {
120 buffer[linelen ] = '\n';
121 buffer[linelen + 1] = '\0';
122 return (linelen);
123 }
124 break;
125 default:
126 buffer[linelen++] = c;
127 }
128 }
129 /* Buffer overflow -- eat rest of line and loop again */
130 /* ===> Should syslog() */
131 do {
132 c = getc_unlocked(f);
133 if (c == EOF) {
134 return (-1);
135 }
136 } while (c != '\n');
137 }
138 /*NOTREACHED*/
139 }
140
141 /*
142 * used only for getgroupbymem() now.
143 */
144 nss_status_t
_nss_files_do_all(be,args,filter,func)145 _nss_files_do_all(be, args, filter, func)
146 files_backend_ptr_t be;
147 void *args;
148 const char *filter;
149 files_do_all_func_t func;
150 {
151 long grlen;
152 char *buffer;
153 int buflen;
154 nss_status_t res;
155
156 if (be->buf == 0) {
157 if ((grlen = sysconf(_SC_GETGR_R_SIZE_MAX)) > 0)
158 be->minbuf = grlen;
159 if ((be->buf = malloc(be->minbuf)) == 0)
160 return (NSS_UNAVAIL);
161 }
162 buffer = be->buf;
163 buflen = be->minbuf;
164
165 if ((res = _nss_files_setent(be, 0)) != NSS_SUCCESS) {
166 return (res);
167 }
168
169 res = NSS_NOTFOUND;
170
171 do {
172 int linelen;
173
174 if ((linelen = _nss_files_read_line(be->f, buffer,
175 buflen)) < 0) {
176 /* End of file */
177 break;
178 }
179 if (filter != 0 && strstr(buffer, filter) == 0) {
180 /*
181 * Optimization: if the entry doesn't contain the
182 * filter string then it can't be the entry we want,
183 * so don't bother looking more closely at it.
184 */
185 continue;
186 }
187 res = (*func)(buffer, linelen, args);
188
189 } while (res == NSS_NOTFOUND);
190
191 (void) _nss_files_endent(be, 0);
192 return (res);
193 }
194
195 /*
196 * Could implement this as an iterator function on top of _nss_files_do_all(),
197 * but the shared code is small enough that it'd be pretty silly.
198 */
199 nss_status_t
_nss_files_XY_all(be,args,netdb,filter,check)200 _nss_files_XY_all(be, args, netdb, filter, check)
201 files_backend_ptr_t be;
202 nss_XbyY_args_t *args;
203 int netdb; /* whether it uses netdb */
204 /* format or not */
205 const char *filter; /* advisory, to speed up */
206 /* string search */
207 files_XY_check_func check; /* NULL means one-shot, for getXXent */
208 {
209 char *r;
210 nss_status_t res;
211 int parsestat;
212 int (*func)();
213
214 if (filter != NULL && *filter == '\0')
215 return (NSS_NOTFOUND);
216 if (be->buf == 0 || (be->minbuf < args->buf.buflen)) {
217 if (be->minbuf < args->buf.buflen) {
218 if (be->buf == 0) {
219 be->minbuf = args->buf.buflen;
220 } else if (
221 (r = realloc(be->buf, args->buf.buflen)) != NULL) {
222 be->buf = r;
223 be->minbuf = args->buf.buflen;
224 }
225 }
226 if (be->buf == 0 &&
227 (be->buf = malloc(be->minbuf)) == 0)
228 return (NSS_UNAVAIL);
229 }
230
231 if (check != 0 || be->f == 0) {
232 if ((res = _nss_files_setent(be, 0)) != NSS_SUCCESS) {
233 return (res);
234 }
235 }
236
237 res = NSS_NOTFOUND;
238
239 /*CONSTCOND*/
240 while (1) {
241 char *instr = be->buf;
242 int linelen;
243
244 if ((linelen = _nss_files_read_line(be->f, instr,
245 be->minbuf)) < 0) {
246 /* End of file */
247 args->returnval = 0;
248 args->returnlen = 0;
249 break;
250 }
251 if (filter != 0 && strstr(instr, filter) == 0) {
252 /*
253 * Optimization: if the entry doesn't contain the
254 * filter string then it can't be the entry we want,
255 * so don't bother looking more closely at it.
256 */
257 continue;
258 }
259 if (netdb) {
260 char *first;
261 char *last;
262
263 if ((last = strchr(instr, '#')) == 0) {
264 last = instr + linelen;
265 }
266 *last-- = '\0'; /* Nuke '\n' or #comment */
267
268 /*
269 * Skip leading whitespace. Normally there isn't
270 * any, so it's not worth calling strspn().
271 */
272 for (first = instr; isspace(*first); first++) {
273 ;
274 }
275 if (*first == '\0') {
276 continue;
277 }
278 /*
279 * Found something non-blank on the line. Skip back
280 * over any trailing whitespace; since we know
281 * there's non-whitespace earlier in the line,
282 * checking for termination is easy.
283 */
284 while (isspace(*last)) {
285 --last;
286 }
287
288 linelen = last - first + 1;
289 if (first != instr) {
290 instr = first;
291 }
292 }
293
294 args->returnval = 0;
295 args->returnlen = 0;
296
297 if (check != NULL && (*check)(args, instr, linelen) == 0)
298 continue;
299
300 parsestat = NSS_STR_PARSE_SUCCESS;
301 if (be->filename != NULL) {
302 /*
303 * Special case for passwd and group wherein we
304 * replace uids/gids > MAXUID by ID_NOBODY
305 * because files backend does not support
306 * ephemeral ids.
307 */
308 if (strcmp(be->filename, PF_PATH) == 0)
309 parsestat = validate_passwd_ids(instr,
310 &linelen, be->minbuf, 2);
311 else if (strcmp(be->filename, GF_PATH) == 0)
312 parsestat = validate_group_ids(instr,
313 &linelen, be->minbuf, 2, check);
314 }
315
316 if (parsestat == NSS_STR_PARSE_SUCCESS) {
317 func = args->str2ent;
318 parsestat = (*func)(instr, linelen, args->buf.result,
319 args->buf.buffer, args->buf.buflen);
320 }
321
322 if (parsestat == NSS_STR_PARSE_SUCCESS) {
323 args->returnval = (args->buf.result != NULL)?
324 args->buf.result : args->buf.buffer;
325 args->returnlen = linelen;
326 res = NSS_SUCCESS;
327 break;
328 } else if (parsestat == NSS_STR_PARSE_ERANGE) {
329 args->erange = 1;
330 break;
331 } /* else if (parsestat == NSS_STR_PARSE_PARSE) don't care ! */
332 }
333
334 /*
335 * stayopen is set to 0 by default in order to close the opened
336 * file. Some applications may break if it is set to 1.
337 */
338 if (check != 0 && !args->stayopen) {
339 (void) _nss_files_endent(be, 0);
340 }
341
342 return (res);
343 }
344
345 /*
346 * File hashing support. Critical for sites with large (e.g. 1000+ lines)
347 * /etc/passwd or /etc/group files. Currently only used by getpw*() and
348 * getgr*() routines, but any files backend can use this stuff.
349 */
350 static void
_nss_files_hash_destroy(files_hash_t * fhp)351 _nss_files_hash_destroy(files_hash_t *fhp)
352 {
353 free(fhp->fh_table);
354 fhp->fh_table = NULL;
355 free(fhp->fh_line);
356 fhp->fh_line = NULL;
357 free(fhp->fh_file_start);
358 fhp->fh_file_start = NULL;
359 }
360 #ifdef PIC
361 /*
362 * It turns out the hashing stuff really needs to be disabled for processes
363 * other than the nscd; the consumption of swap space and memory is otherwise
364 * unacceptable when the nscd is killed w/ a large passwd file (4M) active.
365 * See 4031930 for details.
366 * So we just use this psuedo function to enable the hashing feature. Since
367 * this function name is private, we just create a function w/ the name
368 * __nss_use_files_hash in the nscd itself and everyone else uses the old
369 * interface.
370 * We also disable hashing for .a executables to avoid problems with large
371 * files....
372 */
373
374 #pragma weak __nss_use_files_hash
375
376 extern void __nss_use_files_hash(void);
377 #endif /* pic */
378
379 /*ARGSUSED*/
380 nss_status_t
_nss_files_XY_hash(files_backend_ptr_t be,nss_XbyY_args_t * args,int netdb,files_hash_t * fhp,int hashop,files_XY_check_func check)381 _nss_files_XY_hash(files_backend_ptr_t be, nss_XbyY_args_t *args,
382 int netdb, files_hash_t *fhp, int hashop, files_XY_check_func check)
383 {
384 /* LINTED E_FUNC_VAR_UNUSED */
385 int fd, retries, ht, stat;
386 /* LINTED E_FUNC_VAR_UNUSED */
387 uint_t hash, line, f;
388 /* LINTED E_FUNC_VAR_UNUSED */
389 files_hashent_t *hp, *htab;
390 /* LINTED E_FUNC_VAR_UNUSED */
391 char *cp, *first, *last;
392 /* LINTED E_FUNC_VAR_UNUSED */
393 nss_XbyY_args_t xargs;
394 /* LINTED E_FUNC_VAR_UNUSED */
395 struct stat64 st;
396
397 #ifndef PIC
398 return (_nss_files_XY_all(be, args, netdb, 0, check));
399 }
400 #else
401 if (__nss_use_files_hash == 0)
402 return (_nss_files_XY_all(be, args, netdb, 0, check));
403
404 mutex_lock(&fhp->fh_lock);
405 retry:
406 retries = 100;
407 while (stat64(be->filename, &st) < 0) {
408 /*
409 * On a healthy system this can't happen except during brief
410 * periods when the file is being modified/renamed. Keep
411 * trying until things settle down, but eventually give up.
412 */
413 if (--retries == 0)
414 goto unavail;
415 poll(0, 0, 100);
416 }
417
418 if (st.st_mtim.tv_sec == fhp->fh_mtime.tv_sec &&
419 st.st_mtim.tv_nsec == fhp->fh_mtime.tv_nsec &&
420 fhp->fh_table != NULL) {
421 htab = &fhp->fh_table[hashop * fhp->fh_size];
422 hash = fhp->fh_hash_func[hashop](args, 1, NULL, 0);
423 for (hp = htab[hash % fhp->fh_size].h_first; hp != NULL;
424 hp = hp->h_next) {
425 if (hp->h_hash != hash)
426 continue;
427 line = hp - htab;
428 if ((*check)(args, fhp->fh_line[line].l_start,
429 fhp->fh_line[line].l_len) == 0)
430 continue;
431
432 if (be->filename != NULL) {
433 stat = NSS_STR_PARSE_SUCCESS;
434 if (strcmp(be->filename, PF_PATH) == 0)
435 stat = validate_passwd_ids(
436 fhp->fh_line[line].l_start,
437 &fhp->fh_line[line].l_len,
438 fhp->fh_line[line].l_len + 1,
439 1);
440 else if (strcmp(be->filename, GF_PATH) == 0)
441 stat = validate_group_ids(
442 fhp->fh_line[line].l_start,
443 &fhp->fh_line[line].l_len,
444 fhp->fh_line[line].l_len + 1,
445 1, check);
446 if (stat != NSS_STR_PARSE_SUCCESS) {
447 if (stat == NSS_STR_PARSE_ERANGE)
448 args->erange = 1;
449 continue;
450 }
451 }
452
453 if ((*args->str2ent)(fhp->fh_line[line].l_start,
454 fhp->fh_line[line].l_len, args->buf.result,
455 args->buf.buffer, args->buf.buflen) ==
456 NSS_STR_PARSE_SUCCESS) {
457 args->returnval = (args->buf.result)?
458 args->buf.result:args->buf.buffer;
459 args->returnlen = fhp->fh_line[line].l_len;
460 mutex_unlock(&fhp->fh_lock);
461 return (NSS_SUCCESS);
462 } else {
463 args->erange = 1;
464 }
465 }
466 args->returnval = 0;
467 args->returnlen = 0;
468 mutex_unlock(&fhp->fh_lock);
469 return (NSS_NOTFOUND);
470 }
471
472 _nss_files_hash_destroy(fhp);
473
474 if (st.st_size > SSIZE_MAX)
475 goto unavail;
476
477 if ((fhp->fh_file_start = malloc((ssize_t)st.st_size + 1)) == NULL)
478 goto unavail;
479
480 if ((fd = open(be->filename, O_RDONLY)) < 0)
481 goto unavail;
482
483 if (read(fd, fhp->fh_file_start, (ssize_t)st.st_size) !=
484 (ssize_t)st.st_size) {
485 close(fd);
486 goto retry;
487 }
488
489 close(fd);
490
491 fhp->fh_file_end = fhp->fh_file_start + (off_t)st.st_size;
492 *fhp->fh_file_end = '\n';
493 fhp->fh_mtime = st.st_mtim;
494
495 /*
496 * If the file changed since we read it, or if it's less than
497 * 1-2 seconds old, don't trust it; its modification may still
498 * be in progress. The latter is a heuristic hack to minimize
499 * the likelihood of damage if someone modifies /etc/mumble
500 * directly (as opposed to editing and renaming a temp file).
501 *
502 * Note: the cast to u_int is there in case (1) someone rdated
503 * the system backwards since the last modification of /etc/mumble
504 * or (2) this is a diskless client whose time is badly out of sync
505 * with its server. The 1-2 second age hack doesn't cover these
506 * cases -- oh well.
507 */
508 if (stat64(be->filename, &st) < 0 ||
509 st.st_mtim.tv_sec != fhp->fh_mtime.tv_sec ||
510 st.st_mtim.tv_nsec != fhp->fh_mtime.tv_nsec ||
511 (uint_t)(time(0) - st.st_mtim.tv_sec + 2) < 4) {
512 poll(0, 0, 1000);
513 goto retry;
514 }
515
516 line = 1;
517 for (cp = fhp->fh_file_start; cp < fhp->fh_file_end; cp++)
518 if (*cp == '\n')
519 line++;
520
521 for (f = 2; f * f <= line; f++) { /* find next largest prime */
522 if (line % f == 0) {
523 f = 1;
524 line++;
525 }
526 }
527
528 fhp->fh_size = line;
529 fhp->fh_line = malloc(line * sizeof (files_linetab_t));
530 fhp->fh_table = calloc(line * fhp->fh_nhtab, sizeof (files_hashent_t));
531 if (fhp->fh_line == NULL || fhp->fh_table == NULL)
532 goto unavail;
533
534 line = 0;
535 cp = fhp->fh_file_start;
536 while (cp < fhp->fh_file_end) {
537 first = cp;
538 while (*cp != '\n')
539 cp++;
540 if (cp > first && *(cp - 1) == '\\') {
541 memmove(first + 2, first, cp - first - 1);
542 cp = first + 2;
543 continue;
544 }
545 last = cp;
546 *cp++ = '\0';
547 if (netdb) {
548 if ((last = strchr(first, '#')) == 0)
549 last = cp - 1;
550 *last-- = '\0'; /* nuke '\n' or #comment */
551 while (isspace(*first)) /* nuke leading whitespace */
552 first++;
553 if (*first == '\0') /* skip content-free lines */
554 continue;
555 while (isspace(*last)) /* nuke trailing whitespace */
556 --last;
557 *++last = '\0';
558 }
559 for (ht = 0; ht < fhp->fh_nhtab; ht++) {
560 hp = &fhp->fh_table[ht * fhp->fh_size + line];
561 hp->h_hash = fhp->fh_hash_func[ht](&xargs, 0, first,
562 last - first);
563 }
564 fhp->fh_line[line].l_start = first;
565 fhp->fh_line[line++].l_len = last - first;
566 }
567
568 /*
569 * Populate the hash tables in reverse order so that the hash chains
570 * end up in forward order. This ensures that hashed lookups find
571 * things in the same order that a linear search of the file would.
572 * This is essential in cases where there could be multiple matches.
573 * For example: until 2.7, root and smtp both had uid 0; but we
574 * certainly wouldn't want getpwuid(0) to return smtp.
575 */
576 for (ht = 0; ht < fhp->fh_nhtab; ht++) {
577 htab = &fhp->fh_table[ht * fhp->fh_size];
578 for (hp = &htab[line - 1]; hp >= htab; hp--) {
579 uint_t bucket = hp->h_hash % fhp->fh_size;
580 hp->h_next = htab[bucket].h_first;
581 htab[bucket].h_first = hp;
582 }
583 }
584
585 goto retry;
586
587 unavail:
588 _nss_files_hash_destroy(fhp);
589 mutex_unlock(&fhp->fh_lock);
590 return (NSS_UNAVAIL);
591 }
592 #endif /* PIC */
593
594 nss_status_t
_nss_files_getent_rigid(be,a)595 _nss_files_getent_rigid(be, a)
596 files_backend_ptr_t be;
597 void *a;
598 {
599 nss_XbyY_args_t *args = (nss_XbyY_args_t *)a;
600
601 return (_nss_files_XY_all(be, args, 0, 0, 0));
602 }
603
604 nss_status_t
_nss_files_getent_netdb(be,a)605 _nss_files_getent_netdb(be, a)
606 files_backend_ptr_t be;
607 void *a;
608 {
609 nss_XbyY_args_t *args = (nss_XbyY_args_t *)a;
610
611 return (_nss_files_XY_all(be, args, 1, 0, 0));
612 }
613
614 /*ARGSUSED*/
615 nss_status_t
_nss_files_destr(be,dummy)616 _nss_files_destr(be, dummy)
617 files_backend_ptr_t be;
618 void *dummy;
619 {
620 if (be != 0) {
621 if (be->f != 0) {
622 (void) _nss_files_endent(be, 0);
623 }
624 if (be->hashinfo != NULL) {
625 (void) mutex_lock(&be->hashinfo->fh_lock);
626 if (--be->hashinfo->fh_refcnt == 0)
627 _nss_files_hash_destroy(be->hashinfo);
628 (void) mutex_unlock(&be->hashinfo->fh_lock);
629 }
630 free(be);
631 }
632 return (NSS_SUCCESS); /* In case anyone is dumb enough to check */
633 }
634
635 nss_backend_t *
_nss_files_constr(ops,n_ops,filename,min_bufsize,fhp)636 _nss_files_constr(ops, n_ops, filename, min_bufsize, fhp)
637 files_backend_op_t ops[];
638 int n_ops;
639 const char *filename;
640 int min_bufsize;
641 files_hash_t *fhp;
642 {
643 files_backend_ptr_t be;
644
645 if ((be = (files_backend_ptr_t)malloc(sizeof (*be))) == 0) {
646 return (0);
647 }
648 be->ops = ops;
649 be->n_ops = n_ops;
650 be->filename = filename;
651 be->minbuf = min_bufsize;
652 be->f = 0;
653 be->buf = 0;
654 be->hashinfo = fhp;
655
656 if (fhp != NULL) {
657 (void) mutex_lock(&fhp->fh_lock);
658 fhp->fh_refcnt++;
659 (void) mutex_unlock(&fhp->fh_lock);
660 }
661
662 return ((nss_backend_t *)be);
663 }
664
665 int
_nss_files_check_name_colon(nss_XbyY_args_t * argp,const char * line,int linelen)666 _nss_files_check_name_colon(nss_XbyY_args_t *argp, const char *line,
667 int linelen)
668 {
669 const char *linep, *limit;
670 const char *keyp = argp->key.name;
671
672 linep = line;
673 limit = line + linelen;
674 while (*keyp && linep < limit && *keyp == *linep) {
675 keyp++;
676 linep++;
677 }
678 return (linep < limit && *keyp == '\0' && *linep == ':');
679 }
680
681 /*
682 * This routine is used to parse lines of the form:
683 * name number aliases
684 * It returns 1 if the key in argp matches any one of the
685 * names in the line, otherwise 0
686 * Used by rpc, networks, protocols
687 */
688 int
_nss_files_check_name_aliases(nss_XbyY_args_t * argp,const char * line,int linelen)689 _nss_files_check_name_aliases(nss_XbyY_args_t *argp, const char *line,
690 int linelen)
691 {
692 const char *limit, *linep, *keyp;
693
694 linep = line;
695 limit = line + linelen;
696 keyp = argp->key.name;
697
698 /* compare name */
699 while (*keyp && linep < limit && !isspace(*linep) && *keyp == *linep) {
700 keyp++;
701 linep++;
702 }
703 if (*keyp == '\0' && linep < limit && isspace(*linep))
704 return (1);
705 /* skip remainder of the name, if any */
706 while (linep < limit && !isspace(*linep))
707 linep++;
708 /* skip the delimiting spaces */
709 while (linep < limit && isspace(*linep))
710 linep++;
711 /* compare with the aliases */
712 while (linep < limit) {
713 /*
714 * 1st pass: skip number
715 * Other passes: skip remainder of the alias name, if any
716 */
717 while (linep < limit && !isspace(*linep))
718 linep++;
719 /* skip the delimiting spaces */
720 while (linep < limit && isspace(*linep))
721 linep++;
722 /* compare with the alias name */
723 keyp = argp->key.name;
724 while (*keyp && linep < limit && !isspace(*linep) &&
725 *keyp == *linep) {
726 keyp++;
727 linep++;
728 }
729 if (*keyp == '\0' && (linep == limit || isspace(*linep)))
730 return (1);
731 }
732 return (0);
733 }
734