content: /*
   * plist - An open source library to parse and generate property lists
   * Copyright (C) 2014 Daniel Dreibrodt
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   * SOFTWARE.
   */
  package com.dd.plist;
  
  import java.io.CharArrayWriter;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.Reader;
  import java.io.UnsupportedEncodingException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.text.ParseException;
  import java.text.StringCharacterIterator;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Objects;
  
  /**
   * <p>
   * Parser for ASCII property lists. Supports Apple OS X/iOS and GnuStep/NeXTSTEP format. This parser
   * is based on the recursive descent paradigm, but the underlying grammar is not explicitly
   * defined.
   * </p>
   * <p>
   * Resources on ASCII property list format:
   * </p>
   * <ul>
   * <li><a href="https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html">
   * Property List Programming Guide - Old-Style ASCII Property Lists
   * </a></li>
   * <li><a href="http://www.gnustep.org/resources/documentation/Developer/Base/Reference/NSPropertyList.html">
   * GnuStep - NSPropertyListSerialization class documentation
   * </a></li>
   * </ul>
   *
   * @author Daniel Dreibrodt
   */
  public final class ASCIIPropertyListParser {
  
    /**
     * The space character token.
     */
    public static final char WHITESPACE_SPACE = ' ';
    /**
     * The tab character token.
     */
    public static final char WHITESPACE_TAB = '\t';
    /**
     * The newline character token.
     */
    public static final char WHITESPACE_NEWLINE = '\n';
    /**
     * The carriage return character token.
     */
    public static final char WHITESPACE_CARRIAGE_RETURN = '\r';
  
    /**
     * The token marking the beginning of an array.
     */
    public static final char ARRAY_BEGIN_TOKEN = '(';
    /**
     * The token marking the end of an array.
     */
    public static final char ARRAY_END_TOKEN = ')';
    /**
     * The token marking the end of an array element.
     */
    public static final char ARRAY_ITEM_DELIMITER_TOKEN = ',';
  
    /**
     * The token marking the beginning of a dictionary.
     */
    public static final char DICTIONARY_BEGIN_TOKEN = '{';
    /**
     * The token marking the end of a dictionary.
     */
    public static final char DICTIONARY_END_TOKEN = '}';
    /**
     * The token marking the assignment of a value to a dictionary key.
     */
    public static final char DICTIONARY_ASSIGN_TOKEN = '=';
    /**
     * The token marking the end of a dictionary entry.
     */
    public static final char DICTIONARY_ITEM_DELIMITER_TOKEN = ';';
  
    /**
     * The token marking the beginning of a quoted string.
     */
    public static final char QUOTEDSTRING_BEGIN_TOKEN = '"';
    /**
     * The token marking the end of a quoted string.
     */
    public static final char QUOTEDSTRING_END_TOKEN = '"';
    /**
     * The token marking the beginning of an escape sequence in a quoted string.
     */
    public static final char QUOTEDSTRING_ESCAPE_TOKEN = '\\';
  
    /**
     * The token marking the beginning of a data element.
     */
    public static final char DATA_BEGIN_TOKEN = '<';
    /**
     * The token marking the end of a data element.
     */
    public static final char DATA_END_TOKEN = '>';
  
    /**
     * The token marking the beginning of a data element in Base-64 encoding.
     */
    public static final char DATA_BASE64_BEGIN_TOKEN = '[';
    /**
     * The token marking the end of a data element in Base-64 encoding.
     */
    public static final char DATA_BASE64_END_TOKEN = ']';
  
    /**
     * The token marking the beginning of a GnuStep object.
     */
    public static final char DATA_GSOBJECT_BEGIN_TOKEN = '*';
    /**
     * The token marking the beginning of a GnuStep date.
     */
    public static final char DATA_GSDATE_BEGIN_TOKEN = 'D';
    /**
     * The token marking the beginning of a GnuStep boolean value.
     */
    public static final char DATA_GSBOOL_BEGIN_TOKEN = 'B';
    /**
     * The token representing the boolean value {@code true} in the GnuStep format.
     */
    public static final char DATA_GSBOOL_TRUE_TOKEN = 'Y';
    /**
     * The token representing the boolean value {@code false} in the GnuStep format.
     */
    public static final char DATA_GSBOOL_FALSE_TOKEN = 'N';
    /**
     * The token marking the beginning of a GnuStep integer value.
     */
    public static final char DATA_GSINT_BEGIN_TOKEN = 'I';
    /**
     * The token marking the beginning of a GnuStep real value.
     */
    public static final char DATA_GSREAL_BEGIN_TOKEN = 'R';
  
    /**
     * The token that separates the parts of a date value (year, month and day).
     */
    public static final char DATE_DATE_FIELD_DELIMITER = '-';
    /**
     * The token that separates the parts of a time value (hour, minute and second).
     */
    public static final char DATE_TIME_FIELD_DELIMITER = ':';
    /**
     * The token that separates the date and time in the GnuStep format.
     */
    public static final char DATE_GS_DATE_TIME_DELIMITER = ' ';
    /**
     * The token that marks the beginning of the time zone in the Apple format.
     */
    public static final char DATE_APPLE_DATE_TIME_DELIMITER = 'T';
    /**
     * The token that marks the end of the time zone in the Apple format.
     */
    public static final char DATE_APPLE_END_TOKEN = 'Z';
  
    /**
     * The token marking the beginning of a comment.
     */
    public static final char COMMENT_BEGIN_TOKEN = '/';
    /**
     * The token marking a comment to be multi-line.
     */
    public static final char MULTILINE_COMMENT_SECOND_TOKEN = '*';
    /**
     * The token marking a comment to be single-line.
     */
    public static final char SINGLELINE_COMMENT_SECOND_TOKEN = '/';
    /**
     * The token marking the end of a multi-line comment. Must be preceded by a
     * {@link ASCIIPropertyListParser#MULTILINE_COMMENT_SECOND_TOKEN}.
     */
    public static final char MULTILINE_COMMENT_END_TOKEN = '/';
  
    /**
     * Property list source data
     */
    private final char[] data;
    /**
     * Current parsing index
     */
    private int index;
  
    /**
     * Current line number.
     */
    private int lineNo = 1;
  
    /**
     * The index at which the current line began.
     */
    private int lineBeginning = -1;
  
    /**
     * Creates a new parser for the given property list content.
     *
     * @param propertyListContent The content of the property list that is to be parsed.
     * @param encoding            The name of a supported {@link java.nio.charset.Charset charset} to
     *                            decode the property list.
     * @throws java.io.UnsupportedEncodingException If no support for the named charset is available
     *                                              in this instance of the Java virtual machine.
     */
    private ASCIIPropertyListParser(byte[] propertyListContent, String encoding)
        throws UnsupportedEncodingException {
      this(new String(propertyListContent, encoding).toCharArray());
    }
  
    /**
     * Creates a new parser for the given property list content.
     *
     * @param propertyListContent The content of the property list that is to be parsed.
     */
    private ASCIIPropertyListParser(char[] propertyListContent) {
      this.data = propertyListContent;
    }
  
    /**
     * Parses an ASCII property list file.
     *
     * @param f The ASCII property list file.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException If an error occurs during parsing.
     * @throws java.io.IOException      If an error occurs while reading from the input stream.
     */
    public static NSObject parse(File f) throws IOException, ParseException {
      return parse(f.toPath());
    }
  
    /**
     * Parses an ASCII property list file.
     *
     * @param f        The ASCII property list file.
     * @param encoding The name of a supported {@link java.nio.charset.Charset charset} to decode the
     *                 property list.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException             If an error occurs during parsing.
     * @throws java.io.IOException                  If an error occurs while reading from the input
     *                                              stream.
     * @throws java.io.UnsupportedEncodingException If no support for the named charset is available
     *                                              in this instance of the Java virtual machine.
     */
    public static NSObject parse(File f, String encoding) throws IOException, ParseException {
      return parse(f.toPath(), encoding);
    }
  
    /**
     * Parses an ASCII property list file.
     *
     * @param path     The path to the ASCII property list file.
     * @param encoding The name of a supported {@link java.nio.charset.Charset charset} to decode the
     *                 property list.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException             If an error occurs during parsing.
     * @throws java.io.IOException                  If an error occurs while reading from the input
     *                                              stream.
     * @throws java.io.UnsupportedEncodingException If no support for the named charset is available
     *                                              in this instance of the Java virtual machine.
     */
    public static NSObject parse(Path path, String encoding) throws IOException, ParseException {
      try (InputStream fileInputStream = Files.newInputStream(path)) {
        return parse(fileInputStream, encoding);
      }
    }
  
    /**
     * Parses an ASCII property list file.
     *
     * @param path The path to the ASCII property list file.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException If an error occurs during parsing.
     * @throws java.io.IOException      If an error occurs while reading from the input stream.
     */
    public static NSObject parse(Path path) throws IOException, ParseException {
      try (InputStream fileInputStream = Files.newInputStream(path)) {
        return parse(fileInputStream);
      }
    }
  
    /**
     * Parses an ASCII property list from an input stream. This method does not close the specified
     * input stream.
     *
     * @param in The input stream that provides the property list's data.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException If an error occurs during parsing.
     * @throws java.io.IOException      If an error occurs while reading from the input stream.
     */
    public static NSObject parse(InputStream in) throws ParseException, IOException {
      return parse(PropertyListParser.readAll(in));
    }
  
    /**
     * Parses an ASCII property list from an input stream. This method does not close the specified
     * input stream.
     *
     * @param in       The input stream that points to the property list's data.
     * @param encoding The name of a supported {@link java.nio.charset.Charset charset} to decode the
     *                 property list.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException             If an error occurs during parsing.
     * @throws java.io.IOException                  If an error occurs while reading from the input
     *                                              stream.
     * @throws java.io.UnsupportedEncodingException If no support for the named charset is available
     *                                              in this instance of the Java virtual machine.
     */
    public static NSObject parse(InputStream in, String encoding) throws ParseException, IOException {
      return parse(PropertyListParser.readAll(in), encoding);
    }
  
    /**
     * Parses an ASCII property list from a {@link Reader}. This method does not close the specified
     * reader.
     *
     * @param reader The reader that provides the property list's data.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException If an error occurs during parsing.
     * @throws java.io.IOException      If an error occurs while reading from the input reader.
     */
    public static NSObject parse(Reader reader) throws ParseException, IOException {
      Objects.requireNonNull(reader, "The specified reader is null");
  
      CharArrayWriter charArrayWriter = new CharArrayWriter();
      char[] buf = new char[4096];
      int read;
      while ((read = reader.read(buf)) >= 0) {
        charArrayWriter.write(buf, 0, read);
      }
  
      ASCIIPropertyListParser parser = new ASCIIPropertyListParser(charArrayWriter.toCharArray());
      return parser.parse();
    }
  
    /**
     * Parses an ASCII property list from a {@link String}
     *
     * @param plistData A string containing the property list's data.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws java.text.ParseException If an error occurs during parsing.
     */
    public static NSObject parse(String plistData) throws ParseException {
      ASCIIPropertyListParser parser = new ASCIIPropertyListParser(plistData.toCharArray());
      return parser.parse();
    }
  
    /**
     * Parses an ASCII property list from a byte array.
     *
     * @param bytes The ASCII property list data.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws ParseException If an error occurs during parsing.
     */
    public static NSObject parse(byte[] bytes) throws ParseException {
      String charset = ByteOrderMarkReader.detect(bytes);
      if (charset == null) {
        charset = "UTF-8";
      }
  
      try {
        return parse(bytes, charset);
      } catch (UnsupportedEncodingException e) {
        // Unlikely to happen as only standard codepages are requested
        throw new RuntimeException(
            "Unsupported property list encoding (" + charset + "): " + e.getMessage());
      }
    }
  
    /**
     * Parses an ASCII property list from a byte array.
     *
     * @param bytes    The ASCII property list data.
     * @param encoding The name of a supported {@link java.nio.charset.Charset} charset to decode the
     *                 property list.
     * @return The root object of the property list. This is usually a {@link NSDictionary} but can
     * also be a {@link NSArray}.
     * @throws ParseException                       If an error occurs during parsing.
     * @throws java.io.UnsupportedEncodingException If no support for the named charset is available
     *                                              in this instance of the Java virtual machine.
     */
    public static NSObject parse(byte[] bytes, String encoding)
        throws ParseException, UnsupportedEncodingException {
      ASCIIPropertyListParser parser = new ASCIIPropertyListParser(bytes, encoding);
      return parser.parse();
    }
  
    /**
     * Checks whether the given sequence of symbols can be accepted.
     *
     * @param sequence The sequence of tokens to look for.
     * @return Whether the given tokens occur at the current parsing position.
     */
    private boolean acceptSequence(char... sequence) {
      if (this.index + sequence.length > this.data.length) {
        return false;
      }
  
      for (int i = 0; i < sequence.length; i++) {
        if (this.data[this.index + i] != sequence[i]) {
          return false;
        }
      }
  
      return true;
    }
  
    /**
     * Checks whether the given symbols can be accepted, that is, if one of the given symbols is found
     * at the current parsing position.
     *
     * @param acceptableSymbols The symbols to check.
     * @return Whether one of the symbols can be accepted or not.
     */
    private boolean accept(char... acceptableSymbols) {
      boolean symbolPresent = false;
      if (this.index < this.data.length) {
        for (char c : acceptableSymbols) {
          if (this.data[this.index] == c) {
            symbolPresent = true;
            break;
          }
        }
      }
  
      return symbolPresent;
    }
  
    /**
     * Checks whether the given symbol can be accepted, that is, if the given symbols is found at the
     * current parsing position.
     *
     * @param acceptableSymbol The symbol to check.
     * @return Whether the symbol can be accepted or not.
     */
    private boolean accept(char acceptableSymbol) {
      return this.index < this.data.length && this.data[this.index] == acceptableSymbol;
    }
  
    /**
     * Expects the input to have one of the given symbols at the current parsing position.
     *
     * @param expectedSymbols The expected symbols.
     * @throws ParseException If none of the expected symbols could be found.
     */
    private void expect(char... expectedSymbols) throws ParseException {
      if (!this.accept(expectedSymbols)) {
        StringBuilder excString = new StringBuilder();
        excString.append("Expected '").append(expectedSymbols[0]).append("'");
        for (int i = 1; i < expectedSymbols.length; i++) {
          excString.append(" or '").append(expectedSymbols[i]).append("'");
        }
  
        if (this.index < this.data.length) {
          excString.append(" but found '").append(this.data[this.index]).append("'");
        } else {
          excString.append(" but reached end of input");
        }
  
        throw this.createParseException(excString.toString(), this.index);
      }
    }
  
    /**
     * Expects the input to have the given symbol at the current parsing position.
     *
     * @param expectedSymbol The expected symbol.
     * @throws ParseException If the expected symbol could not be found.
     */
    private void expect(char expectedSymbol) throws ParseException {
      if (!this.accept(expectedSymbol)) {
        throw this.createParseException(
            this.index < this.data.length
                ? "Expected '" + expectedSymbol + "' but found '" + this.data[this.index] + "'"
                : "Expected '" + expectedSymbol + "' but reached end of input",
            this.index);
      }
    }
  
    /**
     * Reads an expected symbol.
     *
     * @param symbol The symbol to read.
     * @throws ParseException If the expected symbol could not be read.
     */
    private void read(char symbol) throws ParseException {
      this.expect(symbol);
      this.index++;
    }
  
    /**
     * Skips the current symbol.
     */
    private void skip() {
      this.index++;
    }
  
    /**
     * Skips several symbols
     *
     * @param numSymbols The amount of symbols to skip.
     */
    private void skip(int numSymbols) {
      this.index += numSymbols;
    }
  
    private void trackLineBreak() {
      if (this.data[this.index] == WHITESPACE_NEWLINE) {
        // \n or \r\n
        this.lineNo++;
        this.lineBeginning = this.index;
      }
      if (this.data[this.index] == WHITESPACE_CARRIAGE_RETURN
          && !(this.index + 1 < this.data.length
          && this.data[this.index + 1] == WHITESPACE_NEWLINE)) {
        // Single \r
        this.lineNo++;
        this.lineBeginning = this.index;
      }
    }
  
    /**
     * Skips all whitespaces and comments from the current parsing position onward.
     */
    private void skipWhitespacesAndComments() {
      boolean commentSkipped;
      do {
        commentSkipped = false;
  
        //Skip whitespaces
        while (this.accept(WHITESPACE_CARRIAGE_RETURN, WHITESPACE_NEWLINE, WHITESPACE_SPACE,
            WHITESPACE_TAB)) {
          this.trackLineBreak();
          this.skip();
        }
  
        //Skip single line comments "//..."
        if (this.acceptSequence(COMMENT_BEGIN_TOKEN, SINGLELINE_COMMENT_SECOND_TOKEN)) {
          this.skip(2);
          this.readInputUntil(WHITESPACE_CARRIAGE_RETURN, WHITESPACE_NEWLINE);
          commentSkipped = true;
        }
  
        //Skip multi line comments "/* ... */"
        else if (this.acceptSequence(COMMENT_BEGIN_TOKEN, MULTILINE_COMMENT_SECOND_TOKEN)) {
          this.skip(2);
          while (this.index < this.data.length) {
            if (this.acceptSequence(MULTILINE_COMMENT_SECOND_TOKEN, MULTILINE_COMMENT_END_TOKEN)) {
              this.skip(2);
              break;
            }
  
            this.trackLineBreak();
            this.skip();
          }
          commentSkipped = true;
        }
      }
      while (commentSkipped); //if a comment was skipped more whitespace or another comment can follow, so skip again
    }
  
    /**
     * Reads input until one of the given symbols is found.
     *
     * @param symbols The symbols that can occur after the string to read.
     * @return The input until one the given symbols.
     */
    private String readInputUntil(char... symbols) {
      StringBuilder strBuf = new StringBuilder();
      while (this.index < this.data.length && !this.accept(symbols)) {
        strBuf.append(this.data[this.index]);
        this.skip();
      }
  
      return strBuf.toString();
    }
  
    /**
     * Reads input until the given symbol is found.
     *
     * @param symbol The symbol that can occur after the string to read.
     * @return The input until the given symbol.
     */
    private String readInputUntil(char symbol) {
      StringBuilder strBuf = new StringBuilder();
      while (this.index < this.data.length && !this.accept(symbol)) {
        strBuf.append(this.data[this.index]);
        this.trackLineBreak();
        this.skip();
      }
      return strBuf.toString();
    }
  
    /**
     * Parses the property list from the beginning and returns the root object of the property list.
     *
     * @return The root object of the property list. This can either be a NSDictionary or a NSArray.
     * @throws ParseException If an error occurred during parsing
     */
    public NSObject parse() throws ParseException {
      this.index = 0;
      if (this.data.length == 0) {
        throw new ParseException("The property list is empty.", 0);
      }
  
      //Skip Unicode byte order mark (BOM)
      if (this.data[0] == '\uFEFF') {
        this.skip(1);
      }
  
      this.skipWhitespacesAndComments();
      this.expect(DICTIONARY_BEGIN_TOKEN, ARRAY_BEGIN_TOKEN, COMMENT_BEGIN_TOKEN);
      try {
        return this.parseObject();
      } catch (ArrayIndexOutOfBoundsException ex) {
        throw this.createParseException("Reached end of input unexpectedly.", this.index);
      }
    }
  
    /**
     * Parses the NSObject found at the current position in the property list data stream.
     *
     * @return The parsed NSObject.
     * @see ASCIIPropertyListParser#index
     */
    private NSObject parseObject() throws ParseException {
      LocationInformation loc = new ASCIILocationInformation(this.index, this.lineNo,
          this.index - this.lineBeginning);
      NSObject result;
      switch (this.data[this.index]) {
        case ARRAY_BEGIN_TOKEN: {
          result = this.parseArray();
          break;
        }
        case DICTIONARY_BEGIN_TOKEN: {
          result = this.parseDictionary();
          break;
        }
        case DATA_BEGIN_TOKEN: {
          result = this.parseData();
          break;
        }
        case QUOTEDSTRING_BEGIN_TOKEN: {
          String quotedString = this.parseQuotedString();
          //apple dates are quoted strings of length 20 and after the 4 year digits a dash is found
          if (quotedString.length() == 20 && quotedString.charAt(4) == DATE_DATE_FIELD_DELIMITER) {
            try {
              result = new NSDate(quotedString);
            } catch (Exception ex) {
              //not a date? --> return string
              result = new NSString(quotedString);
            }
          } else {
            result = new NSString(quotedString);
          }
  
          break;
        }
        default: {
          //0-9
          if (this.data[this.index] >= '0' && this.data[this.index] <= '9') {
            //could be a date or just a string
            result = this.parseDateString();
          } else {
            //non-numerical -> string or boolean
            result = new NSString(this.parseString());
          }
  
          break;
        }
      }
  
      if (result != null) {
        result.setLocationInformation(loc);
      }
  
      return result;
    }
  
    /**
     * Parses an array from the current parsing position. The prerequisite for calling this method is,
     * that an array begin token has been read.
     *
     * @return The array found at the parsing position.
     */
    private NSArray parseArray() throws ParseException {
      //Skip begin token
      this.skip();
      this.skipWhitespacesAndComments();
      List<NSObject> objects = new LinkedList<>();
      while (!this.accept(ARRAY_END_TOKEN)) {
        objects.add(this.parseObject());
        this.skipWhitespacesAndComments();
        if (this.accept(ARRAY_ITEM_DELIMITER_TOKEN)) {
          this.skip();
        } else {
          break; //must have reached end of array
        }
  
        this.skipWhitespacesAndComments();
      }
  
      //parse end token
      this.read(ARRAY_END_TOKEN);
      return new NSArray(objects.toArray(new NSObject[0]));
    }
  
    /**
     * Parses a dictionary from the current parsing position. The prerequisite for calling this method
     * is, that a dictionary begin token has been read.
     *
     * @return The dictionary found at the parsing position.
     */
    private NSDictionary parseDictionary() throws ParseException {
      //Skip begin token
      this.skip();
      this.skipWhitespacesAndComments();
      NSDictionary dict = new NSDictionary();
      while (!this.accept(DICTIONARY_END_TOKEN)) {
        //Parse key
        String keyString;
        if (this.accept(QUOTEDSTRING_BEGIN_TOKEN)) {
          keyString = this.parseQuotedString();
        } else {
          keyString = this.parseString();
        }
  
        this.skipWhitespacesAndComments();
  
        //Parse assign token
        this.read(DICTIONARY_ASSIGN_TOKEN);
        this.skipWhitespacesAndComments();
  
        NSObject object = this.parseObject();
        dict.put(keyString, object);
        this.skipWhitespacesAndComments();
        this.read(DICTIONARY_ITEM_DELIMITER_TOKEN);
        this.skipWhitespacesAndComments();
      }
  
      //skip end token
      this.skip();
  
      return dict;
    }
  
    /**
     * Parses a data object from the current parsing position. This can either be a NSData object or a
     * GnuStep NSNumber or NSDate. The prerequisite for calling this method is, that a data begin
     * token has been read.
     *
     * @return The data object found at the parsing position.
     */
    private NSObject parseData() throws ParseException {
      int dataStartIndex = this.index;
      NSObject obj = null;
      //Skip begin token
      this.skip();
      if (this.accept(DATA_GSOBJECT_BEGIN_TOKEN)) {
        this.skip();
        this.expect(
            DATA_GSBOOL_BEGIN_TOKEN,
            DATA_GSDATE_BEGIN_TOKEN,
            DATA_GSINT_BEGIN_TOKEN,
            DATA_GSREAL_BEGIN_TOKEN);
        if (this.accept(DATA_GSBOOL_BEGIN_TOKEN)) {
          //Boolean
          this.skip();
          this.expect(DATA_GSBOOL_TRUE_TOKEN, DATA_GSBOOL_FALSE_TOKEN);
          if (this.accept(DATA_GSBOOL_TRUE_TOKEN)) {
            obj = new NSNumber(true);
          } else {
            obj = new NSNumber(false);
          }
  
          //Skip the parsed boolean token
          this.skip();
        } else if (this.accept(DATA_GSDATE_BEGIN_TOKEN)) {
          //Date
          this.skip();
          String dateString = this.readInputUntil(DATA_END_TOKEN);
          obj = new NSDate(dateString);
        } else if (this.accept(DATA_GSINT_BEGIN_TOKEN, DATA_GSREAL_BEGIN_TOKEN)) {
          //Number
          this.skip();
          String numberString = this.readInputUntil(DATA_END_TOKEN);
          try {
            obj = new NSNumber(numberString);
          } catch (IllegalArgumentException ex) {
            throw this.createParseException(
                "The NSNumber object has an invalid format.", dataStartIndex);
          }
        }
  
        // parse data end token
        this.read(DATA_END_TOKEN);
      } else if (this.accept(DATA_BASE64_BEGIN_TOKEN)) {
  
        // skip DATA_BASE64_BEGIN_TOKEN token
        this.skip();
  
        String dataString = this.readInputUntil(DATA_BASE64_END_TOKEN);
  
        try {
          obj = new NSData(dataString);
        } catch (IOException e) {
          throw this.createParseException(
              "The NSData object could be parsed.", dataStartIndex);
        }
  
        // skip DATA_BASE64_END_TOKEN token
        this.skip();
  
        // parse data end token
        this.read(DATA_END_TOKEN);
      } else {
        String dataString = this.readInputUntil(DATA_END_TOKEN);
        dataString = dataString.replaceAll("\\s+", "");
  
        int numBytes = dataString.length() / 2;
        byte[] bytes = new byte[numBytes];
        int nibble1, nibble2;
        for (int bi = 0, ci = 0; bi < bytes.length; bi++, ci += 2) {
          nibble1 = Character.digit(dataString.charAt(ci), 16);
          nibble2 = Character.digit(dataString.charAt(ci + 1), 16);
          if (nibble1 == -1 || nibble2 == -1) {
            throw this.createParseException(
                "The NSData object contains non-hexadecimal characters.", dataStartIndex);
          }
  
          bytes[bi] = (byte) (nibble1 << 4 | nibble2);
        }
  
        obj = new NSData(bytes);
  
        // skip DATA_END_TOKEN
        this.skip();
      }
  
      return obj;
    }
  
    /**
     * Attempts to parse a plain string as a date if possible.
     *
     * @return An NSDate if the string represents such an object. Otherwise, an NSString is returned.
     */
    private NSObject parseDateString() {
      String numericalString = this.parseString();
      if (numericalString.length() > 4 && numericalString.charAt(4) == DATE_DATE_FIELD_DELIMITER) {
        try {
          return new NSDate(numericalString);
        } catch (Exception ex) {
          //An exception occurs if the string is not actually a date but just a string
        }
      }
  
      return new NSString(numericalString);
    }
  
    /**
     * Parses a plain string from the current parsing position. The string is made up of all
     * characters to the next whitespace, delimiter token or assignment token.
     *
     * @return The string found at the current parsing position.
     */
    private String parseString() {
      return this.readInputUntil(WHITESPACE_SPACE, WHITESPACE_TAB, WHITESPACE_NEWLINE,
          WHITESPACE_CARRIAGE_RETURN,
          ARRAY_ITEM_DELIMITER_TOKEN, DICTIONARY_ITEM_DELIMITER_TOKEN, DICTIONARY_ASSIGN_TOKEN,
          ARRAY_END_TOKEN);
    }
  
    /**
     * Parses a quoted string from the current parsing position. The prerequisite for calling this
     * method is, that a quoted string begin token has been read.
     *
     * @return The quoted string found at the parsing method with all special characters unescaped.
     * @throws ParseException If an error occurred during parsing.
     */
    private String parseQuotedString() throws ParseException {
      int startIndex = this.index;
      //Skip begin token
      this.skip();
  
      StringBuilder stringBuilder = new StringBuilder();
      boolean unescapedBackslash = true;
      EscapeSequenceHandler escapeSequenceHandler = null;
  
      while (this.data[this.index] != QUOTEDSTRING_END_TOKEN
          || escapeSequenceHandler != null) {
        char c = this.data[this.index];
  
        if (escapeSequenceHandler != null) {
          if (escapeSequenceHandler.handleNextChar(c)) {
            escapeSequenceHandler = null;
          }
        } else if (c == QUOTEDSTRING_ESCAPE_TOKEN) {
          escapeSequenceHandler = new EscapeSequenceHandler(stringBuilder);
        } else {
          stringBuilder.append(c);
        }
  
        this.trackLineBreak();
        this.skip();
      }
  
      if (escapeSequenceHandler != null) {
        escapeSequenceHandler.handleEndOfString();
      }
  
      //skip end token
      this.skip();
  
      return stringBuilder.toString();
    }
  
    private ParseException createParseException(String message) {
      return this.createParseException(message, this.index);
    }
  
    private ParseException createParseException(String message, int index) {
      return new ParseException(
          message + " (" + this.lineNo + ":" + (index - this.lineBeginning) + ")",
          index);
    }
  
    private class EscapeSequenceHandler {
  
      private final int startIndex;
      private final StringBuilder stringBuilder;
      private int unicodeReferenceRadix = 0;
      private StringBuilder unicodeReference;
  
      public EscapeSequenceHandler(StringBuilder stringBuilder) {
        this.startIndex = ASCIIPropertyListParser.this.index;
        this.stringBuilder = stringBuilder;
      }
  
      public boolean handleNextChar(char c) throws ParseException {
        switch (this.unicodeReferenceRadix) {
          case 8:
            return this.handleNextCharOfOctalEscapeSequence(c);
          case 16:
            return this.handleNextCharOfHexEscapeSequence(c);
          default:
            return this.handleFirstChar(c);
        }
      }
  
      public void handleEndOfString() throws ParseException {
        String sequence = new String(
            ASCIIPropertyListParser.this.data,
            this.startIndex,
            ASCIIPropertyListParser.this.index - this.startIndex + 1);
        throw ASCIIPropertyListParser.this.createParseException(
            "The property list contains a string with an incomplete escape sequence: "
                + sequence,
            this.startIndex);
      }
  
      private boolean handleFirstChar(char c) throws ParseException {
        switch (c) {
          case '\\':
          case '"':
          case '\'':
            this.stringBuilder.append(c);
            return true;
          case 'b':
            this.stringBuilder.append('\b');
            return true;
          case 'n':
            this.stringBuilder.append('\n');
            return true;
          case 'r':
            this.stringBuilder.append('\r');
            return true;
          case 't':
            this.stringBuilder.append('\t');
            return true;
          case 'U':
          case 'u':
            this.unicodeReferenceRadix = 16;
            this.unicodeReference = new StringBuilder(4);
            return false;
          case '0':
          case '1':
          case '2':
          case '3':
          case '4':
          case '5':
          case '6':
          case '7':
            this.unicodeReferenceRadix = 8;
            this.unicodeReference = new StringBuilder(3);
            this.unicodeReference.append(c);
            return false;
          default:
            throw ASCIIPropertyListParser.this.createParseException(
                "The property list contains an invalid escape sequence: \\" + c,
                this.startIndex);
        }
      }
  
      private boolean handleNextCharOfHexEscapeSequence(char c) throws ParseException {
        if (Character.digit(c, 16) == -1) {
          String sequence = new String(
              ASCIIPropertyListParser.this.data,
              this.startIndex,
              ASCIIPropertyListParser.this.index - this.startIndex + 1);
          throw ASCIIPropertyListParser.this.createParseException(
              "The property list contains a string with an invalid escape sequence: "
                  + sequence,
              this.startIndex);
        }
  
        this.unicodeReference.append(c);
        if (this.unicodeReference.length() == 4) {
            char escapedChar = (char) Integer.parseInt(this.unicodeReference.toString(),
                16);
            this.stringBuilder.append(escapedChar);
            return true;
        }
  
        return false;
      }
  
      private boolean handleNextCharOfOctalEscapeSequence(char c) throws ParseException {
        if (Character.digit(c, 8) == -1) {
          String sequence = new String(
              ASCIIPropertyListParser.this.data,
              this.startIndex,
              ASCIIPropertyListParser.this.index - this.startIndex + 1);
          throw ASCIIPropertyListParser.this.createParseException(
              "The property list contains a string with an invalid escape sequence: "
                  + sequence,
              this.startIndex);
        }
  
        this.unicodeReference.append(c);
        if (this.unicodeReference.length() == 3) {
            char escapedChar = (char) Integer.parseInt(this.unicodeReference.toString(),
                8);
            this.stringBuilder.append(escapedChar);
            return true;
        }
  
        return false;
      }
    }
  }
  