Mercurial Hosting > luan
view src/org/eclipse/jetty/http/HttpParser.java @ 1065:158d1e6ac17f
fix JBuffer.compact()
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Wed, 09 Nov 2016 04:36:05 -0700 |
parents | 0157e92670f5 |
children | 9d357b9e4bcb |
line wrap: on
line source
// // ======================================================================== // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.http; import java.io.IOException; import org.eclipse.jetty.io.JBuffer; import org.eclipse.jetty.io.BufferUtil; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class HttpParser { private static final Logger LOG = LoggerFactory.getLogger(HttpParser.class); // States private static final int STATE_START = -14; private static final int STATE_FIELD0 = -13; private static final int STATE_SPACE1 = -12; private static final int STATE_STATUS = -11; private static final int STATE_URI = -10; private static final int STATE_SPACE2 = -9; private static final int STATE_FIELD2 = -6; private static final int STATE_HEADER = -5; private static final int STATE_HEADER_NAME = -4; private static final int STATE_HEADER_IN_NAME = -3; private static final int STATE_HEADER_VALUE = -2; private static final int STATE_HEADER_IN_VALUE = -1; private static final int STATE_END = 0; private static final int STATE_EOF_CONTENT = 1; private static final int STATE_CONTENT = 2; private static final int STATE_CHUNKED_CONTENT = 3; private static final int STATE_CHUNK_SIZE = 4; private static final int STATE_CHUNK_PARAMS = 5; private static final int STATE_CHUNK = 6; private static final int STATE_SEEKING_EOF = 7; private final EventHandler _handler; private final EndPoint _endp; public final JBuffer _header; // JBuffer for header data (and small _content) private final JBuffer _body; // JBuffer for large content private JBuffer _buffer; // The current buffer in use (either _header or _content) private int _mark = -1; private String _cached; private String _tok0 = ""; // Saved token: header name, request method or response version private String _tok1 = ""; // Saved token: header value, request URI or response code private String _multiLineValue; private int _responseStatus; // If >0 then we are parsing a response private boolean _persistent; private JBuffer _contentView = BufferUtil.EMPTY_BUFFER; // View of the content in the buffer for {@link Input} private int _state = STATE_START; private byte _eol; private int _length; private long _contentLength; private long _contentPosition; private int _chunkLength; private int _chunkPosition; private boolean _headResponse; public HttpParser(JBuffer headerBuffer,JBuffer bodyBuffer, EndPoint endp, EventHandler handler) { _header = headerBuffer; _body = bodyBuffer; _endp = endp; _handler = handler; } private void mark() { _mark = _buffer.position() - 1; } private String sliceFromMark() { JBuffer buf = _buffer.duplicate(); buf.position(_mark); buf.limit(_buffer.position()-1); _mark = -1; return BufferUtil.getString(buf); } private void clear() { _buffer.limit(0); _mark = -1; } private void compact() { if( _mark == -1 ) { BufferUtil.compact(_buffer); } else if( _mark > 0 ) { int old = _buffer.position(); _buffer.position(_mark); BufferUtil.compact(_buffer); _buffer.position( old - _mark ); _mark = 0; } } private JBuffer getBuffer(int length) { JBuffer dup = _buffer.duplicate(); int end = _buffer.position() + length; dup.limit(end); _buffer.position(end); return dup; } private byte peek() { return _buffer.get(_buffer.position()); } private String bufferToString(int index, int length) { JBuffer dup = _buffer.duplicate(); dup.limit(index+length); dup.position(index); return BufferUtil.getString(dup); } public long getContentLength() { return _contentLength; } public long getContentRead() { return _contentPosition; } /* ------------------------------------------------------------ */ /** Set if a HEAD response is expected * @param head */ public void setHeadResponse(boolean head) { _headResponse = head; } public boolean isChunking() { return _contentLength==HttpTokens.CHUNKED_CONTENT; } public boolean isIdle() { return _state==STATE_START; } public boolean isComplete() { return _state==STATE_END; } public boolean isPersistent() { return _persistent; } public void setPersistent(boolean persistent) { _persistent = persistent; if (!_persistent &&(_state==STATE_END || _state==STATE_START)) _state = STATE_SEEKING_EOF; } /* ------------------------------------------------------------------------------- */ /** * Parse until END state. * This method will parse any remaining content in the current buffer as long as there is * no unconsumed content. It does not care about the {@link #getState current state} of the parser. * @see #parse * @see #parseNext */ public boolean parseAvailable() throws IOException { boolean progress = parseNext() > 0; // continue parsing while (!isComplete() && _buffer!=null && _buffer.remaining()>0 && !_contentView.hasRemaining()) { progress |= parseNext()>0; } return progress; } /* ------------------------------------------------------------------------------- */ /** * Parse until next Event. * @return an indication of progress <0 EOF, 0 no progress, >0 progress. */ private int parseNext() throws IOException { try { int progress = 0; if (_state == STATE_END) { return 0; } if (_buffer==null) _buffer = _header; if (_state == STATE_CONTENT && _contentPosition == _contentLength) { _state = STATE_END; _handler.messageComplete(_contentPosition); return 1; } int length = _buffer.remaining(); // Fill buffer if we can if (length == 0) { int filled = -1; IOException ex = null; try { filled = fill(); LOG.debug("filled {}/{}",filled,_buffer.remaining()); } catch(IOException e) { LOG.debug(this.toString(),e); ex=e; } if (filled > 0 ) progress++; else if (filled < 0 ) { _persistent = false; // do we have content to deliver? if (_state>STATE_END) { if (_buffer.remaining()>0 && !_headResponse) { JBuffer chunk = getBuffer(_buffer.remaining()); _contentPosition += chunk.remaining(); _contentView = chunk; _handler.content(); // May recurse here } } // was this unexpected? switch(_state) { case STATE_END: case STATE_SEEKING_EOF: _state = STATE_END; break; case STATE_EOF_CONTENT: _state = STATE_END; _handler.messageComplete(_contentPosition); break; default: _state = STATE_END; if (!_headResponse) _handler.earlyEOF(); _handler.messageComplete(_contentPosition); } if (ex!=null) throw ex; if (!isComplete() && !isIdle()) throw new EofException(); return -1; } length = _buffer.remaining(); } // Handle header states byte ch; byte[] array = _buffer.hasArray() ? _buffer.array() : null; int last = _state; while (_state<STATE_END && length-->0) { if (last!=_state) { progress++; last = _state; } ch = _buffer.get(); if (_eol == HttpTokens.CARRIAGE_RETURN) { if (ch == HttpTokens.LINE_FEED) { _eol=HttpTokens.LINE_FEED; continue; } throw new HttpException(HttpStatus.BAD_REQUEST_400); } _eol=0; switch (_state) { case STATE_START: _contentLength = HttpTokens.UNKNOWN_CONTENT; _cached = null; if (ch > HttpTokens.SPACE || ch<0) { mark(); _state = STATE_FIELD0; } break; case STATE_FIELD0: if (ch == HttpTokens.SPACE) { _tok0 = bufferToString(_mark, _buffer.position() - 1 - _mark); _responseStatus = !HttpVersions.CACHE.contains(_tok0)?-1:0; _state=STATE_SPACE1; continue; } else if (ch < HttpTokens.SPACE && ch>=0) { throw new HttpException(HttpStatus.BAD_REQUEST_400); } break; case STATE_SPACE1: if (ch > HttpTokens.SPACE || ch<0) { mark(); if (_responseStatus>=0) { _state = STATE_STATUS; _responseStatus=ch-'0'; } else _state=STATE_URI; } else if (ch < HttpTokens.SPACE) { throw new HttpException(HttpStatus.BAD_REQUEST_400); } break; case STATE_STATUS: if (ch == HttpTokens.SPACE) { _tok1 = bufferToString(_mark, _buffer.position() - 1 - _mark); _state = STATE_SPACE2; continue; } else if (ch>='0' && ch<='9') { _responseStatus=_responseStatus*10+(ch-'0'); continue; } else if (ch < HttpTokens.SPACE && ch>=0) { _eol=ch; _state = STATE_HEADER; _tok0 = ""; _tok1 = ""; _multiLineValue=null; continue; } // not a digit, so must be a URI _state=STATE_URI; _responseStatus=-1; break; case STATE_URI: if (ch == HttpTokens.SPACE) { _tok1 = bufferToString(_mark, _buffer.position() - 1 - _mark); _state=STATE_SPACE2; continue; } else if (ch < HttpTokens.SPACE && ch>=0) { // HTTP/0.9 _handler.startRequest(_tok0, sliceFromMark(), null); _persistent = false; _state = STATE_SEEKING_EOF; _handler.headerComplete(); _handler.messageComplete(_contentPosition); return 1; } break; case STATE_SPACE2: if (ch > HttpTokens.SPACE || ch<0) { mark(); _state=STATE_FIELD2; } else if (ch < HttpTokens.SPACE) { if (_responseStatus>0) { _eol=ch; _state=STATE_HEADER; _tok0 = ""; _tok1 = ""; _multiLineValue=null; } else { // HTTP/0.9 _handler.startRequest(_tok0, _tok1, null); _persistent = false; _state = STATE_SEEKING_EOF; _handler.headerComplete(); _handler.messageComplete(_contentPosition); return 1; } } break; case STATE_FIELD2: if (ch == HttpTokens.CARRIAGE_RETURN || ch == HttpTokens.LINE_FEED) { String version; if (_responseStatus > 0) // _handler.startResponse(version=HttpVersions.CACHE.lookup(_tok0), _responseStatus,sliceFromMark()); version = _tok0; else _handler.startRequest(_tok0, _tok1, version=sliceFromMark()); _eol=ch; _persistent = HttpVersions.CACHE.getOrdinal(version) >= HttpVersions.HTTP_1_1_ORDINAL; _state=STATE_HEADER; _tok0 = ""; _tok1 = ""; _multiLineValue=null; continue; } break; case STATE_HEADER: switch(ch) { case HttpTokens.COLON: case HttpTokens.SPACE: case HttpTokens.TAB: { // header value without name - continuation? _length=-1; _state=STATE_HEADER_VALUE; break; } default: { // handler last header if any if (_cached!=null || _tok0.length() > 0 || _tok1.length() > 0 || _multiLineValue != null) { String header = _cached!=null ? _cached : _tok0; _cached = null; String value = _multiLineValue == null ? _tok1 : _multiLineValue; int ho = HttpHeaders.CACHE.getOrdinal(header); if (ho >= 0) { int vo; switch (ho) { case HttpHeaders.CONTENT_LENGTH_ORDINAL: if (_contentLength != HttpTokens.CHUNKED_CONTENT ) { try { _contentLength = BufferUtil.toLong(value); } catch(NumberFormatException e) { LOG.trace("",e); throw new HttpException(HttpStatus.BAD_REQUEST_400); } if (_contentLength <= 0) _contentLength=HttpTokens.NO_CONTENT; } break; case HttpHeaders.TRANSFER_ENCODING_ORDINAL: // value=HttpHeaderValues.CACHE.lookup(value); vo = HttpHeaderValues.CACHE.getOrdinal(value); if (HttpHeaderValues.CHUNKED_ORDINAL == vo) _contentLength = HttpTokens.CHUNKED_CONTENT; else { if (value.endsWith(HttpHeaderValues.CHUNKED)) _contentLength = HttpTokens.CHUNKED_CONTENT; else if (value.indexOf(HttpHeaderValues.CHUNKED) >= 0) throw new HttpException(400,null); } break; case HttpHeaders.CONNECTION_ORDINAL: switch(HttpHeaderValues.CACHE.getOrdinal(value)) { case HttpHeaderValues.CLOSE_ORDINAL: _persistent = false; break; case HttpHeaderValues.KEEP_ALIVE_ORDINAL: _persistent = true; break; case -1: // No match, may be multi valued { for (String v : value.split(",")) { switch(HttpHeaderValues.CACHE.getOrdinal(v.trim())) { case HttpHeaderValues.CLOSE_ORDINAL: _persistent = false; break; case HttpHeaderValues.KEEP_ALIVE_ORDINAL: _persistent = true; break; } } break; } } } } _handler.parsedHeader(header, value); _tok0 = ""; _tok1 = ""; _multiLineValue=null; } _mark = -1; // now handle ch if (ch == HttpTokens.CARRIAGE_RETURN || ch == HttpTokens.LINE_FEED) { // is it a response that cannot have a body? if (_responseStatus > 0 && // response (_responseStatus == 304 || // not-modified response _responseStatus == 204 || // no-content response _responseStatus < 200)) // 1xx response _contentLength=HttpTokens.NO_CONTENT; // ignore any other headers set // else if we don't know framing else if (_contentLength == HttpTokens.UNKNOWN_CONTENT) { if (_responseStatus == 0 // request || _responseStatus == 304 // not-modified response || _responseStatus == 204 // no-content response || _responseStatus < 200) // 1xx response _contentLength=HttpTokens.NO_CONTENT; else _contentLength=HttpTokens.EOF_CONTENT; } _contentPosition=0; _eol=ch; if (_eol==HttpTokens.CARRIAGE_RETURN && _buffer.hasRemaining() && peek()==HttpTokens.LINE_FEED) _eol=_buffer.get(); // We convert _contentLength to an int for this switch statement because // we don't care about the amount of data available just whether there is some. switch (_contentLength > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) _contentLength) { case HttpTokens.EOF_CONTENT: _state=STATE_EOF_CONTENT; _handler.headerComplete(); // May recurse here ! break; case HttpTokens.CHUNKED_CONTENT: _state=STATE_CHUNKED_CONTENT; _handler.headerComplete(); // May recurse here ! break; case HttpTokens.NO_CONTENT: _handler.headerComplete(); _state = _persistent||(_responseStatus>=100&&_responseStatus<200)?STATE_END:STATE_SEEKING_EOF; _handler.messageComplete(_contentPosition); return 1; default: _state=STATE_CONTENT; _handler.headerComplete(); // May recurse here ! break; } return 1; } else { // New header _length = 1; mark(); _state = STATE_HEADER_NAME; // try cached name! if (array!=null) { String s = new String(array, _mark, length+1); _cached = HttpHeaders.CACHE.getBest(s); if (_cached!=null) { _length = _cached.length(); _buffer.position(_mark+_length); length = _buffer.remaining(); } } } } } break; case STATE_HEADER_NAME: switch(ch) { case HttpTokens.CARRIAGE_RETURN: case HttpTokens.LINE_FEED: if (_length > 0) { _tok0 = bufferToString(_mark, _length); } _eol=ch; _state=STATE_HEADER; break; case HttpTokens.COLON: if (_length > 0 && _cached==null) { _tok0 = bufferToString(_mark, _length); } _length=-1; _state=STATE_HEADER_VALUE; break; case HttpTokens.SPACE: case HttpTokens.TAB: break; default: { _cached = null; if (_length == -1) mark(); _length = _buffer.position() - _mark; _state = STATE_HEADER_IN_NAME; } } break; case STATE_HEADER_IN_NAME: switch(ch) { case HttpTokens.CARRIAGE_RETURN: case HttpTokens.LINE_FEED: if (_length > 0) { _tok0 = bufferToString(_mark,_length); } _eol=ch; _state=STATE_HEADER; break; case HttpTokens.COLON: if (_length > 0 && _cached==null) { _tok0 = bufferToString(_mark,_length); } _length=-1; _state=STATE_HEADER_VALUE; break; case HttpTokens.SPACE: case HttpTokens.TAB: _state=STATE_HEADER_NAME; break; default: { _cached = null; _length++; } } break; case STATE_HEADER_VALUE: switch(ch) { case HttpTokens.CARRIAGE_RETURN: case HttpTokens.LINE_FEED: if (_length > 0) { if (_tok1.length() == 0) // _tok1.update(_mark, _mark + _length); _tok1 = bufferToString(_mark, _length); else { // Continuation line! if (_multiLineValue == null) _multiLineValue = _tok1; // _tok1.update(_mark, _mark + _length); _tok1 = bufferToString(_mark, _length); _multiLineValue += " " + _tok1; } } _eol=ch; _state=STATE_HEADER; break; case HttpTokens.SPACE: case HttpTokens.TAB: break; default: { if (_length == -1) mark(); _length = _buffer.position() - _mark; _state = STATE_HEADER_IN_VALUE; } } break; case STATE_HEADER_IN_VALUE: switch(ch) { case HttpTokens.CARRIAGE_RETURN: case HttpTokens.LINE_FEED: if (_length > 0) { if (_tok1.length() == 0) // _tok1.update(_mark, _mark + _length); _tok1 = bufferToString(_mark, _length); else { // Continuation line! if (_multiLineValue == null) _multiLineValue = _tok1; // _tok1.update(_mark, _mark + _length); _tok1 = bufferToString(_mark, _length); _multiLineValue += " " + _tok1; } } _eol=ch; _state=STATE_HEADER; break; case HttpTokens.SPACE: case HttpTokens.TAB: _state=STATE_HEADER_VALUE; break; default: _length++; } break; } } // end of HEADER states loop // ========================== // Handle HEAD response if (_responseStatus>0 && _headResponse) { _state = _persistent||(_responseStatus>=100&&_responseStatus<200)?STATE_END:STATE_SEEKING_EOF; _handler.messageComplete(_contentLength); } // ========================== // Handle _content length=_buffer.remaining(); last=_state; while (_state > STATE_END && length > 0) { if (last!=_state) { progress++; last=_state; } if (_eol == HttpTokens.CARRIAGE_RETURN && peek() == HttpTokens.LINE_FEED) { _eol=_buffer.get(); length=_buffer.remaining(); continue; } _eol=0; switch (_state) { case STATE_EOF_CONTENT: { JBuffer chunk = getBuffer(_buffer.remaining()); _contentPosition += chunk.remaining(); _contentView = chunk; _handler.content(); // May recurse here // TODO adjust the _buffer to keep unconsumed content return 1; } case STATE_CONTENT: { long remaining = _contentLength - _contentPosition; if (remaining == 0) { _state = _persistent?STATE_END:STATE_SEEKING_EOF; _handler.messageComplete(_contentPosition); return 1; } if (length > remaining) { // We can cast reamining to an int as we know that it is smaller than // or equal to length which is already an int. length=(int)remaining; } JBuffer chunk = getBuffer(length); _contentPosition += chunk.remaining(); _contentView = chunk; _handler.content(); // May recurse here if(_contentPosition == _contentLength) { _state = _persistent?STATE_END:STATE_SEEKING_EOF; _handler.messageComplete(_contentPosition); } // TODO adjust the _buffer to keep unconsumed content return 1; } case STATE_CHUNKED_CONTENT: { ch=peek(); if (ch == HttpTokens.CARRIAGE_RETURN || ch == HttpTokens.LINE_FEED) _eol=_buffer.get(); else if (ch <= HttpTokens.SPACE) _buffer.get(); else { _chunkLength=0; _chunkPosition=0; _state = STATE_CHUNK_SIZE; } break; } case STATE_CHUNK_SIZE: { ch=_buffer.get(); if (ch == HttpTokens.CARRIAGE_RETURN || ch == HttpTokens.LINE_FEED) { _eol=ch; if (_chunkLength == 0) { if (_eol==HttpTokens.CARRIAGE_RETURN && _buffer.hasRemaining() && peek()==HttpTokens.LINE_FEED) _eol=_buffer.get(); _state = _persistent?STATE_END:STATE_SEEKING_EOF; _handler.messageComplete(_contentPosition); return 1; } else _state = STATE_CHUNK; } else if (ch <= HttpTokens.SPACE || ch == HttpTokens.SEMI_COLON) _state=STATE_CHUNK_PARAMS; else if (ch >= '0' && ch <= '9') _chunkLength=_chunkLength * 16 + (ch - '0'); else if (ch >= 'a' && ch <= 'f') _chunkLength=_chunkLength * 16 + (10 + ch - 'a'); else if (ch >= 'A' && ch <= 'F') _chunkLength=_chunkLength * 16 + (10 + ch - 'A'); else throw new IOException("bad chunk char: " + ch); break; } case STATE_CHUNK_PARAMS: { ch=_buffer.get(); if (ch == HttpTokens.CARRIAGE_RETURN || ch == HttpTokens.LINE_FEED) { _eol=ch; if (_chunkLength == 0) { if (_eol==HttpTokens.CARRIAGE_RETURN && _buffer.hasRemaining() && peek()==HttpTokens.LINE_FEED) _eol=_buffer.get(); _state = _persistent?STATE_END:STATE_SEEKING_EOF; _handler.messageComplete(_contentPosition); return 1; } else _state=STATE_CHUNK; } break; } case STATE_CHUNK: { int remaining=_chunkLength - _chunkPosition; if (remaining == 0) { _state=STATE_CHUNKED_CONTENT; break; } else if (length > remaining) length=remaining; JBuffer chunk = getBuffer(length); _contentPosition += chunk.remaining(); _chunkPosition += chunk.remaining(); _contentView = chunk; _handler.content(); // May recurse here // TODO adjust the _buffer to keep unconsumed content return 1; } case STATE_SEEKING_EOF: { // Close if there is more data than CRLF if (_buffer.remaining()>2) { _state = STATE_END; _endp.close(); } else { // or if the data is not white space while (_buffer.remaining()>0) if (!Character.isWhitespace(_buffer.get())) { _state = STATE_END; _endp.close(); clear(); } } clear(); break; } } length = _buffer.remaining(); } return progress; } catch(HttpException e) { _persistent = false; _state = STATE_SEEKING_EOF; throw e; } } /* ------------------------------------------------------------------------------- */ /** fill the buffers from the endpoint * */ private int fill() throws IOException { // Do we have a buffer? if (_buffer==null) _buffer = _header; // Is there unconsumed content in body buffer if (_state>STATE_END && _buffer==_header && !_header.hasRemaining() && _body.hasRemaining()) { _buffer = _body; return _buffer.remaining(); } // Shall we switch to a body buffer? if (_buffer==_header && _state>STATE_END && _header.remaining()==0 && ((_contentLength-_contentPosition)>_header.capacity())) { _buffer = _body; } // Shall we compact the body? if (_buffer==_body || _state>STATE_END) { compact(); } // Are we full? if (_buffer.space() == 0) { LOG.warn("HttpParser Full for {} ",_endp); clear(); throw new HttpException(HttpStatus.REQUEST_ENTITY_TOO_LARGE_413, "Request Entity Too Large: "+(_buffer==_body?"body":"head")); } /* why? try { int filled = _endp.fill(_buffer); return filled; } catch(IOException e) { LOG.debug("",e); throw (e instanceof EofException) ? e:new EofException(e); } */ return _endp.fill(_buffer); } @Override public String toString() { return String.format("%s{s=%d,l=%d,c=%d}", getClass().getSimpleName(), _state, _length, _contentLength); } public JBuffer blockForContent(long maxIdleTime) throws IOException { if (_contentView.remaining()>0) return _contentView; if (_state <= STATE_END || _state==STATE_SEEKING_EOF) return null; try { parseNext(); // parse until some progress is made (or IOException thrown for timeout) while(_contentView.remaining() == 0 && !(_state==STATE_END||_state==STATE_SEEKING_EOF) && _endp.isOpen()) { if (!_endp.isBlocking()) { if (parseNext()>0) continue; if (!_endp.blockReadable(maxIdleTime)) { _endp.close(); throw new EofException("timeout"); } } parseNext(); } } catch(IOException e) { // TODO is this needed? _endp.close(); throw e; } return _contentView.remaining()>0 ? _contentView : null; } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see java.io.InputStream#available() */ public int available() throws IOException { if (_contentView.remaining()>0) return _contentView.remaining(); if (_endp.isBlocking()) { return 0; } parseNext(); return _contentView.remaining(); } public interface EventHandler { public abstract void content() throws IOException; public void headerComplete() throws IOException; public void messageComplete(long contentLength) throws IOException; /** * This is the method called by parser when a HTTP Header name and value is found */ public void parsedHeader(String name, String value) throws IOException; /** * This is the method called by parser when the HTTP request line is parsed */ public abstract void startRequest(String method, String url, String version) throws IOException; public void earlyEOF(); } }