# BaseParser.java

    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package org.apache.pdfbox.pdfparser;

    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.charset.CharacterCodingException;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    import java.nio.charset.StandardCharsets;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.pdfbox.cos.COSArray;
    import org.apache.pdfbox.cos.COSBase;
    import org.apache.pdfbox.cos.COSBoolean;
    import org.apache.pdfbox.cos.COSDictionary;
    import org.apache.pdfbox.cos.COSDocument;
    import org.apache.pdfbox.cos.COSInteger;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.cos.COSNull;
    import org.apache.pdfbox.cos.COSNumber;
    import org.apache.pdfbox.cos.COSObject;
    import org.apache.pdfbox.cos.COSObjectKey;
    import org.apache.pdfbox.cos.COSString;
    import org.apache.pdfbox.io.RandomAccessRead;

    /**
     * This class is used to contain parsing logic that will be used by both the
     * PDFParser and the COSStreamParser.
     *
     * @author Ben Litchfield
     */
    public abstract class BaseParser
    {
        private static final long OBJECT_NUMBER_THRESHOLD = 10000000000L;

        private static final long GENERATION_NUMBER_THRESHOLD = 65535;

        static final int MAX_LENGTH_LONG = Long.toString(Long.MAX_VALUE).length();

        private final CharsetDecoder utf8Decoder = StandardCharsets.UTF_8.newDecoder();

        /**
         * Log instance.
         */
        private static final Log LOG = LogFactory.getLog(BaseParser.class);

        protected static final int E = 'e';
        protected static final int N = 'n';
        protected static final int D = 'd';

        protected static final int S = 's';
        protected static final int T = 't';
        protected static final int R = 'r';
        protected static final int A = 'a';
        protected static final int M = 'm';

        protected static final int O = 'o';
        protected static final int B = 'b';
        protected static final int J = 'j';

        /**
         * This is a string constant that will be used for comparisons.
         */
        public static final String DEF = "def";
        /**
         * This is a string constant that will be used for comparisons.
         */
        protected static final String ENDOBJ_STRING = "endobj";
        /**
         * This is a string constant that will be used for comparisons.
         */
        protected static final String ENDSTREAM_STRING = "endstream";
        /**
         * This is a string constant that will be used for comparisons.
         */
        protected static final String STREAM_STRING = "stream";
        /**
         * This is a string constant that will be used for comparisons.
         */
        private static final char[] TRUE = new char[] { 't', 'r', 'u', 'e' };
        /**
         * This is a string constant that will be used for comparisons.
         */
        private static final char[] FALSE = new char[] { 'f', 'a', 'l', 's', 'e' };
        /**
         * This is a string constant that will be used for comparisons.
         */
        private static final char[] NULL = new char[] { 'n', 'u', 'l', 'l' };

        /**
         * ASCII code for line feed.
         */
        protected static final byte ASCII_LF = 10;
        /**
         * ASCII code for carriage return.
         */
        protected static final byte ASCII_CR = 13;
        private static final byte ASCII_ZERO = 48;
        private static final byte ASCII_NINE = 57;
        private static final byte ASCII_SPACE = 32;

        /**
         * This is the stream that will be read from.
         */
        protected final RandomAccessRead source;

        /**
         * This is the document that will be parsed.
         */
        protected COSDocument document;

        /**
         * Default constructor.
         */
        BaseParser(RandomAccessRead pdfSource)
        {
            this.source = pdfSource;
        }

        private static boolean isHexDigit(char ch)
        {
            return isDigit(ch) ||
            (ch >= 'a' && ch <= 'f') ||
            (ch >= 'A' && ch <= 'F');
        }

        /**
         * This will parse a PDF dictionary value.
         *
         * @return The parsed Dictionary object.
         *
         * @throws IOException If there is an error parsing the dictionary object.
         */
        private COSBase parseCOSDictionaryValue() throws IOException
        {
            long numOffset = source.getPosition();
            COSBase value = parseDirObject();
            skipSpaces();
            // proceed if the given object is a number and the following is a number as well
            if (!(value instanceof COSNumber) || !isDigit())
            {
                return value;
            }
            // read the remaining information of the object number
            long genOffset = source.getPosition();
            COSBase generationNumber = parseDirObject();
            skipSpaces();
            readExpectedChar('R');
            if (!(value instanceof COSInteger))
            {
                LOG.error("expected number, actual=" + value + " at offset " + numOffset);
                return COSNull.NULL;
            }
            if (!(generationNumber instanceof COSInteger))
            {
                LOG.error("expected number, actual=" + generationNumber + " at offset " + genOffset);
                return COSNull.NULL;
            }
            long objNumber = ((COSInteger) value).longValue();
            if (objNumber <= 0)
            {
                LOG.error("invalid object number value =" + objNumber + " at offset " + numOffset);
                return COSNull.NULL;
            }
            int genNumber = ((COSInteger) generationNumber).intValue();
            if (genNumber < 0)
            {
                LOG.error("invalid generation number value =" + genNumber + " at offset " + numOffset);
                return COSNull.NULL;
            }
            // dereference the object
            return getObjectFromPool(new COSObjectKey(objNumber, genNumber));
        }

        private COSBase getObjectFromPool(COSObjectKey key) throws IOException
        {
            if (document == null)
            {
                throw new IOException("object reference " + key + " at offset " + source
                        .getPosition()
                        + " in content stream");
            }
            return document.getObjectFromPool(key);
        }

        /**
         * This will parse a PDF dictionary.
         *
         * @param isDirect indicates whether the dictionary to be read is a direct object
         * @return The parsed dictionary, never null.
         *
         * @throws IOException If there is an error reading the stream.
         */
        protected COSDictionary parseCOSDictionary(boolean isDirect) throws IOException
        {
            readExpectedChar('<');
            readExpectedChar('<');
            skipSpaces();
            COSDictionary obj = new COSDictionary();
            obj.setDirect(isDirect);
            boolean done = false;
            while (!done)
            {
                skipSpaces();
                char c = (char) source.peek();
                if (c == '>')
                {
                    done = true;
                }
                else if (c == '/')
                {
                    // something went wrong, most likely the dictionary is corrupted
                    // stop immediately and return everything read so far
                    if (!parseCOSDictionaryNameValuePair(obj))
                    {
                        return obj;
                    }
                }
                else
                {
                    // invalid dictionary, we were expecting a /Name, read until the end or until we can recover
                    LOG.warn("Invalid dictionary, found: '" + c + "' but expected: '/' at offset "
                            + source.getPosition());
                    if (readUntilEndOfCOSDictionary())
                    {
                        // we couldn't recover
                        return obj;
                    }
                }
            }
            readExpectedChar('>');
            readExpectedChar('>');
            return obj;
        }

        /**
         * Keep reading until the end of the dictionary object or the file has been hit, or until a '/'
         * has been found.
         *
         * @return true if the end of the object or the file has been found, false if not, i.e. that the
         * caller can continue to parse the dictionary at the current position.
         *
         * @throws IOException if there is a reading error.
         */
        private boolean readUntilEndOfCOSDictionary() throws IOException
        {
            int c = source.read();
            while (c != -1 && c != '/' && c != '>')
            {
                // in addition to stopping when we find / or >, we also want
                // to stop when we find endstream or endobj.
                if (c == E)
                {
                    c = source.read();
                    if (c == N)
                    {
                        c = source.read();
                        if (c == D)
                        {
                            c = source.read();
                            boolean isStream = c == S && source.read() == T && source.read() == R
                                    && source.read() == E && source.read() == A && source.read() == M;
                            boolean isObj = !isStream && c == O && source.read() == B
                                    && source.read() == J;
                            if (isStream || isObj)
                            {
                                // we're done reading this object!
                                return true;
                            }
                        }
                    }
                }
                c = source.read();
            }
            if (c == -1)
            {
                return true;
            }
            source.rewind(1);
            return false;
        }

        private boolean parseCOSDictionaryNameValuePair(COSDictionary obj) throws IOException
        {
            COSName key = parseCOSName();
            if (key == null || key.getName().isEmpty())
            {
                LOG.warn("Empty COSName at offset " + source.getPosition());
            }
            COSBase value = parseCOSDictionaryValue();
            skipSpaces();
            if (value == null)
            {
                LOG.warn("Bad dictionary declaration at offset " + source.getPosition());
                return false;
            }
            else if (value instanceof COSInteger && !((COSInteger) value).isValid())
            {
                LOG.warn("Skipped out of range number value at offset " + source.getPosition());
            }
            else
            {
                // label this item as direct, to avoid signature problems.
                value.setDirect(true);
                obj.setItem(key, value);
            }
            return true;
        }

        protected void skipWhiteSpaces() throws IOException
        {
            //PDF Ref 3.2.7 A stream must be followed by either
            //a CRLF or LF but nothing else.

            int whitespace = source.read();

            //see brother_scan_cover.pdf, it adds whitespaces
            //after the stream but before the start of the
            //data, so just read those first
            while (ASCII_SPACE == whitespace)
            {
                whitespace = source.read();
            }

            if (ASCII_CR == whitespace)
            {
                whitespace = source.read();
                if (ASCII_LF != whitespace)
                {
                    source.rewind(1);
                    //The spec says this is invalid but it happens in the real
                    //world so we must support it.
                }
            }
            else if (ASCII_LF != whitespace)
            {
                //we are in an error.
                //but again we will do a lenient parsing and just assume that everything
                //is fine
                source.rewind(1);
            }
        }

        /**
         * This is really a bug in the Document creators code, but it caused a crash in PDFBox, the first bug was in this
         * format: /Title ( (5) /Creator which was patched in 1 place.
         *
         * However it missed the case where the number of opening and closing parenthesis isn't balanced
         *
         * The second bug was in this format /Title (c:\) /Producer
         *
         * This patch moves this code out of the parseCOSString method, so it can be used twice.
         *
         * @param bracesParameter the number of braces currently open.
         *
         * @return the corrected value of the brace counter
         * @throws IOException
         */
        private int checkForEndOfString(final int bracesParameter) throws IOException
        {
            int braces = bracesParameter;
            byte[] nextThreeBytes = new byte[3];
            int amountRead = source.read(nextThreeBytes);

            // Check the next 3 bytes if available
            // The following cases are valid indicators for the end of the string
            // 1. Next line contains another COSObject: CR + LF + '/'
            // 2. COSDictionary ends in the next line: CR + LF + '>'
            // 3. Next line contains another COSObject: CR + '/'
            // 4. COSDictionary ends in the next line: CR + '>'
            if (amountRead == 3 && nextThreeBytes[0] == ASCII_CR)
            {
                if ( (nextThreeBytes[1] == ASCII_LF && (nextThreeBytes[2] == '/') || nextThreeBytes[2] == '>')
                        || nextThreeBytes[1] == '/' || nextThreeBytes[1] == '>')
                {
                    braces = 0;
                }
            }
            if (amountRead > 0)
            {
                source.rewind(amountRead);
            }
            return braces;
        }

        /**
         * This will parse a PDF string.
         *
         * @return The parsed PDF string.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected COSString parseCOSString() throws IOException
        {
            char nextChar = (char) source.read();
            if (nextChar == '<')
            {
                return parseCOSHexString();
            }
            else if (nextChar != '(')
            {
                throw new IOException( "parseCOSString string should start with '(' or '<' and not '" +
                        nextChar + "' at offset " + source.getPosition());
            }

            ByteArrayOutputStream out = new ByteArrayOutputStream();

            // This is the number of braces read
            int braces = 1;
            int c = source.read();
            while( braces > 0 && c != -1)
            {
                char ch = (char)c;
                int nextc = -2; // not yet read

                if (ch == ')')
                {

                    braces--;
                    braces = checkForEndOfString(braces);
                    if( braces != 0 )
                    {
                        out.write(ch);
                    }
                }
                else if (ch == '(')
                {
                    braces++;
                    out.write(ch);
                }
                else if( ch == '\\' )
                {
                    //patched by ram
                    char next = (char) source.read();
                    switch(next)
                    {
                        case 'n':
                            out.write('\n');
                            break;
                        case 'r':
                            out.write('\r');
                            break;
                        case 't':
                            out.write('\t');
                            break;
                        case 'b':
                            out.write('\b');
                            break;
                        case 'f':
                            out.write('\f');
                            break;
                        case ')':
                            // PDFBox 276 /Title (c:\)
                        braces = checkForEndOfString(braces);
                            if( braces != 0 )
                            {
                                out.write(next);
                            }
                            else
                            {
                                out.write('\\');
                            }
                            break;
                        case '(':
                        case '\\':
                            out.write(next);
                            break;
                        case ASCII_LF:
                        case ASCII_CR:
                            //this is a break in the line so ignore it and the newline and continue
                            c = source.read();
                            while( isEOL(c) && c != -1)
                            {
                                c = source.read();
                            }
                            nextc = c;
                            break;
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                            StringBuilder octal = new StringBuilder();
                            octal.append( next );
                            c = source.read();
                            char digit = (char)c;
                            if( digit >= '0' && digit <= '7' )
                            {
                                octal.append( digit );
                                c = source.read();
                                digit = (char)c;
                                if( digit >= '0' && digit <= '7' )
                                {
                                    octal.append( digit );
                                }
                                else
                                {
                                    nextc = c;
                                }
                            }
                            else
                            {
                                nextc = c;
                            }

                            int character = 0;
                            try
                            {
                                character = Integer.parseInt( octal.toString(), 8 );
                            }
                            catch( NumberFormatException e )
                            {
                                throw new IOException( "Error: Expected octal character, actual='" + octal + "'", e );
                            }
                            out.write(character);
                            break;
                        default:
                            // dropping the backslash
                            // see 7.3.4.2 Literal Strings for further information
                            out.write(next);
                    }
                }
                else
                {
                    out.write(ch);
                }
                if (nextc != -2)
                {
                    c = nextc;
                }
                else
                {
                    c = source.read();
                }
            }
            if (c != -1)
            {
                source.rewind(1);
            }
            return new COSString(out.toByteArray());
        }

        /**
         * This will parse a PDF HEX string with fail fast semantic
         * meaning that we stop if a not allowed character is found.
         * This is necessary in order to detect malformed input and
         * be able to skip to next object start.
         *
         * We assume starting '&lt;' was already read.
         *
         * @return The parsed PDF string.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        private COSString parseCOSHexString() throws IOException
        {
            final StringBuilder sBuf = new StringBuilder();
            while( true )
            {
                int c = source.read();
                if ( isHexDigit((char)c) )
                {
                    sBuf.append( (char) c );
                }
                else if ( c == '>' )
                {
                    break;
                }
                else if ( c < 0 )
                {
                    throw new IOException( "Missing closing bracket for hex string. Reached EOS." );
                }
                else if ( ( c == ' ' ) || ( c == '\n' ) ||
                        ( c == '\t' ) || ( c == '\r' ) ||
                        ( c == '\b' ) || ( c == '\f' ) )
                {
                    continue;
                }
                else
                {
                    // if invalid chars was found: discard last
                    // hex character if it is not part of a pair
                    if (sBuf.length()%2!=0)
                    {
                        sBuf.deleteCharAt(sBuf.length()-1);
                    }

                    // read till the closing bracket was found
                    do
                    {
                        c = source.read();
                    }
                    while ( c != '>' && c >= 0 );

                    // might have reached EOF while looking for the closing bracket
                    // this can happen for malformed PDFs only. Make sure that there is
                    // no endless loop.
                    if ( c < 0 )
                    {
                        throw new IOException( "Missing closing bracket for hex string. Reached EOS." );
                    }

                    // exit loop
                    break;
                }
            }
            return COSString.parseHex(sBuf.toString());
        }

        /**
         * This will parse a PDF array object.
         *
         * @return The parsed PDF array.
         *
         * @throws IOException If there is an error parsing the stream.
         */
        protected COSArray parseCOSArray() throws IOException
        {
            long startPosition = source.getPosition();
            readExpectedChar('[');
            COSArray po = new COSArray();
            COSBase pbo;
            skipSpaces();
            int i;
            while (((i = source.peek()) > 0) && ((char) i != ']'))
            {
                pbo = parseDirObject();
                if( pbo instanceof COSObject )
                {
                    // We have to check if the expected values are there or not PDFBOX-385
                    if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger)
                    {
                        COSInteger genNumber = (COSInteger)po.remove( po.size() -1 );
                        if (po.size() > 0 && po.get(po.size() - 1) instanceof COSInteger)
                        {
                            COSInteger number = (COSInteger)po.remove( po.size() -1 );
                            COSObjectKey key = new COSObjectKey(number.longValue(), genNumber.intValue());
                            pbo = getObjectFromPool(key);
                        }
                        else
                        {
                            // the object reference is somehow wrong
                            pbo = null;
                        }
                    }
                    else
                    {
                        pbo = null;
                    }
                }
                if( pbo != null )
                {
                    po.add( pbo );
                }
                else
                {
                    //it could be a bad object in the array which is just skipped
                    LOG.warn("Corrupt array element at offset " + source.getPosition()
                            + ", start offset: " + startPosition);
                    String isThisTheEnd = readString();
                    // return immediately if a corrupt element is followed by another array
                    // to avoid a possible infinite recursion as most likely the whole array is corrupted
                    if (isThisTheEnd.isEmpty() && source.peek() == '[')
                    {
                        return po;
                    }
                    source.rewind(isThisTheEnd.getBytes(StandardCharsets.ISO_8859_1).length);
                    // This could also be an "endobj" or "endstream" which means we can assume that
                    // the array has ended.
                    if(ENDOBJ_STRING.equals(isThisTheEnd) || ENDSTREAM_STRING.equals(isThisTheEnd))
                    {
                        return po;
                    }
                }
                skipSpaces();
            }
            // read ']'
            source.read();
            skipSpaces();
            return po;
        }

        /**
         * Determine if a character terminates a PDF name.
         *
         * @param ch The character
         * @return true if the character terminates a PDF name, otherwise false.
         */
        protected boolean isEndOfName(int ch)
        {
            return ch == ASCII_SPACE || ch == ASCII_CR || ch == ASCII_LF || ch == 9 || ch == '>' ||
                   ch == '<' || ch == '[' || ch =='/' || ch ==']' || ch ==')' || ch =='(' ||
                   ch == 0 || ch == '\f' || ch == '%';
        }

        /**
         * This will parse a PDF name from the stream.
         *
         * @return The parsed PDF name.
         * @throws IOException If there is an error reading from the stream.
         */
        protected COSName parseCOSName() throws IOException
        {
            readExpectedChar('/');
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            int c = source.read();
            while (c != -1)
            {
                int ch = c;
                if (ch == '#')
                {
                    int ch1 = source.read();
                    int ch2 = source.read();
                    // Prior to PDF v1.2, the # was not a special character.  Also,
                    // it has been observed that various PDF tools do not follow the
                    // spec with respect to the # escape, even though they report
                    // PDF versions of 1.2 or later.  The solution here is that we
                    // interpret the # as an escape only when it is followed by two
                    // valid hex digits.
                    if (isHexDigit((char)ch1) && isHexDigit((char)ch2))
                    {
                        String hex = Character.toString((char) ch1) + (char) ch2;
                        try
                        {
                            buffer.write(Integer.parseInt(hex, 16));
                        }
                        catch (NumberFormatException e)
                        {
                            throw new IOException("Error: expected hex digit, actual='" + hex + "'", e);
                        }
                        c = source.read();
                    }
                    else
                    {
                        // check for premature EOF
                        if (ch2 == -1 || ch1 == -1)
                        {
                            LOG.error("Premature EOF in BaseParser#parseCOSName");
                            c = -1;
                            break;
                        }
                        source.rewind(1);
                        c = ch1;
                        buffer.write(ch);
                    }
                }
                else if (isEndOfName(ch))
                {
                    break;
                }
                else
                {
                    buffer.write(ch);
                    c = source.read();
                }
            }
            if (c != -1)
            {
                source.rewind(1);
            }

            byte[] bytes = buffer.toByteArray();
            String string;
            if (isValidUTF8(bytes))
            {
                string = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
            }
            else
            {
                // some malformed PDFs don't use UTF-8 see PDFBOX-3347
                string = new String(buffer.toByteArray(), Charset.forName("Windows-1252"));
            }
            return COSName.getPDFName(string);
        }

        /**
         * Returns true if a byte sequence is valid UTF-8.
         */
        private boolean isValidUTF8(byte[] input)
        {
            try
            {
                utf8Decoder.decode(ByteBuffer.wrap(input));
                return true;
            }
            catch (CharacterCodingException e)
            {
                LOG.debug("Character could not be decoded using StandardCharsets.UTF_8 - returning false", e);
                return false;
            }
        }

        /**
         * This will parse a directory object from the stream.
         *
         * @return The parsed object.
         *
         * @throws IOException If there is an error during parsing.
         */
        protected COSBase parseDirObject() throws IOException
        {
            skipSpaces();
            char c = (char) source.peek();
            switch(c)
            {
            case '<':
                // pull off first left bracket
                source.read();
                // check for second left bracket
                c = (char) source.peek();
                source.rewind(1);
                return c == '<' ? parseCOSDictionary(true) : parseCOSString();
            case '[':
                // array
                return parseCOSArray();
            case '(':
                return parseCOSString();
            case '/':
                // name
                return parseCOSName();
            case 'n':
                // null
                readExpectedString(NULL, false);
                return COSNull.NULL;
            case 't':
                readExpectedString(TRUE, false);
                return COSBoolean.TRUE;
            case 'f':
                readExpectedString(FALSE, false);
                return COSBoolean.FALSE;
            case 'R':
                source.read();
                return new COSObject(null);
            case (char)-1:
                return null;
            default:
                if( Character.isDigit(c) || c == '-' || c == '+' || c == '.')
                {
                    return parseCOSNumber();
                }
                // This is not suppose to happen, but we will allow for it
                // so we are more compatible with POS writers that don't
                // follow the spec
                long startOffset = source.getPosition();
                String badString = readString();
                if (badString.isEmpty())
                {
                    int peek = source.peek();
                    // we can end up in an infinite loop otherwise
                    throw new IOException("Unknown dir object c='" + c + "' cInt=" + (int) c + " peek='"
                            + (char) peek + "' peekInt=" + peek + " at offset " + source.getPosition()
                            + " (start offset: " + startOffset + ")");
                }

                // if it's an endstream/endobj, we want to put it back so the caller will see it
                if (ENDOBJ_STRING.equals(badString) || ENDSTREAM_STRING.equals(badString))
                {
                    source.rewind(badString.getBytes(StandardCharsets.ISO_8859_1).length);
                }
                else
                {
                    LOG.warn("Skipped unexpected dir object = '" + badString + "' at offset "
                            + source.getPosition() + " (start offset: " + startOffset + ")");
                }
            }
            return null;
        }

        private COSNumber parseCOSNumber() throws IOException
        {
            StringBuilder buf = new StringBuilder();
            int ic = source.read();
            char c = (char) ic;
            while (Character.isDigit(c) || c == '-' || c == '+' || c == '.' || c == 'E' || c == 'e')
            {
                buf.append(c);
                ic = source.read();
                c = (char) ic;
            }
            if (ic != -1)
            {
                source.rewind(1);
            }
            return COSNumber.get(buf.toString());
        }

        /**
         * This will read the next string from the stream.
         *
         * @return The string that was read from the stream, never null.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected String readString() throws IOException
        {
            skipSpaces();
            StringBuilder buffer = new StringBuilder();
            int c = source.read();
            while( !isEndOfName((char)c) && c != -1 )
            {
                buffer.append( (char)c );
                c = source.read();
            }
            if (c != -1)
            {
                source.rewind(1);
            }
            return buffer.toString();
        }

        /**
         * Reads given pattern from {@link #source}. Skipping whitespace at start and end if wanted.
         *
         * @param expectedString pattern to be skipped
         * @param skipSpaces if set to true spaces before and after the string will be skipped
         * @throws IOException if pattern could not be read
         */
        protected final void readExpectedString(final char[] expectedString, boolean skipSpaces) throws IOException
        {
            skipSpaces();
            for (char c : expectedString)
            {
                if (source.read() != c)
                {
                    throw new IOException("Expected string '" + new String(expectedString)
                            + "' but missed at character '" + c + "' at offset "
                            + source.getPosition());
                }
            }
            skipSpaces();
        }

        /**
         * Read one char and throw an exception if it is not the expected value.
         *
         * @param ec the char value that is expected.
         * @throws IOException if the read char is not the expected value or if an
         * I/O error occurs.
         */
        protected void readExpectedChar(char ec) throws IOException
        {
            char c = (char) source.read();
            if (c != ec)
            {
                throw new IOException(
                        "expected='" + ec + "' actual='" + c + "' at offset " + source.getPosition());
            }
        }

        /**
         * This will read the next string from the stream up to a certain length.
         *
         * @param length The length to stop reading at.
         *
         * @return The string that was read from the stream of length 0 to length.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected String readString( int length ) throws IOException
        {
            skipSpaces();

            int c = source.read();

            //average string size is around 2 and the normal string buffer size is
            //about 16 so lets save some space.
            StringBuilder buffer = new StringBuilder(length);
            while( !isWhitespace(c) && !isClosing(c) && c != -1 && buffer.length() < length &&
                    c != '[' &&
                    c != '<' &&
                    c != '(' &&
                    c != '/' )
            {
                buffer.append( (char)c );
                c = source.read();
            }
            if (c != -1)
            {
                source.rewind(1);
            }
            return buffer.toString();
        }

        /**
         * This will tell if the next character is a closing brace( close of PDF array ).
         *
         * @return true if the next byte is ']', false otherwise.
         *
         * @throws IOException If an IO error occurs.
         */
        protected boolean isClosing() throws IOException
        {
            return isClosing(source.peek());
        }

        /**
         * This will tell if the next character is a closing brace( close of PDF array ).
         *
         * @param c The character to check against end of line
         * @return true if the next byte is ']', false otherwise.
         */
        protected boolean isClosing(int c)
        {
            return c == ']';
        }

        /**
         * This will read bytes until the first end of line marker occurs.
         * NOTE: The EOL marker may consists of 1 (CR or LF) or 2 (CR and CL) bytes
         * which is an important detail if one wants to unread the line.
         *
         * @return The characters between the current position and the end of the line.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected String readLine() throws IOException
        {
            if (source.isEOF())
            {
                throw new IOException( "Error: End-of-File, expected line");
            }

            StringBuilder buffer = new StringBuilder( 11 );

            int c;
            while ((c = source.read()) != -1)
            {
                // CR and LF are valid EOLs
                if (isEOL(c))
                {
                    break;
                }
                buffer.append( (char)c );
            }
            // CR+LF is also a valid EOL
            if (isCR(c) && isLF(source.peek()))
            {
                source.read();
            }
            return buffer.toString();
        }

        /**
         * This will tell if the next byte to be read is an end of line byte.
         *
         * @return true if the next byte is 0x0A or 0x0D.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected boolean isEOL() throws IOException
        {
            return isEOL(source.peek());
        }

        /**
         * This will tell if the end of the data is reached.
         *
         * @return true if the end of the data is reached.
         * @throws IOException If there is an error reading from the stream.
         */
        protected boolean isEOF() throws IOException
        {
            return source.isEOF();
        }

        /**
         * This will tell if the next byte to be read is an end of line byte.
         *
         * @param c The character to check against end of line
         * @return true if the next byte is 0x0A or 0x0D.
         */
        protected boolean isEOL(int c)
        {
            return isLF(c) || isCR(c);
        }

        private boolean isLF(int c)
        {
            return ASCII_LF == c;
        }

        private boolean isCR(int c)
        {
            return ASCII_CR == c;
        }

        /**
         * This will tell if the next byte is whitespace or not.
         *
         * @return true if the next byte in the stream is a whitespace character.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected boolean isWhitespace() throws IOException
        {
            return isWhitespace(source.peek());
        }

        /**
         * This will tell if a character is whitespace or not.  These values are
         * specified in table 1 (page 12) of ISO 32000-1:2008.
         * @param c The character to check against whitespace
         * @return true if the character is a whitespace character.
         */
        protected boolean isWhitespace( int c )
        {
            return c == 0 || c == 9 || c == 12  || c == ASCII_LF
            || c == ASCII_CR || c == ASCII_SPACE;
        }

        /**
         * This will tell if the next byte is a space or not.
         *
         * @return true if the next byte in the stream is a space character.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected boolean isSpace() throws IOException
        {
            return isSpace(source.peek());
        }

        /**
         * This will tell if the given value is a space or not.
         *
         * @param c The character to check against space
         * @return true if the next byte in the stream is a space character.
         */
        protected boolean isSpace(int c)
        {
            return ASCII_SPACE == c;
        }

        /**
         * This will tell if the next byte is a digit or not.
         *
         * @return true if the next byte in the stream is a digit.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected boolean isDigit() throws IOException
        {
            return isDigit(source.peek());
        }

        /**
         * This will tell if the given value is a digit or not.
         *
         * @param c The character to be checked
         * @return true if the next byte in the stream is a digit.
         */
        protected static boolean isDigit(int c)
        {
            return c >= ASCII_ZERO && c <= ASCII_NINE;
        }

        /**
         * This will skip all spaces and comments that are present.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected void skipSpaces() throws IOException
        {
            int c = source.read();
            // 37 is the % character, a comment
            while( isWhitespace(c) || c == 37)
            {
                if ( c == 37 )
                {
                    // skip past the comment section
                    c = source.read();
                    while(!isEOL(c) && c != -1)
                    {
                        c = source.read();
                    }
                }
                else
                {
                    c = source.read();
                }
            }
            if (c != -1)
            {
                source.rewind(1);
            }
        }

        /**
         * This will read a long from the Stream and throw an {@link IOException} if
         * the long value is negative or has more than 10 digits (i.e. : bigger than
         * {@link #OBJECT_NUMBER_THRESHOLD})
         *
         * @return the object number being read.
         * @throws IOException if an I/O error occurs
         */
        protected long readObjectNumber() throws IOException
        {
            long retval = readLong();
            if (retval < 0 || retval >= OBJECT_NUMBER_THRESHOLD)
            {
                throw new IOException("Object Number '" + retval + "' has more than 10 digits or is negative");
            }
            return retval;
        }

        /**
         * This will read a integer from the Stream and throw an {@link IllegalArgumentException} if the integer value
         * has more than the maximum object revision (i.e. : bigger than {@link #GENERATION_NUMBER_THRESHOLD})
         * @return the generation number being read.
         * @throws IOException if an I/O error occurs
         */
        protected int readGenerationNumber() throws IOException
        {
            int retval = readInt();
            if(retval < 0 || retval > GENERATION_NUMBER_THRESHOLD)
            {
                throw new IOException("Generation Number '" + retval + "' has more than 5 digits");
            }
            return retval;
        }

        /**
         * This will read an integer from the stream.
         *
         * @return The integer that was read from the stream.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected int readInt() throws IOException
        {
            skipSpaces();
            int retval = 0;

            StringBuilder intBuffer = readStringNumber();

            try
            {
                retval = Integer.parseInt( intBuffer.toString() );
            }
            catch( NumberFormatException e )
            {
                source.rewind(intBuffer.toString().getBytes(StandardCharsets.ISO_8859_1).length);
                throw new IOException("Error: Expected an integer type at offset " +
                        source.getPosition() +
                                      ", instead got '" + intBuffer + "'", e);
            }
            return retval;
        }

        /**
         * This will read an long from the stream.
         *
         * @return The long that was read from the stream.
         *
         * @throws IOException If there is an error reading from the stream.
         */
        protected long readLong() throws IOException
        {
            skipSpaces();
            long retval = 0;

            StringBuilder longBuffer = readStringNumber();

            try
            {
                retval = Long.parseLong( longBuffer.toString() );
            }
            catch( NumberFormatException e )
            {
                source.rewind(longBuffer.toString().getBytes(StandardCharsets.ISO_8859_1).length);
                throw new IOException( "Error: Expected a long type at offset "
                        + source.getPosition() + ", instead got '" + longBuffer + "'", e);
            }
            return retval;
        }

        /**
         * This method is used to read a token by the {@linkplain #readInt()} and the {@linkplain #readLong()} method. Valid
         * delimiters are any non digit values.
         *
         * @return the token to parse as integer or long by the calling method.
         * @throws IOException throws by the {@link #source} methods.
         */
        protected final StringBuilder readStringNumber() throws IOException
        {
            int lastByte;
            StringBuilder buffer = new StringBuilder();
            while ((lastByte = source.read()) >= '0' && lastByte <= '9')
            {
                buffer.append( (char)lastByte );
                if (buffer.length() > MAX_LENGTH_LONG)
                {
                    throw new IOException("Number '" + buffer +
                            "' is getting too long, stop reading at offset " + source.getPosition());
                }
            }
            if( lastByte != -1 )
            {
                source.rewind(1);
            }
            return buffer;
        }
    }