xref: /freebsd/contrib/dialog/inputstr.c (revision bf2fa8d9d11c9f2ceff09bacc406876fa37096be)
1 /*
2  *  $Id: inputstr.c,v 1.91 2021/01/17 22:19:05 tom Exp $
3  *
4  *  inputstr.c -- functions for input/display of a string
5  *
6  *  Copyright 2000-2019,2021	Thomas E. Dickey
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU Lesser General Public License, version 2.1
10  *  as published by the Free Software Foundation.
11  *
12  *  This program is distributed in the hope that it will be useful, but
13  *  WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  *  Lesser General Public License for more details.
16  *
17  *  You should have received a copy of the GNU Lesser General Public
18  *  License along with this program; if not, write to
19  *	Free Software Foundation, Inc.
20  *	51 Franklin St., Fifth Floor
21  *	Boston, MA 02110, USA.
22  */
23 
24 #include <dialog.h>
25 #include <dlg_keys.h>
26 
27 #include <errno.h>
28 
29 #ifdef HAVE_SETLOCALE
30 #include <locale.h>
31 #endif
32 
33 #if defined(HAVE_SEARCH_H) && defined(HAVE_TSEARCH)
34 #include <search.h>
35 #else
36 #undef HAVE_TSEARCH
37 #endif
38 
39 #ifdef NEED_WCHAR_H
40 #include <wchar.h>
41 #endif
42 
43 #if defined(USE_WIDE_CURSES)
44 #define USE_CACHING 1
45 #elif defined(HAVE_XDIALOG)
46 #define USE_CACHING 1		/* editbox really needs caching! */
47 #else
48 #define USE_CACHING 0
49 #endif
50 
51 typedef struct _cache {
52     struct _cache *next;
53 #if USE_CACHING
54     int cache_num;		/* tells what type of data is in list[] */
55     const char *string_at;	/* unique: associate caches by char* */
56 #endif
57     size_t s_len;		/* strlen(string) - we add 1 for EOS */
58     size_t i_len;		/* length(list) - we add 1 for EOS */
59     char *string;		/* a copy of the last-processed string */
60     int *list;			/* indices into the string */
61 } CACHE;
62 
63 #if USE_CACHING
64 #define SAME_CACHE(c,s,l) (c->string != 0 && memcmp(c->string,s,l) == 0)
65 
66 static CACHE *cache_list;
67 
68 typedef enum {
69     cInxCols
70     ,cCntWideBytes
71     ,cCntWideChars
72     ,cInxWideChars
73     ,cMAX
74 } CACHE_USED;
75 
76 #ifdef HAVE_TSEARCH
77 static void *sorted_cache;
78 #endif
79 
80 #ifdef USE_WIDE_CURSES
81 static int
82 have_locale(void)
83 {
84     static int result = -1;
85     if (result < 0) {
86 	char *test = setlocale(LC_ALL, 0);
87 	if (test == 0 || *test == 0) {
88 	    result = FALSE;
89 	} else if (strcmp(test, "C") && strcmp(test, "POSIX")) {
90 	    result = TRUE;
91 	} else {
92 	    result = FALSE;
93 	}
94     }
95     return result;
96 }
97 #endif
98 
99 #ifdef HAVE_TSEARCH
100 
101 #if 0
102 static void
103 show_tsearch(const void *nodep, const VISIT which, const int depth)
104 {
105     const CACHE *p = *(CACHE * const *) nodep;
106     (void) depth;
107     if (which == postorder || which == leaf) {
108 	DLG_TRACE(("# cache %p %p:%s\n", p, p->string, p->string));
109     }
110 }
111 
112 static void
113 trace_cache(const char *fn, int ln)
114 {
115     DLG_TRACE(("# trace_cache %s@%d\n", fn, ln));
116     twalk(sorted_cache, show_tsearch);
117 }
118 
119 #else
120 #define trace_cache(fn, ln)	/* nothing */
121 #endif
122 
123 #define CMP(a,b) (((a) > (b)) ? 1 : (((a) < (b)) ? -1 : 0))
124 
125 static int
126 compare_cache(const void *a, const void *b)
127 {
128     const CACHE *p = (const CACHE *) a;
129     const CACHE *q = (const CACHE *) b;
130     int result = CMP(p->cache_num, q->cache_num);
131     if (result == 0)
132 	result = CMP(p->string_at, q->string_at);
133     return result;
134 }
135 #endif
136 
137 static CACHE *
138 find_cache(int cache_num, const char *string)
139 {
140     CACHE *p;
141 
142 #ifdef HAVE_TSEARCH
143     void *pp;
144     CACHE find;
145 
146     memset(&find, 0, sizeof(find));
147     find.cache_num = cache_num;
148     find.string_at = string;
149 
150     if ((pp = tfind(&find, &sorted_cache, compare_cache)) != 0) {
151 	p = *(CACHE **) pp;
152     } else {
153 	p = 0;
154     }
155 #else
156     for (p = cache_list; p != 0; p = p->next) {
157 	if (p->string_at == string) {
158 	    break;
159 	}
160     }
161 #endif
162     return p;
163 }
164 
165 static CACHE *
166 make_cache(int cache_num, const char *string)
167 {
168     CACHE *p;
169 
170     p = dlg_calloc(CACHE, 1);
171     assert_ptr(p, "load_cache");
172     p->next = cache_list;
173     cache_list = p;
174 
175     p->cache_num = cache_num;
176     p->string_at = string;
177 
178 #ifdef HAVE_TSEARCH
179     (void) tsearch(p, &sorted_cache, compare_cache);
180 #endif
181     return p;
182 }
183 
184 static CACHE *
185 load_cache(int cache_num, const char *string)
186 {
187     CACHE *p;
188 
189     if ((p = find_cache(cache_num, string)) == 0) {
190 	p = make_cache(cache_num, string);
191     }
192     return p;
193 }
194 #else
195 static CACHE my_cache;
196 #define SAME_CACHE(c,s,l) (c->string != 0)
197 #define load_cache(cache, string) &my_cache
198 #endif /* USE_CACHING */
199 
200 /*
201  * If the given string has not changed, we do not need to update the index.
202  * If we need to update the index, allocate enough memory for it.
203  */
204 static bool
205 same_cache2(CACHE * cache, const char *string, unsigned i_len)
206 {
207     size_t s_len = strlen(string);
208     bool result = TRUE;
209 
210     if (cache->s_len == 0
211 	|| cache->s_len < s_len
212 	|| cache->list == 0
213 	|| !SAME_CACHE(cache, string, (size_t) s_len)) {
214 	unsigned need = (i_len + 1);
215 
216 	if (cache->list == 0) {
217 	    cache->list = dlg_malloc(int, need);
218 	} else if (cache->i_len < i_len) {
219 	    cache->list = dlg_realloc(int, need, cache->list);
220 	}
221 	assert_ptr(cache->list, "load_cache");
222 	cache->i_len = i_len;
223 
224 	if (cache->s_len >= s_len && cache->string != 0) {
225 	    strcpy(cache->string, string);
226 	} else {
227 	    if (cache->string != 0)
228 		free(cache->string);
229 	    cache->string = dlg_strclone(string);
230 	}
231 	cache->s_len = s_len;
232 
233 	result = FALSE;
234     }
235     return result;
236 }
237 
238 #ifdef USE_WIDE_CURSES
239 /*
240  * Like same_cache2(), but we are only concerned about caching a copy of the
241  * string and its associated length.
242  */
243 static bool
244 same_cache1(CACHE * cache, const char *string, size_t i_len)
245 {
246     size_t s_len = strlen(string);
247     bool result = TRUE;
248 
249     if (cache->s_len != s_len
250 	|| !SAME_CACHE(cache, string, (size_t) s_len)) {
251 
252 	if (cache->s_len >= s_len && cache->string != 0) {
253 	    strcpy(cache->string, string);
254 	} else {
255 	    if (cache->string != 0)
256 		free(cache->string);
257 	    cache->string = dlg_strclone(string);
258 	}
259 	cache->s_len = s_len;
260 	cache->i_len = i_len;
261 
262 	result = FALSE;
263     }
264     return result;
265 }
266 #endif /* USE_CACHING */
267 
268 /*
269  * Counts the number of bytes that make up complete wide-characters, up to byte
270  * 'len'.  If there is no locale set, simply return the original length.
271  */
272 #ifdef USE_WIDE_CURSES
273 static int
274 dlg_count_wcbytes(const char *string, size_t len)
275 {
276     int result;
277 
278     if (have_locale()) {
279 	CACHE *cache = load_cache(cCntWideBytes, string);
280 	if (!same_cache1(cache, string, len)) {
281 	    while (len != 0) {
282 		size_t code = 0;
283 		const char *src = cache->string;
284 		mbstate_t state;
285 		char save = cache->string[len];
286 
287 		cache->string[len] = '\0';
288 		memset(&state, 0, sizeof(state));
289 		code = mbsrtowcs((wchar_t *) 0, &src, len, &state);
290 		cache->string[len] = save;
291 		if ((int) code >= 0) {
292 		    break;
293 		}
294 		--len;
295 	    }
296 	    cache->i_len = len;
297 	}
298 	result = (int) cache->i_len;
299     } else {
300 	result = (int) len;
301     }
302     return result;
303 }
304 #endif /* USE_WIDE_CURSES */
305 
306 /*
307  * Counts the number of wide-characters in the string.
308  */
309 int
310 dlg_count_wchars(const char *string)
311 {
312     int result;
313 #ifdef USE_WIDE_CURSES
314 
315     if (have_locale()) {
316 	size_t len = strlen(string);
317 	CACHE *cache = load_cache(cCntWideChars, string);
318 
319 	if (!same_cache1(cache, string, len)) {
320 	    const char *src = cache->string;
321 	    mbstate_t state;
322 	    int part = dlg_count_wcbytes(cache->string, len);
323 	    char save = cache->string[part];
324 	    wchar_t *temp = dlg_calloc(wchar_t, len + 1);
325 
326 	    if (temp != 0) {
327 		size_t code;
328 
329 		cache->string[part] = '\0';
330 		memset(&state, 0, sizeof(state));
331 		code = mbsrtowcs(temp, &src, (size_t) part, &state);
332 		cache->i_len = ((int) code >= 0) ? wcslen(temp) : 0;
333 		cache->string[part] = save;
334 		free(temp);
335 	    } else {
336 		cache->i_len = 0;
337 	    }
338 	}
339 	result = (int) cache->i_len;
340     } else
341 #endif /* USE_WIDE_CURSES */
342     {
343 	result = (int) strlen(string);
344     }
345     return result;
346 }
347 
348 /*
349  * Build an index of the wide-characters in the string, so we can easily tell
350  * which byte-offset begins a given wide-character.
351  */
352 const int *
353 dlg_index_wchars(const char *string)
354 {
355     unsigned len = (unsigned) dlg_count_wchars(string);
356     CACHE *cache = load_cache(cInxWideChars, string);
357 
358     if (!same_cache2(cache, string, len)) {
359 	const char *current = string;
360 	unsigned inx;
361 
362 	cache->list[0] = 0;
363 	for (inx = 1; inx <= len; ++inx) {
364 #ifdef USE_WIDE_CURSES
365 	    if (have_locale()) {
366 		mbstate_t state;
367 		int width;
368 		memset(&state, 0, sizeof(state));
369 		width = (int) mbrlen(current, strlen(current), &state);
370 		if (width <= 0)
371 		    width = 1;	/* FIXME: what if we have a control-char? */
372 		current += width;
373 		cache->list[inx] = cache->list[inx - 1] + width;
374 	    } else
375 #endif /* USE_WIDE_CURSES */
376 	    {
377 		(void) current;
378 		cache->list[inx] = (int) inx;
379 	    }
380 	}
381     }
382     return cache->list;
383 }
384 
385 /*
386  * Given the character-offset to find in the list, return the corresponding
387  * array index.
388  */
389 int
390 dlg_find_index(const int *list, int limit, int to_find)
391 {
392     int result;
393     for (result = 0; result <= limit; ++result) {
394 	if (to_find == list[result]
395 	    || result == limit
396 	    || ((result < limit) && (to_find < list[result + 1]))) {
397 	    break;
398 	}
399     }
400     return result;
401 }
402 
403 /*
404  * Build a list of the display-columns for the given string's characters.
405  */
406 const int *
407 dlg_index_columns(const char *string)
408 {
409     unsigned len = (unsigned) dlg_count_wchars(string);
410     CACHE *cache = load_cache(cInxCols, string);
411 
412     if (!same_cache2(cache, string, len)) {
413 
414 	cache->list[0] = 0;
415 #ifdef USE_WIDE_CURSES
416 	if (have_locale()) {
417 	    unsigned inx;
418 	    size_t num_bytes = strlen(string);
419 	    const int *inx_wchars = dlg_index_wchars(string);
420 	    mbstate_t state;
421 
422 	    for (inx = 0; inx < len; ++inx) {
423 		int result;
424 
425 		if (string[inx_wchars[inx]] == TAB) {
426 		    result = ((cache->list[inx] | 7) + 1) - cache->list[inx];
427 		} else {
428 		    wchar_t temp[2];
429 		    size_t check;
430 
431 		    memset(&state, 0, sizeof(state));
432 		    memset(temp, 0, sizeof(temp));
433 		    check = mbrtowc(temp,
434 				    string + inx_wchars[inx],
435 				    num_bytes - (size_t) inx_wchars[inx],
436 				    &state);
437 		    if ((int) check <= 0) {
438 			result = 1;
439 		    } else {
440 			result = wcwidth(temp[0]);
441 		    }
442 		    if (result < 0) {
443 			const wchar_t *printable;
444 			cchar_t temp2, *temp2p = &temp2;
445 			setcchar(temp2p, temp, 0, 0, 0);
446 			printable = wunctrl(temp2p);
447 			result = printable ? (int) wcslen(printable) : 1;
448 		    }
449 		}
450 		cache->list[inx + 1] = result;
451 		if (inx != 0)
452 		    cache->list[inx + 1] += cache->list[inx];
453 	    }
454 	} else
455 #endif /* USE_WIDE_CURSES */
456 	{
457 	    unsigned inx;
458 
459 	    for (inx = 0; inx < len; ++inx) {
460 		chtype ch = UCH(string[inx]);
461 
462 		if (ch == TAB)
463 		    cache->list[inx + 1] =
464 			((cache->list[inx] | 7) + 1) - cache->list[inx];
465 		else if (isprint(UCH(ch)))
466 		    cache->list[inx + 1] = 1;
467 		else {
468 		    const char *printable;
469 		    printable = unctrl(ch);
470 		    cache->list[inx + 1] = (printable
471 					    ? (int) strlen(printable)
472 					    : 1);
473 		}
474 		if (inx != 0)
475 		    cache->list[inx + 1] += cache->list[inx];
476 	    }
477 	}
478     }
479     return cache->list;
480 }
481 
482 /*
483  * Returns the number of columns used for a string.  That happens to be the
484  * end-value of the cols[] array.
485  */
486 int
487 dlg_count_columns(const char *string)
488 {
489     int result = 0;
490     int limit = dlg_count_wchars(string);
491     if (limit > 0) {
492 	const int *cols = dlg_index_columns(string);
493 	result = cols[limit];
494     } else {
495 	result = (int) strlen(string);
496     }
497     dlg_finish_string(string);
498     return result;
499 }
500 
501 /*
502  * Given a column limit, count the number of wide characters that can fit
503  * into that limit.  The offset is used to skip over a leading character
504  * that was already written.
505  */
506 int
507 dlg_limit_columns(const char *string, int limit, int offset)
508 {
509     const int *cols = dlg_index_columns(string);
510     int result = dlg_count_wchars(string);
511 
512     while (result > 0 && (cols[result] - cols[offset]) > limit)
513 	--result;
514     return result;
515 }
516 
517 /*
518  * Updates the string and character-offset, given various editing characters
519  * or literal characters which are inserted at the character-offset.
520  */
521 bool
522 dlg_edit_string(char *string, int *chr_offset, int key, int fkey, bool force)
523 {
524     int i;
525     int len = (int) strlen(string);
526     int limit = dlg_count_wchars(string);
527     const int *indx = dlg_index_wchars(string);
528     int offset = dlg_find_index(indx, limit, *chr_offset);
529     int max_len = dlg_max_input(MAX_LEN);
530     bool edit = TRUE;
531 
532     /* transform editing characters into equivalent function-keys */
533     if (!fkey) {
534 	fkey = TRUE;		/* assume we transform */
535 	switch (key) {
536 	case 0:
537 	    break;
538 	case ESC:
539 	case TAB:
540 	    fkey = FALSE;	/* this is used for navigation */
541 	    break;
542 	default:
543 	    fkey = FALSE;	/* ...no, we did not transform */
544 	    break;
545 	}
546     }
547 
548     if (fkey) {
549 	switch (key) {
550 	case 0:		/* special case for loop entry */
551 	    edit = force;
552 	    break;
553 	case DLGK_GRID_LEFT:
554 	    if (*chr_offset && offset > 0)
555 		*chr_offset = indx[offset - 1];
556 	    break;
557 	case DLGK_GRID_RIGHT:
558 	    if (offset < limit)
559 		*chr_offset = indx[offset + 1];
560 	    break;
561 	case DLGK_BEGIN:
562 	    if (*chr_offset)
563 		*chr_offset = 0;
564 	    break;
565 	case DLGK_FINAL:
566 	    if (offset < limit)
567 		*chr_offset = indx[limit];
568 	    break;
569 	case DLGK_DELETE_LEFT:
570 	    if (offset) {
571 		int gap = indx[offset] - indx[offset - 1];
572 		*chr_offset = indx[offset - 1];
573 		if (gap > 0) {
574 		    for (i = *chr_offset;
575 			 (string[i] = string[i + gap]) != '\0';
576 			 i++) {
577 			;
578 		    }
579 		}
580 	    }
581 	    break;
582 	case DLGK_DELETE_RIGHT:
583 	    if (limit) {
584 		if (--limit == 0) {
585 		    string[*chr_offset = 0] = '\0';
586 		} else {
587 		    int gap = ((offset <= limit)
588 			       ? (indx[offset + 1] - indx[offset])
589 			       : 0);
590 		    if (gap > 0) {
591 			for (i = indx[offset];
592 			     (string[i] = string[i + gap]) != '\0';
593 			     i++) {
594 			    ;
595 			}
596 		    } else if (offset > 0) {
597 			string[indx[offset - 1]] = '\0';
598 		    }
599 		    if (*chr_offset > indx[limit])
600 			*chr_offset = indx[limit];
601 		}
602 	    }
603 	    break;
604 	case DLGK_DELETE_ALL:
605 	    string[*chr_offset = 0] = '\0';
606 	    break;
607 	case DLGK_ENTER:
608 	    edit = 0;
609 	    break;
610 #ifdef KEY_RESIZE
611 	case KEY_RESIZE:
612 	    edit = 0;
613 	    break;
614 #endif
615 	case DLGK_GRID_UP:
616 	case DLGK_GRID_DOWN:
617 	case DLGK_FIELD_NEXT:
618 	case DLGK_FIELD_PREV:
619 	    edit = 0;
620 	    break;
621 	case ERR:
622 	    edit = 0;
623 	    break;
624 	default:
625 	    beep();
626 	    break;
627 	}
628     } else {
629 	if (key == ESC || key == ERR) {
630 	    edit = 0;
631 	} else {
632 	    if (len < max_len) {
633 		for (i = ++len; i > *chr_offset; i--)
634 		    string[i] = string[i - 1];
635 		string[*chr_offset] = (char) key;
636 		*chr_offset += 1;
637 	    } else {
638 		(void) beep();
639 	    }
640 	}
641     }
642     return edit;
643 }
644 
645 static void
646 compute_edit_offset(const char *string,
647 		    int chr_offset,
648 		    int x_last,
649 		    int *p_dpy_column,
650 		    int *p_scroll_amt)
651 {
652     const int *cols = dlg_index_columns(string);
653     const int *indx = dlg_index_wchars(string);
654     int limit = dlg_count_wchars(string);
655     int offset = dlg_find_index(indx, limit, chr_offset);
656     int offset2;
657     int dpy_column;
658     int n;
659 
660     for (n = offset2 = 0; n <= offset; ++n) {
661 	if ((cols[offset] - cols[n]) < x_last
662 	    && (offset == limit || (cols[offset + 1] - cols[n]) < x_last)) {
663 	    offset2 = n;
664 	    break;
665 	}
666     }
667 
668     dpy_column = cols[offset] - cols[offset2];
669 
670     if (p_dpy_column != 0)
671 	*p_dpy_column = dpy_column;
672     if (p_scroll_amt != 0)
673 	*p_scroll_amt = offset2;
674 }
675 
676 /*
677  * Given the character-offset in the string, returns the display-offset where
678  * we will position the cursor.
679  */
680 int
681 dlg_edit_offset(char *string, int chr_offset, int x_last)
682 {
683     int result;
684 
685     compute_edit_offset(string, chr_offset, x_last, &result, 0);
686 
687     return result;
688 }
689 
690 /*
691  * Displays the string, shifted as necessary, to fit within the box and show
692  * the current character-offset.
693  */
694 void
695 dlg_show_string(WINDOW *win,
696 		const char *string,	/* string to display (may be multibyte) */
697 		int chr_offset,	/* character (not bytes) offset */
698 		chtype attr,	/* window-attributes */
699 		int y_base,	/* beginning row on screen */
700 		int x_base,	/* beginning column on screen */
701 		int x_last,	/* number of columns on screen */
702 		bool hidden,	/* if true, do not echo */
703 		bool force)	/* if true, force repaint */
704 {
705     x_last = MIN(x_last + x_base, getmaxx(win)) - x_base;
706 
707     if (hidden && !dialog_vars.insecure) {
708 	if (force) {
709 	    (void) wmove(win, y_base, x_base);
710 	    wrefresh(win);
711 	}
712     } else {
713 	const int *cols = dlg_index_columns(string);
714 	const int *indx = dlg_index_wchars(string);
715 	int limit = dlg_count_wchars(string);
716 
717 	int i, j, k;
718 	int input_x;
719 	int scrollamt;
720 
721 	compute_edit_offset(string, chr_offset, x_last, &input_x, &scrollamt);
722 
723 	dlg_attrset(win, attr);
724 	(void) wmove(win, y_base, x_base);
725 	for (i = scrollamt, k = 0; i < limit && k < x_last; ++i) {
726 	    int check = cols[i + 1] - cols[scrollamt];
727 	    if (check <= x_last) {
728 		for (j = indx[i]; j < indx[i + 1]; ++j) {
729 		    chtype ch = UCH(string[j]);
730 		    if (hidden && dialog_vars.insecure) {
731 			waddch(win, '*');
732 		    } else if (ch == TAB) {
733 			int count = cols[i + 1] - cols[i];
734 			while (--count >= 0)
735 			    waddch(win, ' ');
736 		    } else {
737 			waddch(win, ch);
738 		    }
739 		}
740 		k = check;
741 	    } else {
742 		break;
743 	    }
744 	}
745 	while (k++ < x_last)
746 	    waddch(win, ' ');
747 	(void) wmove(win, y_base, x_base + input_x);
748 	wrefresh(win);
749     }
750 }
751 
752 /*
753  * Discard cached data for the given string.
754  */
755 void
756 dlg_finish_string(const char *string)
757 {
758 #if USE_CACHING
759     if ((string != 0) && dialog_state.finish_string) {
760 	CACHE *p = cache_list;
761 	CACHE *q = 0;
762 	CACHE *r;
763 
764 	while (p != 0) {
765 	    if (p->string_at == string) {
766 #ifdef HAVE_TSEARCH
767 		if (tdelete(p, &sorted_cache, compare_cache) == 0) {
768 		    continue;
769 		}
770 		trace_cache(__FILE__, __LINE__);
771 #endif
772 		if (p->string != 0)
773 		    free(p->string);
774 		if (p->list != 0)
775 		    free(p->list);
776 		if (p == cache_list) {
777 		    cache_list = p->next;
778 		    r = cache_list;
779 		} else {
780 		    q->next = p->next;
781 		    r = q;
782 		}
783 		free(p);
784 		p = r;
785 	    } else {
786 		q = p;
787 		p = p->next;
788 	    }
789 	}
790     }
791 #else
792     (void) string;
793 #endif
794 }
795 
796 #ifdef NO_LEAKS
797 void
798 _dlg_inputstr_leaks(void)
799 {
800 #if USE_CACHING
801     dialog_state.finish_string = TRUE;
802     trace_cache(__FILE__, __LINE__);
803     while (cache_list != 0) {
804 	dlg_finish_string(cache_list->string_at);
805     }
806 #endif /* USE_CACHING */
807 }
808 #endif /* NO_LEAKS */
809