]> git.sur5r.net Git - cc65/blob - libsrc/common/_scanf.c
Made Olivers devnum patch (r4588) work with the PET-II models. On these
[cc65] / libsrc / common / _scanf.c
1 /*
2  * _scanf.c
3  *
4  * (c) Copyright 2001-2005, Ullrich von Bassewitz <uz@cc65.org>
5  * 2005-01-24, Greg King <gngking@erols.com>
6  *
7  * This is the basic layer for all scanf-type functions.  It should be
8  * rewritten in assembly, at some time in the future.  So, some of the code
9  * is not as elegant as it could be.
10  */
11
12
13
14 #include <stddef.h>
15 #include <stdarg.h>
16 #include <stdbool.h>
17 #include <stdio.h>
18 #include <string.h>
19 #include <setjmp.h>
20 #include <limits.h>
21 #include <errno.h>
22
23 #include <ctype.h>
24 /* _scanf() can give EOF to these functions.  But, the macroes can't
25 ** understand it; so, they are removed.
26 */
27 #undef isspace
28 #undef isxdigit
29
30 #include "_scanf.h"
31
32 extern void __fastcall__ _seterrno (unsigned char code);
33
34 #pragma static-locals(on)
35
36
37
38 /*****************************************************************************/
39 /*                            SetJmp return codes                            */
40 /*****************************************************************************/
41
42
43
44 enum {
45     RC_OK,                              /* setjmp() call */
46     RC_NOCONV,                          /* No conversion possible */
47     RC_EOF                              /* EOF reached */
48 };
49
50
51
52 /*****************************************************************************/
53 /*                                   Data                                    */
54 /*****************************************************************************/
55
56
57
58 static const char*    format;           /* Copy of function argument */
59 static const struct scanfdata* D_;      /* Copy of function argument */
60 static va_list        ap;               /* Copy of function argument */
61 static jmp_buf        JumpBuf;          /* "Label" that is used for failures */
62 static char           F;                /* Character from format string */
63 static unsigned       CharCount;        /* Characters read so far */
64 static int            C;                /* Character from input */
65 static unsigned       Width;            /* Maximum field width */
66 static long           IntVal;           /* Converted int value */
67 static int            Assignments;      /* Number of assignments */
68 static unsigned char  IntBytes;         /* Number of bytes-1 for int conversions */
69
70 /* Flags */
71 static bool           Converted;        /* Some object was converted */
72 static bool           Positive;         /* Flag for positive value */
73 static bool           NoAssign;         /* Suppress assignment */
74 static bool           Invert;           /* Do we need to invert the charset? */
75 static unsigned char  CharSet[(1+UCHAR_MAX)/CHAR_BIT];
76 static const unsigned char Bits[CHAR_BIT] = {
77     0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80
78 };
79
80 /* We need C to be 16 bits since we cannot check for EOF otherwise.
81  * Unfortunately, this causes the code to be quite larger, even if for most
82  * purposes, checking the low byte would be enough, since if C is EOF, the
83  * low byte will not match any useful character anyway (at least for the
84  * supported platforms - I know that this is not portable). So the following
85  * macro is used to access just the low byte of C.
86  */
87 #define CHAR(c)         (*((unsigned char*)&(c)))
88
89
90
91 /*****************************************************************************/
92 /*                              Character sets                               */
93 /*****************************************************************************/
94
95
96
97 /* We don't want the optimizer to ruin our "perfect" ;-)
98  * assembly code!
99  */
100 #pragma optimize (push, off)
101
102 static unsigned FindBit (void)
103 /* Locate the character's bit in the charset array.
104  * < .A - Argument character
105  * > .X - Offset of the byte in the character-set mask
106  * > .A - Bit-mask
107  */
108 {
109     asm ("pha");
110     asm ("lsr a");              /* Divide by CHAR_BIT */
111     asm ("lsr a");
112     asm ("lsr a");
113     asm ("tax");                /* Byte's offset */
114     asm ("pla");
115     asm ("and #%b", CHAR_BIT-1);
116     asm ("tay");                /* Bit's offset */
117     asm ("lda %v,y", Bits);
118     return (unsigned) __AX__;
119 }
120
121 #pragma optimize (pop)
122
123
124 static void __fastcall__ AddCharToSet (unsigned char /* C */)
125 /* Set the given bit in the character set */
126 {
127     FindBit();
128     asm ("ora %v,x", CharSet);
129     asm ("sta %v,x", CharSet);
130 }
131
132
133
134 #pragma optimize (push, off)
135
136 static unsigned char IsCharInSet (void)
137 /* Check if the char. is part of the character set. */
138 {
139     /* Get the character from C. */
140     asm ("lda #$00");
141     asm ("ldx %v+1", C);
142     asm ("bne L1");                     /* EOF never is in the set */
143     asm ("lda %v", C);
144     FindBit();
145     asm ("and %v,x", CharSet);
146     asm ("L1:");
147     asm ("ldx #$00");
148     return (unsigned char) __AX__;
149 }
150
151 #pragma optimize (pop)
152
153
154
155 static void InvertCharSet (void)
156 /* Invert the character set */
157 {
158     asm ("ldy #%b", sizeof (CharSet) - 1);
159     asm ("L1:");
160     asm ("lda %v,y", CharSet);
161     asm ("eor #$FF");
162     asm ("sta %v,y", CharSet);
163     asm ("dey");
164     asm ("bpl L1");
165 }
166
167
168
169 /*****************************************************************************/
170 /*                                   Code                                    */
171 /*****************************************************************************/
172
173
174
175 static void PushBack (void)
176 /* Push back the last (unused) character, provided it is not EOF. */
177 {
178     /* Get the character from C. */
179     /* Only the high-byte needs to be checked for EOF. */
180     asm ("ldx %v+1", C);
181     asm ("bne %g", Done);
182     asm ("lda %v", C);
183
184     /* Put unget()'s first argument on the stack. */
185     asm ("jsr pushax");
186
187     /* Copy D into the zero-page. */
188     (const struct scanfdata*) __AX__ = D_;
189     asm ("sta ptr1");
190     asm ("stx ptr1+1");
191
192     /* Copy the unget vector to jmpvec. */
193     asm ("ldy #%b", offsetof (struct scanfdata, unget));
194     asm ("lda (ptr1),y");
195     asm ("sta jmpvec+1");
196     asm ("iny");
197     asm ("lda (ptr1),y");
198     asm ("sta jmpvec+2");
199
200     /* Load D->data into __AX__. */
201     asm ("ldy #%b", offsetof (struct scanfdata, data) + 1);
202     asm ("lda (ptr1),y");
203     asm ("tax");
204     asm ("dey");
205     asm ("lda (ptr1),y");
206
207     /* Call the unget routine. */
208     asm ("jsr jmpvec");
209
210     /* Take back that character's count. */
211     asm ("lda %v", CharCount);
212     asm ("bne %g", Yank);
213     asm ("dec %v+1", CharCount);
214 Yank:
215     asm ("dec %v", CharCount);
216
217 Done:
218     ;
219 }
220
221
222
223 static void ReadChar (void)
224 /* Get an input character, count characters */
225 {
226     /* Move D to ptr1 */
227     asm ("lda %v", D_);
228     asm ("ldx %v+1", D_);
229     asm ("sta ptr1");
230     asm ("stx ptr1+1");
231
232     /* Copy the get vector to jmpvec */
233     asm ("ldy #%b", offsetof (struct scanfdata, get));
234     asm ("lda (ptr1),y");
235     asm ("sta jmpvec+1");
236     asm ("iny");
237     asm ("lda (ptr1),y");
238     asm ("sta jmpvec+2");
239
240     /* Load D->data into __AX__ */
241     asm ("ldy #%b", offsetof (struct scanfdata, data) + 1);
242     asm ("lda (ptr1),y");
243     asm ("tax");
244     asm ("dey");
245     asm ("lda (ptr1),y");
246
247     /* Call the get routine */
248     asm ("jsr jmpvec");
249
250     /* Assign the result to C */
251     asm ("sta %v", C);
252     asm ("stx %v+1", C);
253
254     /* If C is EOF, don't bump the character counter.
255      * Only the high-byte needs to be checked.
256      */
257     asm ("inx");
258     asm ("beq %g", Done);
259
260     /* Must bump CharCount. */
261     asm ("inc %v", CharCount);
262     asm ("bne %g", Done);
263     asm ("inc %v+1", CharCount);
264
265 Done:
266     ;
267 }
268
269
270
271 #pragma optimize (push, off)
272
273 static void __fastcall__ Error (unsigned char /* Code */)
274 /* Does a longjmp using the given code */
275 {
276     asm ("pha");
277     (char*) __AX__ = JumpBuf;
278     asm ("jsr pushax");
279     asm ("pla");
280     asm ("ldx #>0");
281     asm ("jmp %v", longjmp);
282 }
283
284 #pragma optimize (pop)
285
286
287
288 static void CheckEnd (void)
289 /* Stop a scan if it prematurely reaches the end of a string or a file. */
290 {
291     /* Only the high-byte needs to be checked for EOF. */
292     asm ("ldx %v+1", C);
293     asm ("beq %g", Done);
294
295         Error (RC_EOF);
296 Done:
297     ;
298 }
299
300
301
302 static void SkipWhite (void)
303 /* Skip white space in the input and return the first non white character */
304 {
305     while ((bool) isspace (C)) {
306         ReadChar ();
307     }
308 }
309
310
311
312 #pragma optimize (push, off)
313
314 static void ReadSign (void)
315 /* Read an optional sign and skip it. Store 1 in Positive if the value is
316  * positive, store 0 otherwise.
317  */
318 {
319     /* We can ignore the high byte of C here, since if it is EOF, the lower
320      * byte won't match anyway.
321      */
322     asm ("lda %v", C);
323     asm ("cmp #'-'");
324     asm ("bne %g", NotNeg);
325
326     /* Negative value */
327     asm ("sta %v", Converted);
328     asm ("jsr %v", ReadChar);
329     asm ("lda #$00");           /* Flag as negative */
330     asm ("beq %g", Store);
331
332     /* Positive value */
333 NotNeg:
334     asm ("cmp #'+'");
335     asm ("bne %g", Pos);
336     asm ("sta %v", Converted);
337     asm ("jsr %v", ReadChar);   /* Skip the + sign */
338 Pos:
339     asm ("lda #$01");           /* Flag as positive */
340 Store:
341     asm ("sta %v", Positive);
342 }
343
344 #pragma optimize (pop)
345
346
347
348 static unsigned char __fastcall__ HexVal (char C)
349 /* Convert a digit to a value */
350 {
351     return (bool) isdigit (C) ?
352         C - '0' :
353         (char) tolower ((int) C) - ('a' - 10);
354 }
355
356
357
358 static void __fastcall__ ReadInt (unsigned char Base)
359 /* Read an integer, and store it into IntVal. */
360 {
361     unsigned char Val, CharCount = 0;
362
363     /* Read the integer value */
364     IntVal = 0L;
365     while ((bool) isxdigit (C) && ++Width != 0
366            && (Val = HexVal ((char) C)) < Base) {
367         ++CharCount;
368         IntVal = IntVal * (long) Base + (long) Val;
369         ReadChar ();
370     }
371
372     /* If we didn't convert anything, it's a failure. */
373     if (CharCount == 0) {
374         Error (RC_NOCONV);
375     }
376
377     /* Another conversion */
378     Converted = true;
379 }
380
381
382
383 static void AssignInt (void)
384 /* Assign the integer value in Val to the next argument. The function makes
385  * several non-portable assumptions, to reduce code size:
386  *   - signed and unsigned types have the same representation.
387  *   - short and int have the same representation.
388  *   - all pointer types have the same representation.
389  */
390 {
391     if (NoAssign == false) {
392
393         /* Get the next argument pointer */
394         (void*) __AX__ = va_arg (ap, void*);
395
396         /* Put the argument pointer into the zero-page. */
397         asm ("sta ptr1");
398         asm ("stx ptr1+1");
399
400         /* Get the number of bytes-1 to copy */
401         asm ("ldy %v", IntBytes);
402
403         /* Assign the integer value */
404 Loop:   asm ("lda %v,y", IntVal);
405         asm ("sta (ptr1),y");
406         asm ("dey");
407         asm ("bpl %g", Loop);
408
409         /* Another assignment */
410         asm ("inc %v", Assignments);
411         asm ("bne %g", Done);
412         asm ("inc %v+1", Assignments);
413 Done:   ;
414     }
415 }
416
417
418
419 static void __fastcall__ ScanInt (unsigned char Base)
420 /* Scan an integer including white space, sign and optional base spec,
421  * and store it into IntVal.
422  */
423 {
424     /* Skip whitespace */
425     SkipWhite ();
426
427     /* Read an optional sign */
428     ReadSign ();
429
430     /* If Base is unknown (zero), figure it out */
431     if (Base == 0) {
432         if (CHAR (C) == '0') {
433             ReadChar ();
434             switch (CHAR (C)) {
435                 case 'x':
436                 case 'X':
437                     Base = 16;
438                     Converted = true;
439                     ReadChar ();
440                     break;
441                 default:
442                     Base = 8;
443
444                     /* Restart at the beginning of the number because it might
445                      * be only a single zero digit (which already was read).
446                      */
447                     PushBack ();
448                     C = '0';
449             }
450         } else {
451             Base = 10;
452         }
453     }
454
455     /* Read the integer value */
456     ReadInt (Base);
457
458     /* Apply the sign */
459     if (Positive == false) {
460         IntVal = -IntVal;
461     }
462
463     /* Assign the value to the next argument unless suppressed */
464     AssignInt ();
465 }
466
467
468
469 static char GetFormat (void)
470 /* Pick up the next character from the format string. */
471 {
472 /*  return (F = *format++); */
473     (const char*) __AX__ = format;
474     asm ("sta regsave");
475     asm ("stx regsave+1");
476     ++format;
477     asm ("ldy #0");
478     asm ("lda (regsave),y");
479     asm ("ldx #>0");
480     return (F = (char) __AX__);
481 }
482
483
484
485 int __fastcall__ _scanf (const struct scanfdata* D,
486                          const char* format_, va_list ap_)
487 /* This is the routine used to do the actual work. It is called from several
488  * types of wrappers to implement the actual ISO xxscanf functions.
489  */
490 {
491     register char* S;
492              bool  HaveWidth;   /* True if a width was given */
493              bool  Match;       /* True if a character-set has any matches */
494              char  Start;       /* Walks over a range */
495
496     /* Place copies of the arguments into global variables. This is not very
497      * nice, but on a 6502 platform it gives better code, since the values
498      * do not have to be passed as parameters.
499      */
500     D_     = D;
501     format = format_;
502     ap     = ap_;
503
504     /* Initialize variables */
505     Converted   = false;
506     Assignments = 0;
507     CharCount   = 0;
508
509     /* Set up the jump "label".  CheckEnd() will use that label when EOF
510      * is reached.  ReadInt() will use it when number-conversion fails.
511      */
512     if ((unsigned char) setjmp (JumpBuf) == RC_OK) {
513 Again:
514
515         /* Get the next input character */
516         ReadChar ();
517
518         /* Walk over the format string */
519         while (GetFormat ()) {
520
521             /* Check for a conversion */
522             if (F != '%') {
523
524                 /* Check for a match */
525                 if ((bool) isspace ((int) F)) {
526
527                     /* Special white space handling: Any whitespace in the
528                      * format string matches any amount of whitespace including
529                      * none(!). So this match will never fail.
530                      */
531                     SkipWhite ();
532                     continue;
533                 }
534
535 Percent:
536                 /* ### Note:  The opposite test (C == F)
537                 ** would be optimized into buggy code!
538                 */
539                 if (C != (int) F) {
540
541                     /* A mismatch -- we will stop scanning the input,
542                      * and return the number of assigned conversions.
543                      */
544                     goto NoConv;
545                 }
546
547                 /* A match -- get the next input character, and continue. */
548                 goto Again;
549
550             } else {
551
552                 /* A conversion. Skip the percent sign. */
553                 /* 0. Check for %% */
554                 if (GetFormat () == '%') {
555                     goto Percent;
556                 }
557
558                 /* 1. Assignment suppression */
559                 NoAssign = (F == '*');
560                 if (NoAssign) {
561                     GetFormat ();
562                 }
563
564                 /* 2. Maximum field width */
565                 Width     = UINT_MAX;
566                 HaveWidth = (bool) isdigit (F);
567                 if (HaveWidth) {
568                     Width = 0;
569                     do {
570                         /* ### Non portable ### */
571                         Width = Width * 10 + (F & 0x0F);
572                     } while ((bool) isdigit (GetFormat ()));
573                 }
574                 if (Width == 0) {
575                     /* Invalid specification */
576                     /* Note:  This method of leaving the function might seem
577                      * to be crude, but it optimizes very well because
578                      * the four exits can share this code.
579                      */
580                     _seterrno (EINVAL);
581                     Assignments = EOF;
582                     PushBack ();
583                     return Assignments;
584                 }
585                 /* Increment-and-test makes better code than test-and-decrement
586                  * does.  So, change the width into a form that can be used in
587                  * that way.
588                  */
589                 Width = ~Width;
590
591                 /* 3. Length modifier */
592                 IntBytes = sizeof(int) - 1;
593                 switch (F) {
594                     case 'h':
595                         if (*format == 'h') {
596                             IntBytes = sizeof(char) - 1;
597                             ++format;
598                         }
599                         GetFormat ();
600                         break;
601
602                     case 'l':
603                         if (*format == 'l') {
604                             /* Treat long long as long */
605                             ++format;
606                         }
607                         /* FALLTHROUGH */
608                     case 'j':   /* intmax_t */
609                         IntBytes = sizeof(long) - 1;
610                         /* FALLTHROUGH */
611
612                     case 'z':   /* size_t */
613                     case 't':   /* ptrdiff_t */
614                         /* Same size as int */
615
616                     case 'L':   /* long double - ignore this one */
617                         GetFormat ();
618                 }
619
620                 /* 4. Conversion specifier */
621                 switch (F) {
622                     /* 'd' and 'u' conversions are actually the same, since the
623                      * standard says that even the 'u' modifier allows an
624                      * optionally signed integer.
625                      */
626                     case 'd':   /* Optionally signed decimal integer */
627                     case 'u':
628                         ScanInt (10);
629                         break;
630
631                     case 'i':
632                         /* Optionally signed integer with a base */
633                         ScanInt (0);
634                         break;
635
636                     case 'o':
637                         /* Optionally signed octal integer */
638                         ScanInt (8);
639                         break;
640
641                     case 'x':
642                     case 'X':
643                         /* Optionally signed hexadecimal integer */
644                         ScanInt (16);
645                         break;
646
647                     case 's':
648                         /* Whitespace-terminated string */
649                         SkipWhite ();
650                         CheckEnd ();       /* Is it an input failure? */
651                         Converted = true;  /* No, conversion will succeed */
652                         if (NoAssign == false) {
653                             S = va_arg (ap, char*);
654                         }
655                         while (C != EOF
656                                && (bool) isspace (C) == false
657                                && ++Width) {
658                             if (NoAssign == false) {
659                                 *S++ = C;
660                             }
661                             ReadChar ();
662                         }
663                         /* Terminate the string just read */
664                         if (NoAssign == false) {
665                             *S = '\0';
666                             ++Assignments;
667                         }
668                         break;
669
670                     case 'c':
671                         /* Fixed-length string, NOT zero-terminated */
672                         if (HaveWidth == false) {
673                             /* No width given, default is 1 */
674                             Width = ~1u;
675                         }
676                         CheckEnd ();       /* Is it an input failure? */
677                         Converted = true;  /* No, at least 1 char. available */
678                         if (NoAssign == false) {
679                             S = va_arg (ap, char*);
680                             /* ## This loop is convenient for us, but it isn't
681                              * standard C.  The standard implies that a failure
682                              * shouldn't put anything into the array argument.
683                              */
684                             while (++Width) {
685                                 CheckEnd ();  /* Is it a matching failure? */
686                                 *S++ = C;
687                                 ReadChar ();
688                             }
689                             ++Assignments;
690                         } else {
691                             /* Just skip as many chars as given */
692                             while (++Width) {
693                                 CheckEnd ();  /* Is it a matching failure? */
694                                 ReadChar ();
695                             }
696                         }
697                         break;
698
699                     case '[':
700                         /* String using characters from a set */
701                         /* Clear the set */
702                         memset (CharSet, 0, sizeof (CharSet));
703                         /* Skip the left-bracket, and test for inversion. */
704                         Invert = (GetFormat () == '^');
705                         if (Invert) {
706                             GetFormat ();
707                         }
708                         if (F == ']') {
709                             /* Empty sets aren't allowed; so, a right-bracket
710                              * at the beginning must be a member of the set.
711                              */
712                             AddCharToSet (F);
713                             GetFormat ();
714                         }
715                         /* Read the characters that are part of the set */
716                         while (F != '\0' && F != ']') {
717                             if (*format == '-') {  /* Look ahead at next char. */
718                                 /* A range. Get start and end, skip the '-' */
719                                 Start = F;
720                                 ++format;
721                                 switch (GetFormat ()) {
722                                     case '\0':
723                                     case ']':
724                                         /* '-' as last char means:  include '-' */
725                                         AddCharToSet (Start);
726                                         AddCharToSet ('-');
727                                         break;
728                                     default:
729                                         /* Include all characters
730                                          * that are in the range.
731                                          */
732                                         while (1) {
733                                             AddCharToSet (Start);
734                                             if (Start == F) {
735                                                 break;
736                                             }
737                                             ++Start;
738                                         }
739                                         /* Get next char after range */
740                                         GetFormat ();
741                                 }
742                             } else {
743                                 /* Just a character */
744                                 AddCharToSet (F);
745                                 /* Get next char */
746                                 GetFormat ();
747                             }
748                         }
749                         /* Don't go beyond the end of the format string. */
750                         /* (Maybe, this should mean an invalid specification.) */
751                         if (F == '\0') {
752                             --format;
753                         }
754
755                         /* Invert the set if requested */
756                         if (Invert) {
757                             InvertCharSet ();
758                         }
759
760                         /* We have the set in CharSet. Read characters and
761                          * store them into a string while they are part of
762                          * the set.
763                          */
764                         Match = false;
765                         if (NoAssign == false) {
766                             S = va_arg (ap, char*);
767                         }
768                         while (IsCharInSet () && ++Width) {
769                             if (NoAssign == false) {
770                                 *S++ = C;
771                             }
772                             Match = Converted = true;
773                             ReadChar ();
774                         }
775                         /* At least one character must match the set. */
776                         if (Match == false) {
777                             goto NoConv;
778                         }
779                         if (NoAssign == false) {
780                             *S = '\0';
781                             ++Assignments;
782                         }
783                         break;
784
785                     case 'p':
786                         /* Pointer, general format is 0xABCD.
787                          * %hhp --> zero-page pointer
788                          * %hp --> near pointer
789                          * %lp --> far pointer
790                          */
791                         SkipWhite ();
792                         if (CHAR (C) != '0') {
793                             goto NoConv;
794                         }
795                         Converted = true;
796                         ReadChar ();
797                         switch (CHAR (C)) {
798                             case 'x':
799                             case 'X':
800                                 break;
801                             default:
802                                 goto NoConv;
803                         }
804                         ReadChar ();
805                         ReadInt (16);
806                         AssignInt ();
807                         break;
808
809                     case 'n':
810                         /* Store the number of characters consumed so far
811                          * (the read-ahead character hasn't been consumed).
812                          */
813                         IntVal = (long) (CharCount - (C == EOF ? 0u : 1u));
814                         AssignInt ();
815                         /* Don't count it. */
816                         if (NoAssign == false) {
817                             --Assignments;
818                         }
819                         break;
820
821                     case 'S':
822                     case 'C':
823                         /* Wide characters */
824
825                     case 'a':
826                     case 'A':
827                     case 'e':
828                     case 'E':
829                     case 'f':
830                     case 'F':
831                     case 'g':
832                     case 'G':
833                         /* Optionally signed float */
834
835                         /* Those 2 groups aren't implemented. */
836                         _seterrno (ENOSYS);
837                         Assignments = EOF;
838                         PushBack ();
839                         return Assignments;
840
841                     default:
842                         /* Invalid specification */
843                         _seterrno (EINVAL);
844                         Assignments = EOF;
845                         PushBack ();
846                         return Assignments;
847                 }
848             }
849         }
850     } else {
851 NoConv:
852
853         /* Coming here means a failure. If that happens at EOF, with no
854          * conversion attempts, then it is considered an error; otherwise,
855          * the number of assignments is returned (the default behaviour).
856          */
857         if (C == EOF && Converted == false) {
858             Assignments = EOF;  /* Special case:  error */
859         }
860     }
861
862     /* Put the read-ahead character back into the input stream. */
863     PushBack ();
864
865     /* Return the number of conversion-and-assignments. */
866     return Assignments;
867 }
868
869
870