View Javadoc
1   package org.apache.maven.surefire.api.stream;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
23  import org.apache.maven.surefire.api.fork.ForkNodeArguments;
24  
25  import javax.annotation.Nonnegative;
26  import javax.annotation.Nonnull;
27  import java.io.EOFException;
28  import java.io.File;
29  import java.io.IOException;
30  import java.nio.Buffer;
31  import java.nio.ByteBuffer;
32  import java.nio.CharBuffer;
33  import java.nio.channels.ReadableByteChannel;
34  import java.nio.charset.Charset;
35  import java.nio.charset.CharsetDecoder;
36  import java.nio.charset.CoderResult;
37  import java.util.ArrayList;
38  import java.util.List;
39  import java.util.Map;
40  
41  import static java.lang.Math.max;
42  import static java.lang.Math.min;
43  import static java.nio.charset.CodingErrorAction.REPLACE;
44  import static java.nio.charset.StandardCharsets.US_ASCII;
45  import static java.util.Arrays.copyOf;
46  import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
47  import static org.apache.maven.surefire.api.stream.AbstractStreamDecoder.StreamReadStatus.OVERFLOW;
48  import static org.apache.maven.surefire.api.stream.AbstractStreamDecoder.StreamReadStatus.UNDERFLOW;
49  import static org.apache.maven.surefire.shared.lang3.StringUtils.isBlank;
50  
51  /**
52   * @param <M> message object
53   * @param <MT> enum describing the meaning of the message
54   * @param <ST> enum for segment type
55   */
56  public abstract class AbstractStreamDecoder<M, MT extends Enum<MT>, ST extends Enum<ST>> implements AutoCloseable
57  {
58      public static final int BUFFER_SIZE = 1024;
59  
60      private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
61  
62      private static final String[] JVM_ERROR_PATTERNS = {
63          "could not create the java virtual machine", "error occurred during initialization", // of VM, of boot layer
64          "error:", // general errors
65          "could not reserve enough space", "could not allocate", "unable to allocate", // memory errors
66          "java.lang.module.findexception" // JPMS errors
67      };
68  
69      private static final byte[] DEFAULT_STREAM_ENCODING_BYTES = DEFAULT_STREAM_ENCODING.name().getBytes( US_ASCII );
70  
71      private static final int NO_POSITION = -1;
72      private static final int DELIMITER_LENGTH = 1;
73      private static final int BYTE_LENGTH = 1;
74      private static final int INT_LENGTH = 4;
75      private static final int LONG_LENGTH = 8;
76  
77      private final ReadableByteChannel channel;
78      private final ForkNodeArguments arguments;
79      private final Map<Segment, MT> messageTypes;
80      private final ConsoleLogger logger;
81  
82      protected AbstractStreamDecoder( @Nonnull ReadableByteChannel channel,
83                                       @Nonnull ForkNodeArguments arguments,
84                                       @Nonnull Map<Segment, MT> messageTypes )
85      {
86          this.channel = channel;
87          this.arguments = arguments;
88          this.messageTypes = messageTypes;
89          logger = arguments.getConsoleLogger();
90      }
91  
92      public abstract M decode( @Nonnull Memento memento ) throws MalformedChannelException, IOException;
93  
94      @Nonnull
95      protected abstract byte[] getEncodedMagicNumber();
96  
97      @Nonnull
98      protected abstract ST[] nextSegmentType( @Nonnull MT messageType );
99  
100     @Nonnull
101     protected abstract M toMessage( @Nonnull MT messageType, @Nonnull Memento memento )
102         throws MalformedFrameException;
103 
104     @Nonnull
105     protected final ForkNodeArguments getArguments()
106     {
107         return arguments;
108     }
109 
110     protected void debugStream( byte[] array, int position, int remaining )
111     {
112     }
113 
114     protected MT readMessageType( @Nonnull Memento memento ) throws IOException, MalformedFrameException
115     {
116         byte[] header = getEncodedMagicNumber();
117         int readCount = DELIMITER_LENGTH + header.length + DELIMITER_LENGTH + BYTE_LENGTH + DELIMITER_LENGTH;
118         read( memento, readCount );
119         checkHeader( memento );
120         return messageTypes.get( readSegment( memento ) );
121     }
122 
123     @Nonnull
124     @SuppressWarnings( "checkstyle:magicnumber" )
125     protected Segment readSegment( @Nonnull Memento memento ) throws IOException, MalformedFrameException
126     {
127         int readCount = readByte( memento ) & 0xff;
128         read( memento, readCount + DELIMITER_LENGTH );
129         ByteBuffer bb = memento.getByteBuffer();
130         Segment segment = new Segment( bb.array(), bb.arrayOffset() + ( (Buffer) bb ).position(), readCount );
131         ( (Buffer) bb ).position( ( (Buffer) bb ).position() + readCount );
132         checkDelimiter( memento );
133         return segment;
134     }
135 
136     @Nonnull
137     @SuppressWarnings( "checkstyle:magicnumber" )
138     protected Charset readCharset( @Nonnull Memento memento ) throws IOException, MalformedFrameException
139     {
140         int length = readByte( memento ) & 0xff;
141         read( memento, length + DELIMITER_LENGTH );
142         ByteBuffer bb = memento.getByteBuffer();
143         byte[] array = bb.array();
144         int offset = bb.arrayOffset() + ( (Buffer) bb ).position();
145         ( (Buffer) bb ).position( ( (Buffer) bb ).position() + length );
146         boolean isDefaultEncoding = false;
147         if ( length == DEFAULT_STREAM_ENCODING_BYTES.length )
148         {
149             isDefaultEncoding = true;
150             for ( int i = 0; i < length; i++ )
151             {
152                 isDefaultEncoding &= DEFAULT_STREAM_ENCODING_BYTES[i] == array[offset + i];
153             }
154         }
155 
156         try
157         {
158             Charset charset =
159                 isDefaultEncoding
160                     ? DEFAULT_STREAM_ENCODING
161                     : Charset.forName( new String( array, offset, length, US_ASCII ) );
162 
163             checkDelimiter( memento );
164             return charset;
165         }
166         catch ( IllegalArgumentException e )
167         {
168             throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(), ( (Buffer) bb ).position() );
169         }
170     }
171 
172     protected String readString( @Nonnull Memento memento ) throws IOException, MalformedFrameException
173     {
174         ( (Buffer) memento.getCharBuffer() ).clear();
175         int readCount = readInt( memento );
176         if ( readCount < 0 )
177         {
178             throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
179                 ( (Buffer) memento.getByteBuffer() ).position() );
180         }
181         read( memento, readCount + DELIMITER_LENGTH );
182 
183         final String string;
184         if ( readCount == 0 )
185         {
186             string = "";
187         }
188         else if ( readCount == 1 )
189         {
190             read( memento, 1 );
191             byte oneChar = memento.getByteBuffer().get();
192             string = oneChar == 0 ? null : String.valueOf( (char) oneChar );
193         }
194         else
195         {
196             string = readString( memento, readCount );
197         }
198         read( memento, 1 );
199         checkDelimiter( memento );
200         return string;
201     }
202 
203     protected Integer readInteger( @Nonnull Memento memento ) throws IOException, MalformedFrameException
204     {
205         read( memento, BYTE_LENGTH );
206         boolean isNullObject = memento.getByteBuffer().get() == 0;
207         if ( isNullObject )
208         {
209             read( memento, DELIMITER_LENGTH );
210             checkDelimiter( memento );
211             return null;
212         }
213         return readInt( memento );
214     }
215 
216     protected byte readByte( @Nonnull Memento memento ) throws IOException, MalformedFrameException
217     {
218         read( memento, BYTE_LENGTH + DELIMITER_LENGTH );
219         byte b = memento.getByteBuffer().get();
220         checkDelimiter( memento );
221         return b;
222     }
223 
224     protected int readInt( @Nonnull Memento memento ) throws IOException, MalformedFrameException
225     {
226         read( memento, INT_LENGTH + DELIMITER_LENGTH );
227         int i = memento.getByteBuffer().getInt();
228         checkDelimiter( memento );
229         return i;
230     }
231 
232     protected Long readLong( @Nonnull Memento memento ) throws IOException, MalformedFrameException
233     {
234         read( memento, BYTE_LENGTH );
235         boolean isNullObject = memento.getByteBuffer().get() == 0;
236         if ( isNullObject )
237         {
238             read( memento, DELIMITER_LENGTH );
239             checkDelimiter( memento );
240             return null;
241         }
242         return readLongPrivate( memento );
243     }
244 
245     protected long readLongPrivate( @Nonnull Memento memento ) throws IOException, MalformedFrameException
246     {
247         read( memento, LONG_LENGTH + DELIMITER_LENGTH );
248         long num = memento.getByteBuffer().getLong();
249         checkDelimiter( memento );
250         return num;
251     }
252 
253     @SuppressWarnings( "checkstyle:magicnumber" )
254     protected final void checkDelimiter( Memento memento ) throws MalformedFrameException
255     {
256         ByteBuffer bb = memento.bb;
257         if ( ( 0xff & bb.get() ) != ':' )
258         {
259             throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(), ( (Buffer) bb ).position() );
260         }
261     }
262 
263     protected final void checkHeader( Memento memento ) throws MalformedFrameException
264     {
265         ByteBuffer bb = memento.bb;
266 
267         checkDelimiter( memento );
268 
269         int shift = 0;
270         try
271         {
272             byte[] header = getEncodedMagicNumber();
273             byte[] bbArray = bb.array();
274             for ( int start = bb.arrayOffset() + ( (Buffer) bb ).position(), length = header.length;
275                   shift < length; shift++ )
276             {
277                 if ( bbArray[shift + start] != header[shift] )
278                 {
279                     throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
280                         ( (Buffer) bb ).position() + shift );
281                 }
282             }
283         }
284         finally
285         {
286             ( (Buffer) bb ).position( ( (Buffer) bb ).position() + shift );
287         }
288 
289         checkDelimiter( memento );
290     }
291 
292     protected void checkArguments( Memento memento, int expectedDataElements )
293         throws MalformedFrameException
294     {
295         if ( memento.getData().size() != expectedDataElements )
296         {
297             throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
298                 ( (Buffer) memento.getByteBuffer() ).position() );
299         }
300     }
301 
302     private String readString( @Nonnull final Memento memento, @Nonnegative final int totalBytes )
303         throws IOException, MalformedFrameException
304     {
305         memento.getDecoder().reset();
306         final CharBuffer output = memento.getCharBuffer();
307         ( (Buffer) output ).clear();
308         final ByteBuffer input = memento.getByteBuffer();
309         final List<String> strings = new ArrayList<>();
310         int countDecodedBytes = 0;
311         for ( boolean endOfInput = false; !endOfInput; )
312         {
313             final int bytesToRead = totalBytes - countDecodedBytes;
314             read( memento, bytesToRead );
315             int bytesToDecode = min( input.remaining(), bytesToRead );
316             final boolean isLastChunk = bytesToDecode == bytesToRead;
317             endOfInput = countDecodedBytes + bytesToDecode >= totalBytes;
318             do
319             {
320                 boolean endOfChunk = output.remaining() >= bytesToRead;
321                 boolean endOfOutput = isLastChunk && endOfChunk;
322                 int readInputBytes = decodeString( memento.getDecoder(), input, output, bytesToDecode, endOfOutput,
323                     memento.getLine().getPositionByteBuffer() );
324                 bytesToDecode -= readInputBytes;
325                 countDecodedBytes += readInputBytes;
326             }
327             while ( isLastChunk && bytesToDecode > 0 && output.hasRemaining() );
328 
329             if ( isLastChunk || !output.hasRemaining() )
330             {
331                 strings.add( ( (Buffer) output ).flip().toString() );
332                 ( (Buffer) output ).clear();
333             }
334         }
335 
336         memento.getDecoder().reset();
337         ( (Buffer) output ).clear();
338 
339         return toString( strings );
340     }
341 
342     private static int decodeString( @Nonnull CharsetDecoder decoder, @Nonnull ByteBuffer input,
343                                      @Nonnull CharBuffer output, @Nonnegative int bytesToDecode,
344                                      boolean endOfInput, @Nonnegative int errorStreamFrom )
345         throws MalformedFrameException
346     {
347         int limit = ( (Buffer) input ).limit();
348         ( (Buffer) input ).limit( ( (Buffer) input ).position() + bytesToDecode );
349 
350         CoderResult result = decoder.decode( input, output, endOfInput );
351         if ( result.isError() || result.isMalformed() )
352         {
353             throw new MalformedFrameException( errorStreamFrom, ( (Buffer) input ).position() );
354         }
355 
356         int decodedBytes = bytesToDecode - input.remaining();
357         ( (Buffer) input ).limit( limit );
358         return decodedBytes;
359     }
360 
361     private static String toString( List<String> strings )
362     {
363         if ( strings.size() == 1 )
364         {
365             return strings.get( 0 );
366         }
367         StringBuilder concatenated = new StringBuilder( strings.size() * BUFFER_SIZE );
368         for ( String s : strings )
369         {
370             concatenated.append( s );
371         }
372         return concatenated.toString();
373     }
374 
375     private void printCorruptedStream( Memento memento )
376     {
377         ByteBuffer bb = memento.getByteBuffer();
378         if ( bb.hasRemaining() )
379         {
380             int bytesToWrite = bb.remaining();
381             memento.getLine().write( bb, ( (Buffer) bb ).position(), bytesToWrite );
382             ( (Buffer) bb ).position( ( (Buffer) bb ).position() + bytesToWrite );
383         }
384     }
385 
386     /**
387      * Print the last string which has not been finished by a new line character.
388      *
389      * @param memento current memento object
390      */
391     protected final void printRemainingStream( Memento memento )
392     {
393         printCorruptedStream( memento );
394         memento.getLine().printExistingLine();
395         memento.getLine().clear();
396     }
397 
398     /**
399      *
400      */
401     public static final class Segment
402     {
403         private final byte[] array;
404         private final int fromIndex;
405         private final int length;
406         private final int hashCode;
407 
408         public Segment( byte[] array, int fromIndex, int length )
409         {
410             this.array = array;
411             this.fromIndex = fromIndex;
412             this.length = length;
413 
414             int hashCode = 0;
415             int i = fromIndex;
416             for ( int loops = length >> 1; loops-- != 0; )
417             {
418                 hashCode = 31 * hashCode + array[i++];
419                 hashCode = 31 * hashCode + array[i++];
420             }
421             this.hashCode = i == fromIndex + length ? hashCode : 31 * hashCode + array[i];
422         }
423 
424         @Override
425         public int hashCode()
426         {
427             return hashCode;
428         }
429 
430         @Override
431         public boolean equals( Object obj )
432         {
433             if ( !( obj instanceof Segment ) )
434             {
435                 return false;
436             }
437 
438             Segment that = (Segment) obj;
439             if ( that.length != length )
440             {
441                 return false;
442             }
443 
444             for ( int i = 0; i < length; i++ )
445             {
446                 if ( that.array[that.fromIndex + i] != array[fromIndex + i] )
447                 {
448                     return false;
449                 }
450             }
451             return true;
452         }
453     }
454 
455     protected @Nonnull StreamReadStatus read( @Nonnull Memento memento, int recommendedCount ) throws IOException
456     {
457         ByteBuffer buffer = memento.getByteBuffer();
458         if ( buffer.remaining() >= recommendedCount && ( (Buffer) buffer ).limit() != 0 )
459         {
460             return OVERFLOW;
461         }
462         else
463         {
464             if ( ( (Buffer) buffer ).position() != 0
465                 && recommendedCount > buffer.capacity() - ( (Buffer) buffer ).position() )
466             {
467                 ( (Buffer) buffer.compact() ).flip();
468                 memento.getLine().setPositionByteBuffer( 0 );
469             }
470             int mark = ( (Buffer) buffer ).position();
471             ( (Buffer) buffer ).position( ( (Buffer) buffer ).limit() );
472             ( (Buffer) buffer ).limit( min( ( (Buffer) buffer ).position() + recommendedCount, buffer.capacity() ) );
473             return read( buffer, mark, recommendedCount );
474         }
475     }
476 
477     private StreamReadStatus read( ByteBuffer buffer, int oldPosition, int recommendedCount )
478         throws IOException
479     {
480         StreamReadStatus readStatus = null;
481         boolean isEnd = false;
482         try
483         {
484             while ( !isEnd && ( (Buffer) buffer ).position() - oldPosition < recommendedCount
485                 && ( (Buffer) buffer ).position() < ( (Buffer) buffer ).limit() )
486             {
487                 isEnd = channel.read( buffer ) == -1;
488             }
489         }
490         finally
491         {
492             ( (Buffer) buffer ).limit( ( (Buffer) buffer ).position() );
493             ( (Buffer) buffer ).position( oldPosition );
494             int readBytes = buffer.remaining();
495             boolean readComplete = readBytes >= recommendedCount;
496             if ( !isEnd || readComplete )
497             {
498                 debugStream( buffer.array(),
499                     buffer.arrayOffset() + ( (Buffer) buffer ).position(), buffer.remaining() );
500                 readStatus = readComplete ? OVERFLOW : UNDERFLOW;
501             }
502         }
503 
504         if ( readStatus == null )
505         {
506             throw new EOFException();
507         }
508         else
509         {
510             return readStatus;
511         }
512     }
513 
514     /**
515      *
516      */
517     public final class Memento
518     {
519         private CharsetDecoder currentDecoder;
520         private final CharsetDecoder defaultDecoder;
521         private final BufferedStream line = new BufferedStream( 32 );
522         private final List<Object> data = new ArrayList<>();
523         private final CharBuffer cb = CharBuffer.allocate( BUFFER_SIZE );
524         private final ByteBuffer bb = ByteBuffer.allocate( BUFFER_SIZE );
525 
526         public Memento()
527         {
528             defaultDecoder = DEFAULT_STREAM_ENCODING.newDecoder()
529                 .onMalformedInput( REPLACE )
530                 .onUnmappableCharacter( REPLACE );
531             ( (Buffer) bb ).limit( 0 );
532         }
533 
534         public void reset()
535         {
536             currentDecoder = null;
537             data.clear();
538         }
539 
540         public CharsetDecoder getDecoder()
541         {
542             return currentDecoder == null ? defaultDecoder : currentDecoder;
543         }
544 
545         public void setCharset( Charset charset )
546         {
547             if ( charset.name().equals( defaultDecoder.charset().name() ) )
548             {
549                 currentDecoder = defaultDecoder;
550             }
551             else
552             {
553                 currentDecoder = charset.newDecoder()
554                     .onMalformedInput( REPLACE )
555                     .onUnmappableCharacter( REPLACE );
556             }
557         }
558 
559         public BufferedStream getLine()
560         {
561             return line;
562         }
563 
564         public List<Object> getData()
565         {
566             return data;
567         }
568 
569         public <T> T ofDataAt( int indexOfData )
570         {
571             //noinspection unchecked
572             return (T) data.get( indexOfData );
573         }
574 
575         public CharBuffer getCharBuffer()
576         {
577             return cb;
578         }
579 
580         public ByteBuffer getByteBuffer()
581         {
582             return bb;
583         }
584     }
585 
586     /**
587      * This class avoids locking which gains the performance of this decoder.
588      */
589     public final class BufferedStream
590     {
591         private byte[] buffer;
592         private int count;
593         private int positionByteBuffer;
594         private boolean isNewLine;
595 
596         BufferedStream( int capacity )
597         {
598             this.buffer = new byte[capacity];
599         }
600 
601         public int getPositionByteBuffer()
602         {
603             return positionByteBuffer;
604         }
605 
606         public void setPositionByteBuffer( int positionByteBuffer )
607         {
608             this.positionByteBuffer = positionByteBuffer;
609         }
610 
611         public void write( ByteBuffer bb, int position, int length )
612         {
613             ensureCapacity( length );
614             byte[] array = bb.array();
615             int pos = bb.arrayOffset() + position;
616             while ( length-- > 0 )
617             {
618                 positionByteBuffer++;
619                 byte b = array[pos++];
620                 if ( b == '\r' || b == '\n' )
621                 {
622                     if ( !isNewLine )
623                     {
624                         printExistingLine();
625                         count = 0;
626                     }
627                     isNewLine = true;
628                 }
629                 else
630                 {
631                     buffer[count++] = b;
632                     isNewLine = false;
633                 }
634             }
635         }
636 
637         public void clear()
638         {
639             count = 0;
640         }
641 
642         @Override
643         public String toString()
644         {
645             return new String( buffer, 0, count, DEFAULT_STREAM_ENCODING );
646         }
647 
648         private boolean isEmpty()
649         {
650             return count == 0;
651         }
652 
653         private void ensureCapacity( int addCapacity )
654         {
655             int oldCapacity = buffer.length;
656             int exactCapacity = count + addCapacity;
657             if ( exactCapacity < 0 )
658             {
659                 throw new OutOfMemoryError();
660             }
661 
662             if ( oldCapacity < exactCapacity )
663             {
664                 int newCapacity = oldCapacity << 1;
665                 buffer = copyOf( buffer, max( newCapacity, exactCapacity ) );
666             }
667         }
668 
669         void printExistingLine()
670         {
671             if ( isEmpty() )
672             {
673                 return;
674             }
675 
676             String s = toString();
677             if ( isBlank( s ) )
678             {
679                 return;
680             }
681 
682             if ( s.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
683             {
684                 if ( logger.isDebugEnabled() )
685                 {
686                     logger.debug( s );
687                 }
688                 else if ( logger.isInfoEnabled() )
689                 {
690                     logger.info( s );
691                 }
692                 else
693                 {
694                     // In case of debugging forked JVM, see PRINTABLE_JVM_NATIVE_STREAM.
695                     System.out.println( s );
696                 }
697             }
698             else
699             {
700                 if ( isJvmError( s ) )
701                 {
702                     logger.error( s );
703                 }
704                 else if ( logger.isDebugEnabled() )
705                 {
706                     logger.debug( s );
707                 }
708 
709                 String msg = "Corrupted channel by directly writing to native stream in forked JVM "
710                     + arguments.getForkChannelId() + ".";
711                 File dumpFile = arguments.dumpStreamText( msg + " Stream '" + s + "'." );
712                 String dumpPath = dumpFile.getAbsolutePath();
713                 arguments.logWarningAtEnd( msg + " See FAQ web page and the dump file " + dumpPath );
714             }
715         }
716 
717         private boolean isJvmError( String line )
718         {
719             String lineLower = line.toLowerCase();
720             for ( String errorPattern : JVM_ERROR_PATTERNS )
721             {
722                 if ( lineLower.contains( errorPattern ) )
723                 {
724                     return true;
725                 }
726             }
727             return false;
728         }
729     }
730 
731     /**
732      *
733      */
734     public static final class MalformedFrameException extends Exception
735     {
736         private final int readFrom;
737         private final int readTo;
738 
739         public MalformedFrameException( int readFrom, int readTo )
740         {
741             this.readFrom = readFrom;
742             this.readTo = readTo;
743         }
744 
745         public int readFrom()
746         {
747             return readFrom;
748         }
749 
750         public int readTo()
751         {
752             return readTo;
753         }
754 
755         public boolean hasValidPositions()
756         {
757             return readFrom != NO_POSITION && readTo != NO_POSITION && readTo - readFrom > 0;
758         }
759     }
760 
761     /**
762      * Underflow - could not completely read out al bytes in one call.
763      * <br>
764      * Overflow - read all bytes or more
765      * <br>
766      * EOF - end of stream
767      */
768     public enum StreamReadStatus
769     {
770         UNDERFLOW,
771         OVERFLOW,
772         EOF
773     }
774 }