xref: /original-bsd/usr.sbin/sendmail/src/util.c (revision 48611f03)
1 /*
2  * Copyright (c) 1983 Eric P. Allman
3  * Copyright (c) 1988 Regents of the University of California.
4  * All rights reserved.
5  *
6  * %sccs.include.redist.c%
7  */
8 
9 #ifndef lint
10 static char sccsid[] = "@(#)util.c	6.14 (Berkeley) 03/23/93";
11 #endif /* not lint */
12 
13 # include "sendmail.h"
14 # include <sys/stat.h>
15 # include <sysexits.h>
16 /*
17 **  STRIPQUOTES -- Strip quotes & quote bits from a string.
18 **
19 **	Runs through a string and strips off unquoted quote
20 **	characters and quote bits.  This is done in place.
21 **
22 **	Parameters:
23 **		s -- the string to strip.
24 **
25 **	Returns:
26 **		none.
27 **
28 **	Side Effects:
29 **		none.
30 **
31 **	Called By:
32 **		deliver
33 */
34 
35 stripquotes(s)
36 	char *s;
37 {
38 	register char *p;
39 	register char *q;
40 	register char c;
41 
42 	if (s == NULL)
43 		return;
44 
45 	p = q = s;
46 	do
47 	{
48 		c = *p++;
49 		if (c == '\\')
50 			c = *p++;
51 		else if (c == '"')
52 			continue;
53 		*q++ = c;
54 	} while (c != '\0');
55 }
56 /*
57 **  CAPITALIZE -- return a copy of a string, properly capitalized.
58 **
59 **	Parameters:
60 **		s -- the string to capitalize.
61 **
62 **	Returns:
63 **		a pointer to a properly capitalized string.
64 **
65 **	Side Effects:
66 **		none.
67 */
68 
69 char *
70 capitalize(s)
71 	register char *s;
72 {
73 	static char buf[50];
74 	register char *p;
75 
76 	p = buf;
77 
78 	for (;;)
79 	{
80 		while (!(isascii(*s) && isalpha(*s)) && *s != '\0')
81 			*p++ = *s++;
82 		if (*s == '\0')
83 			break;
84 		*p++ = toupper(*s);
85 		s++;
86 		while (isascii(*s) && isalpha(*s))
87 			*p++ = *s++;
88 	}
89 
90 	*p = '\0';
91 	return (buf);
92 }
93 /*
94 **  XALLOC -- Allocate memory and bitch wildly on failure.
95 **
96 **	THIS IS A CLUDGE.  This should be made to give a proper
97 **	error -- but after all, what can we do?
98 **
99 **	Parameters:
100 **		sz -- size of area to allocate.
101 **
102 **	Returns:
103 **		pointer to data region.
104 **
105 **	Side Effects:
106 **		Memory is allocated.
107 */
108 
109 char *
110 xalloc(sz)
111 	register int sz;
112 {
113 	register char *p;
114 
115 	p = malloc((unsigned) sz);
116 	if (p == NULL)
117 	{
118 		syserr("Out of memory!!");
119 		abort();
120 		/* exit(EX_UNAVAILABLE); */
121 	}
122 	return (p);
123 }
124 /*
125 **  COPYPLIST -- copy list of pointers.
126 **
127 **	This routine is the equivalent of newstr for lists of
128 **	pointers.
129 **
130 **	Parameters:
131 **		list -- list of pointers to copy.
132 **			Must be NULL terminated.
133 **		copycont -- if TRUE, copy the contents of the vector
134 **			(which must be a string) also.
135 **
136 **	Returns:
137 **		a copy of 'list'.
138 **
139 **	Side Effects:
140 **		none.
141 */
142 
143 char **
144 copyplist(list, copycont)
145 	char **list;
146 	bool copycont;
147 {
148 	register char **vp;
149 	register char **newvp;
150 
151 	for (vp = list; *vp != NULL; vp++)
152 		continue;
153 
154 	vp++;
155 
156 	newvp = (char **) xalloc((int) (vp - list) * sizeof *vp);
157 	bcopy((char *) list, (char *) newvp, (int) (vp - list) * sizeof *vp);
158 
159 	if (copycont)
160 	{
161 		for (vp = newvp; *vp != NULL; vp++)
162 			*vp = newstr(*vp);
163 	}
164 
165 	return (newvp);
166 }
167 /*
168 **  COPYQUEUE -- copy address queue.
169 **
170 **	This routine is the equivalent of newstr for address queues
171 **	addresses marked with QDONTSEND aren't copied
172 **
173 **	Parameters:
174 **		addr -- list of address structures to copy.
175 **
176 **	Returns:
177 **		a copy of 'addr'.
178 **
179 **	Side Effects:
180 **		none.
181 */
182 
183 ADDRESS *
184 copyqueue(addr)
185 	ADDRESS *addr;
186 {
187 	register ADDRESS *newaddr;
188 	ADDRESS *ret;
189 	register ADDRESS **tail = &ret;
190 
191 	while (addr != NULL)
192 	{
193 		if (!bitset(QDONTSEND, addr->q_flags))
194 		{
195 			newaddr = (ADDRESS *) xalloc(sizeof(ADDRESS));
196 			STRUCTCOPY(*addr, *newaddr);
197 			*tail = newaddr;
198 			tail = &newaddr->q_next;
199 		}
200 		addr = addr->q_next;
201 	}
202 	*tail = NULL;
203 
204 	return ret;
205 }
206 /*
207 **  PRINTAV -- print argument vector.
208 **
209 **	Parameters:
210 **		av -- argument vector.
211 **
212 **	Returns:
213 **		none.
214 **
215 **	Side Effects:
216 **		prints av.
217 */
218 
219 printav(av)
220 	register char **av;
221 {
222 	while (*av != NULL)
223 	{
224 		if (tTd(0, 44))
225 			printf("\n\t%08x=", *av);
226 		else
227 			(void) putchar(' ');
228 		xputs(*av++);
229 	}
230 	(void) putchar('\n');
231 }
232 /*
233 **  LOWER -- turn letter into lower case.
234 **
235 **	Parameters:
236 **		c -- character to turn into lower case.
237 **
238 **	Returns:
239 **		c, in lower case.
240 **
241 **	Side Effects:
242 **		none.
243 */
244 
245 char
246 lower(c)
247 	register char c;
248 {
249 	return((isascii(c) && isupper(c)) ? tolower(c) : c);
250 }
251 /*
252 **  XPUTS -- put string doing control escapes.
253 **
254 **	Parameters:
255 **		s -- string to put.
256 **
257 **	Returns:
258 **		none.
259 **
260 **	Side Effects:
261 **		output to stdout
262 */
263 
264 xputs(s)
265 	register char *s;
266 {
267 	register int c;
268 	register struct metamac *mp;
269 	extern struct metamac MetaMacros[];
270 
271 	if (s == NULL)
272 	{
273 		printf("<null>");
274 		return;
275 	}
276 	while ((c = (*s++ & 0377)) != '\0')
277 	{
278 		if (!isascii(c))
279 		{
280 			if (c == MATCHREPL || c == MACROEXPAND)
281 			{
282 				putchar('$');
283 				continue;
284 			}
285 			for (mp = MetaMacros; mp->metaname != '\0'; mp++)
286 			{
287 				if ((mp->metaval & 0377) == c)
288 				{
289 					printf("$%c", mp->metaname);
290 					break;
291 				}
292 			}
293 			if (mp->metaname != '\0')
294 				continue;
295 			(void) putchar('\\');
296 			c &= 0177;
297 		}
298 		if (isprint(c))
299 		{
300 			putchar(c);
301 			continue;
302 		}
303 
304 		/* wasn't a meta-macro -- find another way to print it */
305 		switch (c)
306 		{
307 		  case '\0':
308 			continue;
309 
310 		  case '\n':
311 			c = 'n';
312 			break;
313 
314 		  case '\r':
315 			c = 'r';
316 			break;
317 
318 		  case '\t':
319 			c = 't';
320 			break;
321 
322 		  default:
323 			(void) putchar('^');
324 			(void) putchar(c ^ 0100);
325 			continue;
326 		}
327 	}
328 	(void) fflush(stdout);
329 }
330 /*
331 **  MAKELOWER -- Translate a line into lower case
332 **
333 **	Parameters:
334 **		p -- the string to translate.  If NULL, return is
335 **			immediate.
336 **
337 **	Returns:
338 **		none.
339 **
340 **	Side Effects:
341 **		String pointed to by p is translated to lower case.
342 **
343 **	Called By:
344 **		parse
345 */
346 
347 makelower(p)
348 	register char *p;
349 {
350 	register char c;
351 
352 	if (p == NULL)
353 		return;
354 	for (; (c = *p) != '\0'; p++)
355 		if (isascii(c) && isupper(c))
356 			*p = tolower(c);
357 }
358 /*
359 **  BUILDFNAME -- build full name from gecos style entry.
360 **
361 **	This routine interprets the strange entry that would appear
362 **	in the GECOS field of the password file.
363 **
364 **	Parameters:
365 **		p -- name to build.
366 **		login -- the login name of this user (for &).
367 **		buf -- place to put the result.
368 **
369 **	Returns:
370 **		none.
371 **
372 **	Side Effects:
373 **		none.
374 */
375 
376 buildfname(gecos, login, buf)
377 	register char *gecos;
378 	char *login;
379 	char *buf;
380 {
381 	register char *p;
382 	register char *bp = buf;
383 	int l;
384 
385 	if (*gecos == '*')
386 		gecos++;
387 
388 	/* find length of final string */
389 	l = 0;
390 	for (p = gecos; *p != '\0' && *p != ',' && *p != ';' && *p != '%'; p++)
391 	{
392 		if (*p == '&')
393 			l += strlen(login);
394 		else
395 			l++;
396 	}
397 
398 	/* now fill in buf */
399 	for (p = gecos; *p != '\0' && *p != ',' && *p != ';' && *p != '%'; p++)
400 	{
401 		if (*p == '&')
402 		{
403 			(void) strcpy(bp, login);
404 			*bp = toupper(*bp);
405 			while (*bp != '\0')
406 				bp++;
407 		}
408 		else
409 			*bp++ = *p;
410 	}
411 	*bp = '\0';
412 }
413 /*
414 **  SAFEFILE -- return true if a file exists and is safe for a user.
415 **
416 **	Parameters:
417 **		fn -- filename to check.
418 **		uid -- uid to compare against.
419 **		mode -- mode bits that must match.
420 **
421 **	Returns:
422 **		0 if fn exists, is owned by uid, and matches mode.
423 **		An errno otherwise.  The actual errno is cleared.
424 **
425 **	Side Effects:
426 **		none.
427 */
428 
429 int
430 safefile(fn, uid, mode)
431 	char *fn;
432 	uid_t uid;
433 	int mode;
434 {
435 	struct stat stbuf;
436 
437 	if (stat(fn, &stbuf) < 0)
438 	{
439 		int ret = errno;
440 
441 		errno = 0;
442 		return ret;
443 	}
444 	if (stbuf.st_uid == uid && (stbuf.st_mode & mode) == mode)
445 		return 0;
446 	return EPERM;
447 }
448 /*
449 **  FIXCRLF -- fix <CR><LF> in line.
450 **
451 **	Looks for the <CR><LF> combination and turns it into the
452 **	UNIX canonical <NL> character.  It only takes one line,
453 **	i.e., it is assumed that the first <NL> found is the end
454 **	of the line.
455 **
456 **	Parameters:
457 **		line -- the line to fix.
458 **		stripnl -- if true, strip the newline also.
459 **
460 **	Returns:
461 **		none.
462 **
463 **	Side Effects:
464 **		line is changed in place.
465 */
466 
467 fixcrlf(line, stripnl)
468 	char *line;
469 	bool stripnl;
470 {
471 	register char *p;
472 
473 	p = strchr(line, '\n');
474 	if (p == NULL)
475 		return;
476 	if (p > line && p[-1] == '\r')
477 		p--;
478 	if (!stripnl)
479 		*p++ = '\n';
480 	*p = '\0';
481 }
482 /*
483 **  DFOPEN -- determined file open
484 **
485 **	This routine has the semantics of fopen, except that it will
486 **	keep trying a few times to make this happen.  The idea is that
487 **	on very loaded systems, we may run out of resources (inodes,
488 **	whatever), so this tries to get around it.
489 */
490 
491 FILE *
492 dfopen(filename, mode)
493 	char *filename;
494 	char *mode;
495 {
496 	register int tries;
497 	register FILE *fp;
498 
499 	for (tries = 0; tries < 10; tries++)
500 	{
501 		sleep((unsigned) (10 * tries));
502 		errno = 0;
503 		fp = fopen(filename, mode);
504 		if (fp != NULL)
505 			break;
506 		if (errno != ENFILE && errno != EINTR)
507 			break;
508 	}
509 	if (fp != NULL)
510 	{
511 		int locktype;
512 		extern bool lockfile();
513 
514 		/* lock the file to avoid accidental conflicts */
515 		if (*mode == 'w' || *mode == 'a')
516 			locktype = LOCK_EX;
517 		else
518 			locktype = LOCK_SH;
519 		(void) lockfile(fileno(fp), filename, locktype);
520 		errno = 0;
521 	}
522 	return (fp);
523 }
524 /*
525 **  PUTLINE -- put a line like fputs obeying SMTP conventions
526 **
527 **	This routine always guarantees outputing a newline (or CRLF,
528 **	as appropriate) at the end of the string.
529 **
530 **	Parameters:
531 **		l -- line to put.
532 **		fp -- file to put it onto.
533 **		m -- the mailer used to control output.
534 **
535 **	Returns:
536 **		none
537 **
538 **	Side Effects:
539 **		output of l to fp.
540 */
541 
542 putline(l, fp, m)
543 	register char *l;
544 	FILE *fp;
545 	MAILER *m;
546 {
547 	register char *p;
548 	register char svchar;
549 
550 	/* strip out 0200 bits -- these can look like TELNET protocol */
551 	if (bitnset(M_7BITS, m->m_flags))
552 	{
553 		for (p = l; svchar = *p; ++p)
554 			if (svchar & 0200)
555 				*p = svchar &~ 0200;
556 	}
557 
558 	do
559 	{
560 		/* find the end of the line */
561 		p = strchr(l, '\n');
562 		if (p == NULL)
563 			p = &l[strlen(l)];
564 
565 		/* check for line overflow */
566 		while (m->m_linelimit > 0 && (p - l) > m->m_linelimit)
567 		{
568 			register char *q = &l[m->m_linelimit - 1];
569 
570 			svchar = *q;
571 			*q = '\0';
572 			if (l[0] == '.' && bitnset(M_XDOT, m->m_flags))
573 				(void) putc('.', fp);
574 			fputs(l, fp);
575 			(void) putc('!', fp);
576 			fputs(m->m_eol, fp);
577 			*q = svchar;
578 			l = q;
579 		}
580 
581 		/* output last part */
582 		if (l[0] == '.' && bitnset(M_XDOT, m->m_flags))
583 			(void) putc('.', fp);
584 		for ( ; l < p; ++l)
585 			(void) putc(*l, fp);
586 		fputs(m->m_eol, fp);
587 		if (*l == '\n')
588 			++l;
589 	} while (l[0] != '\0');
590 }
591 /*
592 **  XUNLINK -- unlink a file, doing logging as appropriate.
593 **
594 **	Parameters:
595 **		f -- name of file to unlink.
596 **
597 **	Returns:
598 **		none.
599 **
600 **	Side Effects:
601 **		f is unlinked.
602 */
603 
604 xunlink(f)
605 	char *f;
606 {
607 	register int i;
608 
609 # ifdef LOG
610 	if (LogLevel > 98)
611 		syslog(LOG_DEBUG, "%s: unlink %s", CurEnv->e_id, f);
612 # endif /* LOG */
613 
614 	i = unlink(f);
615 # ifdef LOG
616 	if (i < 0 && LogLevel > 97)
617 		syslog(LOG_DEBUG, "%s: unlink-fail %d", f, errno);
618 # endif /* LOG */
619 }
620 /*
621 **  XFCLOSE -- close a file, doing logging as appropriate.
622 **
623 **	Parameters:
624 **		fp -- file pointer for the file to close
625 **		a, b -- miscellaneous crud to print for debugging
626 **
627 **	Returns:
628 **		none.
629 **
630 **	Side Effects:
631 **		fp is closed.
632 */
633 
634 xfclose(fp, a, b)
635 	FILE *fp;
636 	char *a, *b;
637 {
638 	if (tTd(53, 99))
639 		printf("xfclose(%x) %s %s\n", fp, a, b);
640 	if (fclose(fp) < 0 && tTd(53, 99))
641 		printf("xfclose FAILURE: %s\n", errstring(errno));
642 }
643 /*
644 **  SFGETS -- "safe" fgets -- times out and ignores random interrupts.
645 **
646 **	Parameters:
647 **		buf -- place to put the input line.
648 **		siz -- size of buf.
649 **		fp -- file to read from.
650 **		timeout -- the timeout before error occurs.
651 **
652 **	Returns:
653 **		NULL on error (including timeout).  This will also leave
654 **			buf containing a null string.
655 **		buf otherwise.
656 **
657 **	Side Effects:
658 **		none.
659 */
660 
661 static jmp_buf	CtxReadTimeout;
662 
663 char *
664 sfgets(buf, siz, fp, timeout)
665 	char *buf;
666 	int siz;
667 	FILE *fp;
668 	time_t timeout;
669 {
670 	register EVENT *ev = NULL;
671 	register char *p;
672 	static int readtimeout();
673 
674 	/* set the timeout */
675 	if (timeout != 0)
676 	{
677 		if (setjmp(CtxReadTimeout) != 0)
678 		{
679 # ifdef LOG
680 			syslog(LOG_NOTICE,
681 			    "timeout waiting for input from %s\n",
682 			    CurHostName? CurHostName: "local");
683 # endif
684 			errno = 0;
685 			usrerr("451 timeout waiting for input");
686 			buf[0] = '\0';
687 			return (NULL);
688 		}
689 		ev = setevent(timeout, readtimeout, 0);
690 	}
691 
692 	/* try to read */
693 	p = NULL;
694 	while (p == NULL && !feof(fp) && !ferror(fp))
695 	{
696 		errno = 0;
697 		p = fgets(buf, siz, fp);
698 		if (errno == EINTR)
699 			clearerr(fp);
700 	}
701 
702 	/* clear the event if it has not sprung */
703 	clrevent(ev);
704 
705 	/* clean up the books and exit */
706 	LineNumber++;
707 	if (p == NULL)
708 	{
709 		buf[0] = '\0';
710 		return (NULL);
711 	}
712 	if (!EightBit)
713 		for (p = buf; *p != '\0'; p++)
714 			*p &= ~0200;
715 	return (buf);
716 }
717 
718 static
719 readtimeout()
720 {
721 	longjmp(CtxReadTimeout, 1);
722 }
723 /*
724 **  FGETFOLDED -- like fgets, but know about folded lines.
725 **
726 **	Parameters:
727 **		buf -- place to put result.
728 **		n -- bytes available.
729 **		f -- file to read from.
730 **
731 **	Returns:
732 **		input line(s) on success, NULL on error or EOF.
733 **		This will normally be buf -- unless the line is too
734 **			long, when it will be xalloc()ed.
735 **
736 **	Side Effects:
737 **		buf gets lines from f, with continuation lines (lines
738 **		with leading white space) appended.  CRLF's are mapped
739 **		into single newlines.  Any trailing NL is stripped.
740 */
741 
742 char *
743 fgetfolded(buf, n, f)
744 	char *buf;
745 	register int n;
746 	FILE *f;
747 {
748 	register char *p = buf;
749 	char *bp = buf;
750 	register int i;
751 
752 	n--;
753 	while ((i = getc(f)) != EOF)
754 	{
755 		if (i == '\r')
756 		{
757 			i = getc(f);
758 			if (i != '\n')
759 			{
760 				if (i != EOF)
761 					(void) ungetc(i, f);
762 				i = '\r';
763 			}
764 		}
765 		if (--n <= 0)
766 		{
767 			/* allocate new space */
768 			char *nbp;
769 			int nn;
770 
771 			nn = (p - bp);
772 			if (nn < MEMCHUNKSIZE)
773 				nn *= 2;
774 			else
775 				nn += MEMCHUNKSIZE;
776 			nbp = xalloc(nn);
777 			bcopy(bp, nbp, p - bp);
778 			p = &nbp[p - bp];
779 			if (bp != buf)
780 				free(bp);
781 			bp = nbp;
782 			n = nn - (p - bp);
783 		}
784 		*p++ = i;
785 		if (i == '\n')
786 		{
787 			LineNumber++;
788 			i = getc(f);
789 			if (i != EOF)
790 				(void) ungetc(i, f);
791 			if (i != ' ' && i != '\t')
792 				break;
793 		}
794 	}
795 	if (p == bp)
796 		return (NULL);
797 	*--p = '\0';
798 	return (bp);
799 }
800 /*
801 **  CURTIME -- return current time.
802 **
803 **	Parameters:
804 **		none.
805 **
806 **	Returns:
807 **		the current time.
808 **
809 **	Side Effects:
810 **		none.
811 */
812 
813 time_t
814 curtime()
815 {
816 	auto time_t t;
817 
818 	(void) time(&t);
819 	return (t);
820 }
821 /*
822 **  ATOBOOL -- convert a string representation to boolean.
823 **
824 **	Defaults to "TRUE"
825 **
826 **	Parameters:
827 **		s -- string to convert.  Takes "tTyY" as true,
828 **			others as false.
829 **
830 **	Returns:
831 **		A boolean representation of the string.
832 **
833 **	Side Effects:
834 **		none.
835 */
836 
837 bool
838 atobool(s)
839 	register char *s;
840 {
841 	if (*s == '\0' || strchr("tTyY", *s) != NULL)
842 		return (TRUE);
843 	return (FALSE);
844 }
845 /*
846 **  ATOOCT -- convert a string representation to octal.
847 **
848 **	Parameters:
849 **		s -- string to convert.
850 **
851 **	Returns:
852 **		An integer representing the string interpreted as an
853 **		octal number.
854 **
855 **	Side Effects:
856 **		none.
857 */
858 
859 atooct(s)
860 	register char *s;
861 {
862 	register int i = 0;
863 
864 	while (*s >= '0' && *s <= '7')
865 		i = (i << 3) | (*s++ - '0');
866 	return (i);
867 }
868 /*
869 **  WAITFOR -- wait for a particular process id.
870 **
871 **	Parameters:
872 **		pid -- process id to wait for.
873 **
874 **	Returns:
875 **		status of pid.
876 **		-1 if pid never shows up.
877 **
878 **	Side Effects:
879 **		none.
880 */
881 
882 waitfor(pid)
883 	int pid;
884 {
885 	auto int st;
886 	int i;
887 
888 	do
889 	{
890 		errno = 0;
891 		i = wait(&st);
892 	} while ((i >= 0 || errno == EINTR) && i != pid);
893 	if (i < 0)
894 		st = -1;
895 	return (st);
896 }
897 /*
898 **  BITINTERSECT -- tell if two bitmaps intersect
899 **
900 **	Parameters:
901 **		a, b -- the bitmaps in question
902 **
903 **	Returns:
904 **		TRUE if they have a non-null intersection
905 **		FALSE otherwise
906 **
907 **	Side Effects:
908 **		none.
909 */
910 
911 bool
912 bitintersect(a, b)
913 	BITMAP a;
914 	BITMAP b;
915 {
916 	int i;
917 
918 	for (i = BITMAPBYTES / sizeof (int); --i >= 0; )
919 		if ((a[i] & b[i]) != 0)
920 			return (TRUE);
921 	return (FALSE);
922 }
923 /*
924 **  BITZEROP -- tell if a bitmap is all zero
925 **
926 **	Parameters:
927 **		map -- the bit map to check
928 **
929 **	Returns:
930 **		TRUE if map is all zero.
931 **		FALSE if there are any bits set in map.
932 **
933 **	Side Effects:
934 **		none.
935 */
936 
937 bool
938 bitzerop(map)
939 	BITMAP map;
940 {
941 	int i;
942 
943 	for (i = BITMAPBYTES / sizeof (int); --i >= 0; )
944 		if (map[i] != 0)
945 			return (FALSE);
946 	return (TRUE);
947 }
948 /*
949 **  STRCONTAINEDIN -- tell if one string is contained in another
950 **
951 **	Parameters:
952 **		a -- possible substring.
953 **		b -- possible superstring.
954 **
955 **	Returns:
956 **		TRUE if a is contained in b.
957 **		FALSE otherwise.
958 */
959 
960 bool
961 strcontainedin(a, b)
962 	register char *a;
963 	register char *b;
964 {
965 	int l;
966 
967 	l = strlen(a);
968 	for (;;)
969 	{
970 		b = strchr(b, a[0]);
971 		if (b == NULL)
972 			return FALSE;
973 		if (strncmp(a, b, l) == 0)
974 			return TRUE;
975 		b++;
976 	}
977 }
978