From 961358f736ffd4aab1594e0d96f25cfd9302014e Mon Sep 17 00:00:00 2001 From: Daniel Dreibrodt Date: Sun, 10 Mar 2024 14:08:03 +0100 Subject: [PATCH] Add location information to parsed objects and exceptions (#95) * Store object locations when parsing and provide location of error in exceptions, where available * Add line number tracking to ASCII plist parsing * Add line information to ASCIIPropertyListParser exceptions * Add parse method overloads to store XML line information when parsing * Restructure unit tests * Remove old ant script --- build.xml | 56 - pom.xml | 1 + src/main/assembly/sources.xml | 3 - .../dd/plist/ASCIILocationInformation.java | 46 + .../com/dd/plist/ASCIIPropertyListParser.java | 1737 ++++++++++------- .../dd/plist/BinaryLocationInformation.java | 37 + .../dd/plist/BinaryPropertyListParser.java | 164 +- .../com/dd/plist/LocationInformation.java | 46 + src/main/java/com/dd/plist/NSObject.java | 21 +- .../dd/plist/PropertyListFormatException.java | 40 +- .../java/com/dd/plist/XMLLocationFilter.java | 39 + .../com/dd/plist/XMLLocationInformation.java | 96 + .../com/dd/plist/XMLPropertyListParser.java | 314 ++- .../test/ASCIIPropertyListParserTest.java | 265 +++ .../test/ASCIIPropertyListWriterTest.java | 41 + .../test/BinaryPropertyListParserTest.java | 99 + .../test/BinaryPropertyListWriterTest.java | 27 + .../java/com/dd/plist/test/IssueTest.java | 4 +- .../java/com/dd/plist/test/NSNumberTest.java | 63 +- .../test/{NSSetTests.java => NSSetTest.java} | 8 +- .../java/com/dd/plist/test/NSStringTest.java | 60 +- .../java/com/dd/plist/test/ParseTest.java | 329 ---- .../test/{UidTests.java => UidTest.java} | 12 +- .../plist/test/XMLPropertyListParserTest.java | 153 ++ .../plist/test/XMLPropertyListWriterTest.java | 22 + .../test1-ascii-multiline-handling.plist | 25 + test-files/test1-binary.plist | Bin 0 -> 145 bytes 27 files changed, 2361 insertions(+), 1347 deletions(-) delete mode 100644 build.xml create mode 100644 src/main/java/com/dd/plist/ASCIILocationInformation.java create mode 100644 src/main/java/com/dd/plist/BinaryLocationInformation.java create mode 100644 src/main/java/com/dd/plist/LocationInformation.java create mode 100644 src/main/java/com/dd/plist/XMLLocationFilter.java create mode 100644 src/main/java/com/dd/plist/XMLLocationInformation.java create mode 100644 src/test/java/com/dd/plist/test/ASCIIPropertyListParserTest.java create mode 100644 src/test/java/com/dd/plist/test/ASCIIPropertyListWriterTest.java create mode 100644 src/test/java/com/dd/plist/test/BinaryPropertyListParserTest.java create mode 100644 src/test/java/com/dd/plist/test/BinaryPropertyListWriterTest.java rename src/test/java/com/dd/plist/test/{NSSetTests.java => NSSetTest.java} (87%) delete mode 100644 src/test/java/com/dd/plist/test/ParseTest.java rename src/test/java/com/dd/plist/test/{UidTests.java => UidTest.java} (94%) create mode 100644 src/test/java/com/dd/plist/test/XMLPropertyListParserTest.java create mode 100644 src/test/java/com/dd/plist/test/XMLPropertyListWriterTest.java create mode 100644 test-files/test1-ascii-multiline-handling.plist create mode 100644 test-files/test1-binary.plist diff --git a/build.xml b/build.xml deleted file mode 100644 index 14d9dc8..0000000 --- a/build.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pom.xml b/pom.xml index 6c8399f..ca4caad 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ 3.8.1 -Xlint:unchecked + -Xlint:deprecation diff --git a/src/main/assembly/sources.xml b/src/main/assembly/sources.xml index af90def..e3d695e 100644 --- a/src/main/assembly/sources.xml +++ b/src/main/assembly/sources.xml @@ -5,9 +5,6 @@ false - - build.xml - pom.xml diff --git a/src/main/java/com/dd/plist/ASCIILocationInformation.java b/src/main/java/com/dd/plist/ASCIILocationInformation.java new file mode 100644 index 0000000..b0f5b63 --- /dev/null +++ b/src/main/java/com/dd/plist/ASCIILocationInformation.java @@ -0,0 +1,46 @@ +package com.dd.plist; + +/** + * Information about the location of an NSObject within an ASCII property list file. + * @author Daniel Dreibrodt + */ +public class ASCIILocationInformation extends LocationInformation { + private final int offset; + private final int lineNo; + private final int column; + + ASCIILocationInformation(int offset, int lineNo, int column) { + this.offset = offset; + this.lineNo = lineNo; + this.column = column; + } + + /** + * Gets the offset of the NSObject inside the file. + * @return The offset of the NSObject. + */ + public int getOffset() { + return this.offset; + } + + /** + * Gets the line number. + * @return The line number, starting at 1. + */ + public int getLineNumber() { + return this.lineNo; + } + + /** + * Gets the column number. + * @return The column, starting at 1. + */ + public int getColumnNumber() { + return this.column; + } + + @Override + public String getDescription() { + return "Line: " + this.lineNo + ", Column: " + this.column + ", Offset: " + this.offset; + } +} diff --git a/src/main/java/com/dd/plist/ASCIIPropertyListParser.java b/src/main/java/com/dd/plist/ASCIIPropertyListParser.java index 69f1dd4..cf901fa 100644 --- a/src/main/java/com/dd/plist/ASCIIPropertyListParser.java +++ b/src/main/java/com/dd/plist/ASCIIPropertyListParser.java @@ -38,9 +38,9 @@ /** *

- * 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. + * 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. *

*

* Resources on ASCII property list format: @@ -58,797 +58,1032 @@ */ public final class ASCIIPropertyListParser { - public static final char WHITESPACE_SPACE = ' '; - public static final char WHITESPACE_TAB = '\t'; - public static final char WHITESPACE_NEWLINE = '\n'; - public static final char WHITESPACE_CARRIAGE_RETURN = '\r'; - - public static final char ARRAY_BEGIN_TOKEN = '('; - public static final char ARRAY_END_TOKEN = ')'; - public static final char ARRAY_ITEM_DELIMITER_TOKEN = ','; - - public static final char DICTIONARY_BEGIN_TOKEN = '{'; - public static final char DICTIONARY_END_TOKEN = '}'; - public static final char DICTIONARY_ASSIGN_TOKEN = '='; - public static final char DICTIONARY_ITEM_DELIMITER_TOKEN = ';'; - - public static final char QUOTEDSTRING_BEGIN_TOKEN = '"'; - public static final char QUOTEDSTRING_END_TOKEN = '"'; - public static final char QUOTEDSTRING_ESCAPE_TOKEN = '\\'; - - public static final char DATA_BEGIN_TOKEN = '<'; - public static final char DATA_END_TOKEN = '>'; - - public static final char DATA_BASE64_BEGIN_TOKEN = '['; - public static final char DATA_BASE64_END_TOKEN = ']'; - - public static final char DATA_GSOBJECT_BEGIN_TOKEN = '*'; - public static final char DATA_GSDATE_BEGIN_TOKEN = 'D'; - public static final char DATA_GSBOOL_BEGIN_TOKEN = 'B'; - public static final char DATA_GSBOOL_TRUE_TOKEN = 'Y'; - public static final char DATA_GSBOOL_FALSE_TOKEN = 'N'; - public static final char DATA_GSINT_BEGIN_TOKEN = 'I'; - public static final char DATA_GSREAL_BEGIN_TOKEN = 'R'; - - public static final char DATE_DATE_FIELD_DELIMITER = '-'; - public static final char DATE_TIME_FIELD_DELIMITER = ':'; - public static final char DATE_GS_DATE_TIME_DELIMITER = ' '; - public static final char DATE_APPLE_DATE_TIME_DELIMITER = 'T'; - public static final char DATE_APPLE_END_TOKEN = 'Z'; - - public static final char COMMENT_BEGIN_TOKEN = '/'; - public static final char MULTILINE_COMMENT_SECOND_TOKEN = '*'; - public static final char SINGLELINE_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; - - /** - * 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); - } + /** + * 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); } - /** - * 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); - } + 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"; } - /** - * 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); - } + 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; + } - 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"; - } + for (int i = 0; i < sequence.length; i++) { + if (this.data[this.index + i] != sequence[i]) { + return false; + } + } - 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()); + 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; } + } } - /** - * 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; + 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(); + } - for (int i = 0; i < sequence.length; i++) { - if (this.data[this.index + i] != sequence[i]) { - return false; - } - } + 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); + } - 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; - } - } + 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); } - 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 new ParseException(excString.toString(), this.index); + 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; + } } - /** - * 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 new ParseException( - this.index < this.data.length - ? "Expected '" + expectedSymbol + "' but found '" + this.data[this.index] + "'" - : "Expected '" + expectedSymbol + "' but reached end of input", - this.index); - } + if (result != null) { + result.setLocationInformation(loc); } - /** - * 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; - } - - /** - * 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.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.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 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 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 + } - return strBuf.toString(); + this.skipWhitespacesAndComments(); } - /** - * 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.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); - } + //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 Unicode byte order mark (BOM) - if (this.data[0] == '\uFEFF') { - this.skip(1); + //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); } - this.skipWhitespacesAndComments(); - this.expect(DICTIONARY_BEGIN_TOKEN, ARRAY_BEGIN_TOKEN, COMMENT_BEGIN_TOKEN); + //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 { - return this.parseObject(); - } catch (ArrayIndexOutOfBoundsException ex) { - throw new ParseException("Reached end of input unexpectedly.", this.index); + 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(); } - /** - * 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 { - switch (this.data[this.index]) { - case ARRAY_BEGIN_TOKEN: { - return this.parseArray(); - } - case DICTIONARY_BEGIN_TOKEN: { - return this.parseDictionary(); - } - case DATA_BEGIN_TOKEN: { - return this.parseData(); - } - 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 { - return new NSDate(quotedString); - } catch (Exception ex) { - //not a date? --> return string - return new NSString(quotedString); - } - } else { - return new NSString(quotedString); - } - } - default: { - //0-9 - if (this.data[this.index] >= '0' && this.data[this.index] <= '9') { - //could be a date or just a string - return this.parseDateString(); - } else { - //non-numerical -> string or boolean - return new NSString(this.parseString()); - } - } - } + 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 + } } - /** - * 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 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(); + 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(); + } - //parse end token - this.read(ARRAY_END_TOKEN); - return new NSArray(objects.toArray(new NSObject[0])); + if (escapeSequenceHandler != null) { + escapeSequenceHandler.handleEndOfString(); } - /** - * 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(); - //skip end token - this.skip(); + return stringBuilder.toString(); + } - return dict; - } + private ParseException createParseException(String message) { + return this.createParseException(message, this.index); + } - /** - * 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 { - 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 new ParseException("The NSNumber object has an invalid format.", this.index); - } - } - - // 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(); - - int dataStartIndex = this.index; - String dataString = this.readInputUntil(DATA_BASE64_END_TOKEN); - - try { - obj = new NSData(dataString); - } - catch (IOException e) { - throw new ParseException("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 { - int dataStartIndex = this.index; - 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 new ParseException("The NSData object contains non-hexadecimal characters.", dataStartIndex); - } - - bytes[bi] = (byte) (nibble1 << 4 | nibble2); - } - - obj = new NSData(bytes); - - // skip DATA_END_TOKEN - this.skip(); - } + private ParseException createParseException(String message, int index) { + return new ParseException( + message + " (" + this.lineNo + ":" + (index - this.lineBeginning) + ")", + index); + } - 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 a date but just a string - } - } + private class EscapeSequenceHandler { - 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 { - //Skip begin token - this.skip(); - StringBuilder stringBuilder = new StringBuilder(); - boolean unescapedBackslash = true; - //Read from opening quotation marks to closing quotation marks and skip escaped quotation marks - while (this.data[this.index] != QUOTEDSTRING_END_TOKEN || (this.data[this.index - 1] == QUOTEDSTRING_ESCAPE_TOKEN && unescapedBackslash)) { - stringBuilder.append(this.data[this.index]); - if (this.accept(QUOTEDSTRING_ESCAPE_TOKEN)) { - unescapedBackslash = !(this.data[this.index - 1] == QUOTEDSTRING_ESCAPE_TOKEN && unescapedBackslash); - } - - this.skip(); - } + private final int startIndex; + private final StringBuilder stringBuilder; + private int unicodeReferenceRadix = 0; + private StringBuilder unicodeReference; - String unescapedString; - try { - unescapedString = parseQuotedString(stringBuilder.toString()); - } catch (ParseException ex) { - throw new ParseException(ex.getMessage(), this.index - stringBuilder.length() - 1 + ex.getErrorOffset()); - } catch (Exception ex) { - throw new ParseException("A quoted string could not be parsed.", this.index); - } + public EscapeSequenceHandler(StringBuilder stringBuilder) { + this.startIndex = ASCIIPropertyListParser.this.index; + this.stringBuilder = stringBuilder; + } - //skip end token - this.skip(); + 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); + } + } - return unescapedString; - } - - /** - * Parses a string according to the format specified for ASCII property lists. - * Such strings can contain escape sequences which are unescaped in this method. - * - * @param s The escaped string according to the ASCII property list format, without leading and trailing quotation marks. - * @return The unescaped string in UTF-8 - * @throws ParseException The string contains an invalid escape sequence. - */ - private static synchronized String parseQuotedString(String s) throws ParseException { - StringBuilder result = new StringBuilder(); - - StringCharacterIterator iterator = new StringCharacterIterator(s); - char c = iterator.current(); - - while (iterator.getIndex() < iterator.getEndIndex()) { - if (c == '\\') { - //An escaped sequence is following - result.append(parseEscapedSequence(iterator)); - } else { - //a normal UTF-8 char - result.append(c); - } - - c = iterator.next(); - } + 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); + } - //Build string - return result.toString(); - } - - /** - * Unescapes an escaped character sequence, e.g. \\u00FC. - * - * @param iterator The string character iterator pointing to the first character after the backslash - * @return The unescaped character. - * @throws ParseException The string contains an invalid escape sequence. - */ - private static char parseEscapedSequence(StringCharacterIterator iterator) throws ParseException { - char c = iterator.next(); - switch (c) { - case '\\': - case '"': - case '\'': - return c; - case 'b': - return '\b'; - case 'n': - return '\n'; - case 'r': - return '\r'; - case 't': - return '\t'; - - case 'U': - case 'u': { - //4 digit hex Unicode value - String unicodeValue = new String(new char[]{iterator.next(), iterator.next(), iterator.next(), iterator.next()}); - try { - return (char) Integer.parseInt(unicodeValue, 16); - } catch (NumberFormatException ex) { - throw new ParseException("The property list contains a string with an invalid escape sequence: \\" + c + unicodeValue, iterator.getIndex() - 4); - } - } - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': { - //3 digit octal ASCII value - String num = new String(new char[]{c, iterator.next(), iterator.next()}); - try { - return (char) Integer.parseInt(num, 8); - } catch (NumberFormatException ex) { - throw new ParseException("The property list contains a string with an invalid escape sequence: \\" + num, iterator.getIndex() - 2); - } - } - - default: - throw new ParseException("The property list contains a string with an invalid escape sequence: \\" + c, iterator.getIndex()); - } + 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; } + } } diff --git a/src/main/java/com/dd/plist/BinaryLocationInformation.java b/src/main/java/com/dd/plist/BinaryLocationInformation.java new file mode 100644 index 0000000..5bd8425 --- /dev/null +++ b/src/main/java/com/dd/plist/BinaryLocationInformation.java @@ -0,0 +1,37 @@ +package com.dd.plist; + +/** + * Information about the location of an NSObject within a binary property list file. + * @author Daniel Dreibrodt + */ +public class BinaryLocationInformation extends LocationInformation { + + private final int id; + private final int offset; + + BinaryLocationInformation(int id, int offset) { + this.id = id; + this.offset = offset; + } + + /** + * Gets the ID of the NSObject. + * @return The ID of the NSObject. + */ + public int getId() { + return this.id; + } + + /** + * Gets the offset of the NSObject inside the file. + * @return The offset of the NSObject. + */ + public int getOffset() { + return this.offset; + } + + @Override + public String getDescription() { + return "Object ID: " + this.id + ", Offset: " + this.offset; + } +} diff --git a/src/main/java/com/dd/plist/BinaryPropertyListParser.java b/src/main/java/com/dd/plist/BinaryPropertyListParser.java index b20e2b6..afb84fa 100644 --- a/src/main/java/com/dd/plist/BinaryPropertyListParser.java +++ b/src/main/java/com/dd/plist/BinaryPropertyListParser.java @@ -258,9 +258,8 @@ public static byte[] copyOfRange(byte[] src, int startIndex, int endIndex) { * @param data The binary 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 PropertyListFormatException When the property list's format could not be parsed. - * @throws java.io.UnsupportedEncodingException If a {@link NSString} object could not be decoded. */ - private NSObject doParse(byte[] data) throws PropertyListFormatException, UnsupportedEncodingException { + private NSObject doParse(byte[] data) throws PropertyListFormatException { Objects.requireNonNull(data); if (data.length < 8) { throw new PropertyListFormatException("The available binary property list data is too short."); @@ -318,10 +317,9 @@ private NSObject doParse(byte[] data) throws PropertyListFormatException, Unsupp * @param stack The stack to keep track of parsed objects and detect cyclic references. * @param obj The object ID. * @return The parsed object. - * @throws PropertyListFormatException When the property list's format could not be parsed. - * @throws java.io.UnsupportedEncodingException If a {@link NSString} object could not be decoded. + * @throws PropertyListFormatException When the property list's format could not be parsed. */ - private NSObject parseObject(ParsedObjectStack stack, int obj) throws PropertyListFormatException, UnsupportedEncodingException { + private NSObject parseObject(ParsedObjectStack stack, int obj) throws PropertyListFormatException { stack = stack.push(obj); if (this.parsedObjects.containsKey(obj)) { @@ -329,56 +327,78 @@ private NSObject parseObject(ParsedObjectStack stack, int obj) throws PropertyLi } int offset = this.getObjectOffset(obj); + LocationInformation loc = new BinaryLocationInformation(obj, offset); + byte type = this.bytes[offset]; int objType = (type & 0xF0) >> 4; int objInfo = type & 0x0F; NSObject result; - switch (objType) { - case SIMPLE_TYPE: - result = this.parseSimpleObject(offset, objInfo, objType, obj); - break; - case INT_TYPE: - result = this.parseNumber(offset, objInfo, NSNumber.INTEGER); - break; - case REAL_TYPE: - result = this.parseNumber(offset, objInfo, NSNumber.REAL); - break; - case DATE_TYPE: - result = this.parseDate(offset, objInfo); - break; - case DATA_TYPE: - result = this.parseData(offset, objInfo); - break; - case ASCII_STRING_TYPE: - result = this.parseString(offset, objInfo, (o, l) -> l, StandardCharsets.US_ASCII.name()); - break; - case UTF16_STRING_TYPE: - // UTF-16 characters can have variable length, but the Core Foundation reference implementation - // assumes 2 byte characters, thus only covering the Basic Multilingual Plane - result = this.parseString(offset, objInfo, (o, l) -> 2 * l, StandardCharsets.UTF_16BE.name()); - break; - case UTF8_STRING_TYPE: - // UTF-8 characters can have variable length, so we need to calculate the byte length dynamically - // by reading the UTF-8 characters one by one - result = this.parseString(offset, objInfo, this::calculateUtf8StringLength, StandardCharsets.UTF_8.name()); - break; - case UID_TYPE: - result = this.parseUid(obj, offset, objInfo + 1); - break; - case ARRAY_TYPE: - result = this.parseArray(offset, objInfo, stack); - break; - case ORDERED_SET_TYPE: - result = this.parseSet(offset, objInfo, true, stack); - break; - case SET_TYPE: - result = this.parseSet(offset, objInfo, false, stack); - break; - case DICTIONARY_TYPE: - result = this.parseDictionary(offset, objInfo, stack); - break; - default: - throw new PropertyListFormatException("The given binary property list contains an object of unknown type (" + objType + ")"); + try { + switch (objType) { + case SIMPLE_TYPE: + result = this.parseSimpleObject(offset, objInfo, obj); + break; + case INT_TYPE: + result = this.parseNumber(offset, objInfo, NSNumber.INTEGER); + break; + case REAL_TYPE: + result = this.parseNumber(offset, objInfo, NSNumber.REAL); + break; + case DATE_TYPE: + result = this.parseDate(offset, objInfo); + break; + case DATA_TYPE: + result = this.parseData(offset, objInfo); + break; + case ASCII_STRING_TYPE: + result = this.parseString(offset, objInfo, (o, l) -> l, + StandardCharsets.US_ASCII.name()); + break; + case UTF16_STRING_TYPE: + // UTF-16 characters can have variable length, but the Core Foundation reference implementation + // assumes 2 byte characters, thus only covering the Basic Multilingual Plane + result = this.parseString(offset, objInfo, (o, l) -> 2 * l, + StandardCharsets.UTF_16BE.name()); + break; + case UTF8_STRING_TYPE: + // UTF-8 characters can have variable length, so we need to calculate the byte length dynamically + // by reading the UTF-8 characters one by one + result = this.parseString(offset, objInfo, this::calculateUtf8StringLength, + StandardCharsets.UTF_8.name()); + break; + case UID_TYPE: + result = this.parseUid(obj, offset, objInfo + 1); + break; + case ARRAY_TYPE: + result = this.parseArray(offset, objInfo, stack); + break; + case ORDERED_SET_TYPE: + result = this.parseSet(offset, objInfo, true, stack); + break; + case SET_TYPE: + result = this.parseSet(offset, objInfo, false, stack); + break; + case DICTIONARY_TYPE: + result = this.parseDictionary(offset, objInfo, stack); + break; + default: + throw new PropertyListFormatException(this.buildTypeError(offset)); + } + } catch (PropertyListFormatException ex) { + if (ex.getLocationInformation() == null) { + ex.setLocationInformation(loc); + } + + throw ex; + } catch (java.io.UnsupportedEncodingException ex) { + throw new PropertyListFormatException( + "The encoding of the NSString at offset " + offset + " is not supported.", + loc, + ex); + } + + if (result != null) { + result.setLocationInformation(loc); } this.parsedObjects.put(obj, result); @@ -387,11 +407,11 @@ private NSObject parseObject(ParsedObjectStack stack, int obj) throws PropertyLi private NSDate parseDate(int offset, int objInfo) throws PropertyListFormatException { if (objInfo != 0x3) { - throw new PropertyListFormatException("The given binary property list contains a date object of an unknown type (" + objInfo + ")"); + throw new PropertyListFormatException(this.buildTypeError(offset, "NSDate")); } if (offset + 9 > this.bytes.length) { - throw new PropertyListFormatException("The given binary property list contains a date object longer than the amount of available data."); + throw new PropertyListFormatException(buildLengthError(offset, "NSDate")); } return new NSDate(this.bytes, offset + 1, offset + 9); @@ -402,13 +422,13 @@ private NSData parseData(int offset, int objInfo) throws PropertyListFormatExcep int length = lengthAndOffset[0]; int dataOffset = offset + lengthAndOffset[1]; if (dataOffset + length > this.bytes.length) { - throw new PropertyListFormatException("The given binary property list contains a data object longer than the amount of available data."); + throw new PropertyListFormatException(buildLengthError(offset, "NSData")); } return new NSData(copyOfRange(this.bytes, dataOffset, dataOffset + length)); } - private NSObject parseSimpleObject(int offset, int objInfo, int objType, int obj) throws PropertyListFormatException { + private NSObject parseSimpleObject(int offset, int objInfo, int obj) throws PropertyListFormatException { switch (objInfo) { case 0x0: //null object (v1.0 and later) return null; @@ -419,17 +439,17 @@ private NSObject parseSimpleObject(int offset, int objInfo, int objType, int obj case 0xC: // URL with no base URL (v1.0 and later) case 0xD: // URL with base URL (v1.0 and later) //TODO Implement binary URL parsing (not implemented in Core Foundation) - throw new PropertyListFormatException("The given binary property list contains a URL object. This object type is not supported."); + throw new PropertyListFormatException("The NSObject at offset " + offset + " is a URL, which is not supported."); case 0xE: //16-byte UUID (v1.0 and later) return this.parseUid(obj, offset, 16); default: - throw new PropertyListFormatException("The given binary property list contains an object of unknown type (" + objType + ")"); + throw new PropertyListFormatException(this.buildTypeError(offset)); } } private UID parseUid(int obj, int offset, int length) throws PropertyListFormatException { if (offset + 1 + length >= this.bytes.length) { - throw new PropertyListFormatException("The given property list contains an UID larger than the amount of available data."); + throw new PropertyListFormatException(buildLengthError(offset, "UID")); } return new UID(String.valueOf(obj), copyOfRange(this.bytes, offset + 1, offset + 1 + length)); @@ -441,7 +461,7 @@ private NSNumber parseNumber(int offset, int objInfo, int integer) throws Proper try { return new NSNumber(this.bytes, offset + 1, offset + 1 + length, integer); } catch (IndexOutOfBoundsException ex) { - throw new PropertyListFormatException("The given property list contains an NSNumber with a length larger than the amount of available data.", ex); + throw new PropertyListFormatException(buildLengthError(offset, "NSNumber"), ex); } } @@ -450,7 +470,7 @@ private NSString parseString(int offset, int objInfo, BiFunction this.bytes.length) { - throw new PropertyListFormatException("The given binary property list contains an NSString that is larger than the amount of available data."); + throw new PropertyListFormatException(buildLengthError(offset, "NSString")); } return new NSString(this.bytes, strOffset, strOffset + length, charsetName); @@ -499,11 +519,10 @@ private NSDictionary parseDictionary(int offset, int objInfo, ParsedObjectStack int valRef = this.parseObjectReferenceFromList(valueListOffset, i); NSObject key = this.parseObject(stack, keyRef); if (key == null) { - throw new PropertyListFormatException("The given binary property list contains a dictionary with an invalid NULL key."); + throw new PropertyListFormatException("The key #" + (i + 1) + " of the NSDictionary at offset " + offset + " is NULL."); } NSObject val = this.parseObject(stack, valRef); - dict.put(key.toString(), val); } return dict; @@ -531,7 +550,7 @@ private int[] readLengthAndOffset(int objInfo, int offset) throws PropertyListFo return new int[]{lengthValue, offsetValue}; } catch (IllegalArgumentException | IndexOutOfBoundsException ex) { - throw new PropertyListFormatException("The given binary property list contains an invalid length/offset integer at offset " + offset, ex); + throw new PropertyListFormatException("The length/offset integer at offset " + offset + " is invalid.", ex); } } @@ -589,7 +608,7 @@ private int parseObjectReferenceFromList(int baseOffset, int objectIndex) throws private int parseObjectReference(int offset) throws PropertyListFormatException { if (offset + this.objectRefSize >= this.bytes.length) { - throw new PropertyListFormatException("The given property list contains an incomplete object reference at offset " + offset + "."); + throw new PropertyListFormatException("Encountered the end of the file while parsing the object reference at offset " + offset + "."); } return (int) parseUnsignedInt(this.bytes, offset, offset + this.objectRefSize); @@ -597,11 +616,26 @@ private int parseObjectReference(int offset) throws PropertyListFormatException private int getObjectOffset(int obj) throws PropertyListFormatException { if (obj >= this.numObjects) { - throw new PropertyListFormatException("The given binary property list contains an invalid object identifier."); + throw new PropertyListFormatException("The given binary property list contains an invalid object identifier (" + obj + ")."); } int startOffset = this.offsetTableOffset + obj * this.offsetSize; return (int) parseUnsignedInt(this.bytes, startOffset, startOffset + this.offsetSize); } + + private String buildTypeError(int offset) { + return this.buildTypeError(offset, "NSObject"); + } + + private String buildTypeError(int offset, String objectType) { + return String.format("The %s at offset %d has an unknown or unsupported type (0x%02x)", + objectType, offset, this.bytes[offset]); + } + + private static String buildLengthError(int offset, String objectType) { + return String.format( + "The length of the %s at offset %d is larger than the amount of available data.", + objectType, offset); + } } diff --git a/src/main/java/com/dd/plist/LocationInformation.java b/src/main/java/com/dd/plist/LocationInformation.java new file mode 100644 index 0000000..80f8eb6 --- /dev/null +++ b/src/main/java/com/dd/plist/LocationInformation.java @@ -0,0 +1,46 @@ +/* + * plist - An open source library to parse and generate property lists + * Copyright (C) 2024 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; + +/** + * Information about the location of an NSObject within the parsed property list file. + * @author Daniel Dreibrodt + */ +public abstract class LocationInformation { + + /** + * Gets a description of the object location. + * @return The location description. + */ + public abstract String getDescription(); + + /** + * Returns the description of the object location. + * @return A description of the object location. + * @see LocationInformation#getDescription() + */ + @Override + public String toString() { + return this.getDescription(); + } +} diff --git a/src/main/java/com/dd/plist/NSObject.java b/src/main/java/com/dd/plist/NSObject.java index ac7e9f6..8741d44 100644 --- a/src/main/java/com/dd/plist/NSObject.java +++ b/src/main/java/com/dd/plist/NSObject.java @@ -39,7 +39,7 @@ public abstract class NSObject implements Cloneable, Comparable { * This constant will be different depending on the operating system on * which you use this library. */ - final static String NEWLINE = System.getProperty("line.separator"); + final static String NEWLINE = System.lineSeparator(); /** * The maximum length of the text lines to be used when generating @@ -54,6 +54,25 @@ public abstract class NSObject implements Cloneable, Comparable { */ private final static String INDENT = "\t"; + private LocationInformation locationInformation; + + /** + * Gets information about the location of this NSObject within the parsed property list, + * if available. + * @return The location information, or {@code null} if it is not available. + */ + public LocationInformation getLocationInformation() { + return this.locationInformation; + } + + /** + * Sets the location information. + * @param locationInformation The location information. + */ + void setLocationInformation(LocationInformation locationInformation) { + this.locationInformation = locationInformation; + } + /** * Creates and returns a deep copy of this instance. * @return A clone of this instance. diff --git a/src/main/java/com/dd/plist/PropertyListFormatException.java b/src/main/java/com/dd/plist/PropertyListFormatException.java index d6f29ff..715260e 100644 --- a/src/main/java/com/dd/plist/PropertyListFormatException.java +++ b/src/main/java/com/dd/plist/PropertyListFormatException.java @@ -30,12 +30,17 @@ */ public class PropertyListFormatException extends Exception { + /** + * The location of the object that caused the exception. + */ + private LocationInformation locationInformation; + /** * Creates a new exception with the given message. * @param message A message containing information about the nature of the exception. */ public PropertyListFormatException(String message) { - super(message); + this(message, (LocationInformation) null); } /** @@ -44,6 +49,39 @@ public PropertyListFormatException(String message) { * @param cause The original exception that caused this exception. */ public PropertyListFormatException(String message, Throwable cause) { + this(message, null, cause); + } + + /** + * Creates a new exception with the given message. + * @param message A message containing information about the nature of the exception. + * @param locationInformation The location of the element causing the exception. + */ + PropertyListFormatException(String message, LocationInformation locationInformation) { + super(message); + this.locationInformation = locationInformation; + } + + /** + * Creates a new exception with the given message. + * @param message A message containing information about the nature of the exception. + * @param locationInformation The location of the element causing the exception. + * @param cause The original exception that caused this exception. + */ + PropertyListFormatException(String message, LocationInformation locationInformation, Throwable cause) { super(message, cause); + this.locationInformation = locationInformation; + } + + /** + * The location of the element that caused the exception, if available. + * @return The location information, if available. + */ + public LocationInformation getLocationInformation() { + return this.locationInformation; + } + + void setLocationInformation(LocationInformation locationInformation) { + this.locationInformation = locationInformation; } } diff --git a/src/main/java/com/dd/plist/XMLLocationFilter.java b/src/main/java/com/dd/plist/XMLLocationFilter.java new file mode 100644 index 0000000..d13e6d4 --- /dev/null +++ b/src/main/java/com/dd/plist/XMLLocationFilter.java @@ -0,0 +1,39 @@ +package com.dd.plist; + +import org.xml.sax.*; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.XMLFilterImpl; + +/** + * XML Filter that stores the location of nodes in custom attributes. + * + * @author Daniel Dreibrodt + */ +class XMLLocationFilter extends XMLFilterImpl { + public static final String NS = "https://github.com/3breadt/dd-plist/"; + public static final String LINE_NUMBER = "LINE_NUMBER"; + public static final String COLUMN_NUMBER = "COLUMN_NUMBER"; + + private Locator locator = null; + + XMLLocationFilter(XMLReader xmlReader) { + super(xmlReader); + } + + @Override + public void setDocumentLocator(Locator locator) { + super.setDocumentLocator(locator); + this.locator = locator; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + AttributesImpl enhancedAttributes = new AttributesImpl(attributes); + enhancedAttributes.addAttribute( + NS, LINE_NUMBER, "dd:" + LINE_NUMBER, "CDATA", String.valueOf(this.locator.getLineNumber())); + enhancedAttributes.addAttribute( + NS, COLUMN_NUMBER, "dd:" + COLUMN_NUMBER, "CDATA", String.valueOf(this.locator.getColumnNumber())); + super.startElement(uri, localName, qName, enhancedAttributes); + } + +} diff --git a/src/main/java/com/dd/plist/XMLLocationInformation.java b/src/main/java/com/dd/plist/XMLLocationInformation.java new file mode 100644 index 0000000..70ae35d --- /dev/null +++ b/src/main/java/com/dd/plist/XMLLocationInformation.java @@ -0,0 +1,96 @@ +package com.dd.plist; + +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.Locator; + +/** + * Information about the location of an NSObject within an XML property list file. + *

+ * Line and column number are only available if the {@code withLineInformation} parameter in the call to one of the + * {@link XMLPropertyListParser}'s parse methods was set to {@code true}. The line information is taken from the + * {@link Locator} class and thus is only an approximation of the actual location that can only be used + * for diagnostic purposes. + *

+ * @author Daniel Dreibrodt + */ +public class XMLLocationInformation extends LocationInformation { + + private final String xpath; + private int lineNo = -1; + private int column = -1; + + XMLLocationInformation(Node n, String xpath) { + this.xpath = xpath; + + if (n.hasAttributes()) { + NamedNodeMap attrs = n.getAttributes(); + Node lineNumberNode = attrs.getNamedItemNS(XMLLocationFilter.NS, XMLLocationFilter.LINE_NUMBER); + if (lineNumberNode != null) { + try { + this.lineNo = Integer.parseInt(lineNumberNode.getNodeValue()); + } catch (NumberFormatException ignored) { + // Invalid location information should not abort parsing + } + } + + Node colNumberNode = attrs.getNamedItemNS(XMLLocationFilter.NS, XMLLocationFilter.COLUMN_NUMBER); + if (colNumberNode != null) { + try { + this.column = Integer.parseInt(colNumberNode.getNodeValue()); + } catch (NumberFormatException ignored) { + // Invalid location information should not abort parsing + } + } + } + } + + /** + * Gets the XPath of the XML node that is the source for the NSObject. + * + * @return The XPath. + */ + public String getXPath() { + return this.xpath; + } + + /** + * Gets a value indicating whether line and column number are available. + * + * @return {@code true}, if both line and column number are available; {@code false}, otherwise. + */ + public boolean hasLineInformation() { + return this.lineNo > 0 && this.column > 0; + } + + /** + * Gets the line number of the end of the XML node's start tag, if available. + * + * @return The line number (starting at 1), or -1 if the line number is not available. + * @see Locator#getLineNumber() + * @see XMLLocationInformation#hasLineInformation() + */ + public int getLineNumber() { + return this.lineNo; + } + + /** + * Gets the column number of the end of the XML node's start tag, if available. + * + * @return The column (starting at 1), or -1 if the column is not available. + * @see Locator#getColumnNumber() + * @see XMLLocationInformation#hasLineInformation() + */ + public int getColumnNumber() { + return this.column; + } + + @Override + public String getDescription() { + if (this.hasLineInformation()) { + return "Line: " + this.lineNo + ", Column: " + this.column + ", XPath: " + this.xpath; + } + + return "XPath: " + this.xpath; + } +} diff --git a/src/main/java/com/dd/plist/XMLPropertyListParser.java b/src/main/java/com/dd/plist/XMLPropertyListParser.java index 0067592..50bb009 100644 --- a/src/main/java/com/dd/plist/XMLPropertyListParser.java +++ b/src/main/java/com/dd/plist/XMLPropertyListParser.java @@ -22,18 +22,6 @@ */ package com.dd.plist; -import org.w3c.dom.Document; -import org.w3c.dom.DocumentType; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.w3c.dom.Text; -import org.xml.sax.EntityResolver; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -41,9 +29,26 @@ import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import javax.xml.XMLConstants; +import javax.xml.parsers.*; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.sax.SAXSource; + +import org.w3c.dom.Document; +import org.w3c.dom.DocumentType; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; /** * Parses XML property lists. @@ -107,13 +112,12 @@ public static DocumentBuilder getDocBuilder() throws ParserConfigurationExceptio * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list * could not be created. This should not occur. * @throws java.io.IOException If any I/O error occurs while reading the file. - * @throws org.xml.sax.SAXException If any parse error occurs. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. - * @throws java.text.ParseException If a number or date string could not be parsed. * @see javax.xml.parsers.DocumentBuilder#parse(java.io.File) */ public static NSObject parse(File f) - throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException { + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { return parse(f.toPath()); } @@ -125,13 +129,12 @@ public static NSObject parse(File f) * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list * could not be created. This should not occur. * @throws java.io.IOException If any I/O error occurs while reading the file. - * @throws org.xml.sax.SAXException If any parse error occurs. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. - * @throws java.text.ParseException If a number or date string could not be parsed. * @see javax.xml.parsers.DocumentBuilder#parse(java.io.File) */ public static NSObject parse(Path path) - throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException { + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { try (InputStream fileInputStream = Files.newInputStream(path)) { return parse(fileInputStream); } @@ -145,12 +148,11 @@ public static NSObject parse(Path path) * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list * could not be created. This should not occur. * @throws java.io.IOException If any I/O error occurs while reading the file. - * @throws org.xml.sax.SAXException If any parse error occurs. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. - * @throws java.text.ParseException If a number or date string could not be parsed. */ public static NSObject parse(final byte[] bytes) - throws ParserConfigurationException, ParseException, SAXException, PropertyListFormatException, IOException { + throws ParserConfigurationException, SAXException, PropertyListFormatException, IOException { try (InputStream inputStream = new ByteArrayInputStream(bytes)) { return parse(inputStream); } @@ -165,16 +167,15 @@ public static NSObject parse(final byte[] bytes) * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list * could not be created. This should not occur. * @throws java.io.IOException If any I/O error occurs while reading the file. - * @throws org.xml.sax.SAXException If any parse error occurs. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. - * @throws java.text.ParseException If a number or date string could not be parsed. * @see javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream) */ public static NSObject parse(InputStream is) - throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException { + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { // Do not pass BOM to XML parser because it can't handle it InputStream filteredInputStream = new ByteOrderMarkFilterInputStream(is, false); - return parse(getDocBuilder().parse(filteredInputStream)); + return parse(parseXml(new InputSource(filteredInputStream), false)); } /** @@ -186,14 +187,117 @@ public static NSObject parse(InputStream is) * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list * could not be created. This should not occur. * @throws java.io.IOException If any I/O error occurs while reading the file. - * @throws org.xml.sax.SAXException If any parse error occurs. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. - * @throws java.text.ParseException If a number or date string could not be parsed. * @see javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream) */ public static NSObject parse(Reader reader) - throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException, ParseException { - return parse(getDocBuilder().parse(new InputSource(reader))); + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { + return parse(parseXml(new InputSource(reader), false)); + } + + + /** + * Parses an XML property list file. + * + * @param f The XML property list file. + * @param withLineInformation If set to {@code true}, the parser will try to collect line information and store it + * in the parsed object's location information + * (See {@link NSObject#getLocationInformation()}). + * @return The root object of the property list. This is usually a {@link NSDictionary} but can also be a {@link NSArray}. + * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list + * could not be created. This should not occur. + * @throws java.io.IOException If any I/O error occurs while reading the file. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. + * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. + * @see javax.xml.parsers.DocumentBuilder#parse(java.io.File) + */ + public static NSObject parse(File f, boolean withLineInformation) + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { + return parse(f.toPath(), withLineInformation); + } + + /** + * Parses an XML property list file. + * + * @param path The XML property list file path. + * @param withLineInformation If set to {@code true}, the parser will try to collect line information and store it + * in the parsed object's location information + * @return The root object of the property list. This is usually a {@link NSDictionary} but can also be a {@link NSArray}. + * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list + * could not be created. This should not occur. + * @throws java.io.IOException If any I/O error occurs while reading the file. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. + * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. + * @see javax.xml.parsers.DocumentBuilder#parse(java.io.File) + */ + public static NSObject parse(Path path, boolean withLineInformation) + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { + try (InputStream fileInputStream = Files.newInputStream(path)) { + return parse(fileInputStream, withLineInformation); + } + } + + /** + * Parses an XML property list from a byte array. + * + * @param bytes The byte array containing the property list's data. + * @param withLineInformation If set to {@code true}, the parser will try to collect line information and store it + * in the parsed object's location information + * @return The root object of the property list. This is usually a {@link NSDictionary} but can also be a {@link NSArray}. + * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list + * could not be created. This should not occur. + * @throws java.io.IOException If any I/O error occurs while reading the file. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. + * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. + */ + public static NSObject parse(final byte[] bytes, boolean withLineInformation) + throws ParserConfigurationException, SAXException, PropertyListFormatException, IOException { + try (InputStream inputStream = new ByteArrayInputStream(bytes)) { + return parse(inputStream, withLineInformation); + } + } + + /** + * Parses an XML property list from an input stream. + * This method does not close the specified input stream. + * + * @param is The input stream pointing to the property list's data. + * @param withLineInformation If set to {@code true}, the parser will try to collect line information and store it + * in the parsed object's location information + * @return The root object of the property list. This is usually a {@link NSDictionary} but can also be a {@link NSArray}. + * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list + * could not be created. This should not occur. + * @throws java.io.IOException If any I/O error occurs while reading the file. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. + * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. + * @see javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream) + */ + public static NSObject parse(InputStream is, boolean withLineInformation) + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { + // Do not pass BOM to XML parser because it can't handle it + InputStream filteredInputStream = new ByteOrderMarkFilterInputStream(is, false); + return parse(parseXml(new InputSource(filteredInputStream), withLineInformation)); + } + + /** + * Parses an XML property list from a {@link Reader}. + * This method does not close the specified reader. + * + * @param reader The reader providing the property list's data. + * @param withLineInformation If set to {@code true}, the parser will try to collect line information and store it + * in the parsed object's location information + * @return The root object of the property list. This is usually a {@link NSDictionary} but can also be a {@link NSArray}. + * @throws javax.xml.parsers.ParserConfigurationException If a document builder for parsing an XML property list + * could not be created. This should not occur. + * @throws java.io.IOException If any I/O error occurs while reading the file. + * @throws org.xml.sax.SAXException If any XML parsing error occurs. + * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. + * @see javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream) + */ + public static NSObject parse(Reader reader, boolean withLineInformation) + throws ParserConfigurationException, IOException, SAXException, PropertyListFormatException { + return parse(parseXml(new InputSource(reader), withLineInformation)); } /** @@ -203,9 +307,8 @@ public static NSObject parse(Reader reader) * @return The root NSObject of the property list contained in the XML document. * @throws java.io.IOException If any I/O error occurs while reading the file. * @throws com.dd.plist.PropertyListFormatException If the given property list has an invalid format. - * @throws java.text.ParseException If a number or date string could not be parsed. */ - public static NSObject parse(Document doc) throws PropertyListFormatException, IOException, ParseException { + public static NSObject parse(Document doc) throws PropertyListFormatException, IOException { DocumentType docType = doc.getDoctype(); if (docType == null) { if (!doc.getDocumentElement().getNodeName().equals("plist")) { @@ -215,9 +318,11 @@ public static NSObject parse(Document doc) throws PropertyListFormatException, I throw new PropertyListFormatException("The given XML document is not a property list."); } + String xpath; Node rootNode; if (doc.getDocumentElement().getNodeName().equals("plist")) { + xpath = "/plist"; //Root element wrapped in plist tag List rootNodes = filterElementNodes(doc.getDocumentElement().getChildNodes()); if (rootNodes.isEmpty()) { @@ -230,9 +335,50 @@ public static NSObject parse(Document doc) throws PropertyListFormatException, I } else { //Root NSObject not wrapped in plist-tag rootNode = doc.getDocumentElement(); + xpath = ""; + } + + return parseObject(rootNode, xpath + "/" + rootNode.getNodeName()); + } + + private static Document parseXml(InputSource inputSource, boolean withLineInformation) throws IOException, SAXException, ParserConfigurationException { + if (withLineInformation) { + XMLReader xmlReader = createSafeXmlReader(); + + XMLLocationFilter locationFilter = new XMLLocationFilter(xmlReader); + SAXSource saxSource = new SAXSource(locationFilter, inputSource); + + DOMResult domResult = new DOMResult(); + try { + Transformer transformer = createSafeTransformer(); + transformer.transform(saxSource, domResult); + } catch (TransformerException e) { + throw new IOException(e.getMessage(), e); + } + + return (Document) domResult.getNode(); + } else { + return getDocBuilder().parse(inputSource); } + } - return parseObject(rootNode); + private static XMLReader createSafeXmlReader() throws SAXException, ParserConfigurationException { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + parserFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + parserFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + parserFactory.setXIncludeAware(false); + + SAXParser parser = parserFactory.newSAXParser(); + return parser.getXMLReader(); + } + + private static Transformer createSafeTransformer() throws TransformerConfigurationException { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + return transformerFactory.newTransformer(); } /** @@ -240,52 +386,78 @@ public static NSObject parse(Document doc) throws PropertyListFormatException, I * * @param n The XML node. * @return The corresponding NSObject. - * @throws java.io.IOException If any I/O error occurs while parsing a Base64 encoded NSData object. - * @throws java.text.ParseException If a number or date string could not be parsed. + * @throws PropertyListFormatException A parsing error occurred. */ - private static NSObject parseObject(Node n) throws ParseException, IOException { + private static NSObject parseObject(Node n, String xpath) throws PropertyListFormatException { String type = n.getNodeName(); - switch (type) { - case "dict": { - NSDictionary dict = new NSDictionary(); - List children = filterElementNodes(n.getChildNodes()); - for (int i = 0; i < children.size(); i += 2) { - Node key = children.get(i); - Node val = children.get(i + 1); + XMLLocationInformation loc = new XMLLocationInformation(n, xpath); + NSObject parsedObject = null; + try { + switch (type) { + case "dict": { + NSDictionary dict = new NSDictionary(); + parsedObject = dict; - String keyString = getNodeTextContents(key); + List children = filterElementNodes(n.getChildNodes()); + for (int i = 0; i < children.size(); i += 2) { + Node key = children.get(i); + String keyString = getNodeTextContents(key); - dict.put(keyString, parseObject(val)); + Node value = children.get(i + 1); + String childPath = xpath + "/*[" + (1 + i + 1) + "]"; + dict.put(keyString, parseObject(value, childPath)); + } + + break; } - return dict; - } - case "array": { - List children = filterElementNodes(n.getChildNodes()); - NSArray array = new NSArray(children.size()); - for (int i = 0; i < children.size(); i++) { - array.setValue(i, parseObject(children.get(i))); + case "array": { + List children = filterElementNodes(n.getChildNodes()); + NSArray array = new NSArray(children.size()); + parsedObject = array; + + for (int i = 0; i < children.size(); i++) { + String childPath = xpath + "/*[" + (i + 1) + "]"; + array.setValue(i, parseObject(children.get(i), childPath)); + } + + break; } - return array; + case "true": + parsedObject = new NSNumber(true); + break; + case "false": + parsedObject = new NSNumber(false); + break; + case "integer": + case "real": + parsedObject = new NSNumber(getNodeTextContents(n)); + break; + case "string": + parsedObject = new NSString(getNodeTextContents(n)); + break; + case "data": + parsedObject = new NSData(getNodeTextContents(n)); + break; + case "date": + parsedObject = new NSDate(getNodeTextContents(n)); + break; } - case "true": - return new NSNumber(true); - case "false": - return new NSNumber(false); - case "integer": - case "real": - try { - return new NSNumber(getNodeTextContents(n)); - } catch (IllegalArgumentException ex) { - throw new ParseException("The NSNumber object has an invalid format.", -1); - } - case "string": - return new NSString(getNodeTextContents(n)); - case "data": - return new NSData(getNodeTextContents(n)); - case "date": - return new NSDate(getNodeTextContents(n)); + } catch (PropertyListFormatException ex) { + throw ex; + } catch (Exception ex) { + throw new PropertyListFormatException( + loc.hasLineInformation() + ? ("The " + n.getNodeName() + " node at line " + loc.getLineNumber() + " and column " + loc.getColumnNumber() + " could not be parsed.") + : ("The " + n.getNodeName() + " node at " + xpath + " could not be parsed."), + loc, + ex); } - return null; + + if (parsedObject != null) { + parsedObject.setLocationInformation(loc); + } + + return parsedObject; } /** diff --git a/src/test/java/com/dd/plist/test/ASCIIPropertyListParserTest.java b/src/test/java/com/dd/plist/test/ASCIIPropertyListParserTest.java new file mode 100644 index 0000000..3c3d489 --- /dev/null +++ b/src/test/java/com/dd/plist/test/ASCIIPropertyListParserTest.java @@ -0,0 +1,265 @@ +package com.dd.plist.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.dd.plist.ASCIILocationInformation; +import com.dd.plist.ASCIIPropertyListParser; +import com.dd.plist.NSArray; +import com.dd.plist.NSData; +import com.dd.plist.NSDate; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSNumber; +import com.dd.plist.NSObject; +import com.dd.plist.NSString; +import com.dd.plist.PropertyListParser; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.function.BiConsumer; +import org.junit.jupiter.api.Test; + +/** + * Tests for the {@link ASCIIPropertyListParser} class. + * @author Daniel Dreibrodt + */ +public class ASCIIPropertyListParserTest { + @Test + public void parse_canParseAppleFormat() throws Exception { + NSObject x = PropertyListParser.parse(new File("test-files/test1-ascii.plist")); + NSDictionary d = assertInstanceOf(NSDictionary.class, x); + assertEquals(5, d.count()); + assertEquals("valueA", d.objectForKey("keyA").toString()); + assertEquals("value&B", d.objectForKey("key&B").toString()); + assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); + assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), + new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); + NSArray a = (NSArray) d.objectForKey("array"); + assertEquals(4, a.count()); + assertEquals(a.objectAtIndex(0), new NSString("YES")); + assertEquals(a.objectAtIndex(1), new NSString("NO")); + assertEquals(a.objectAtIndex(2), new NSString("87")); + assertEquals(a.objectAtIndex(3), new NSString("3.14159")); + } + + @Test + public void parse_providesCorrectObjectLocationsForAppleFormat() throws Exception { + File file = new File("test-files/test1-ascii.plist"); + String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.US_ASCII); + List lines = Files.readAllLines(file.toPath()); + + BiConsumer locationChecker = (NSObject object, String token) -> { + ASCIILocationInformation location = assertInstanceOf(ASCIILocationInformation.class, + object.getLocationInformation()); + assertEquals(text.indexOf(token), location.getOffset()); + assertEquals(lines.get(location.getLineNumber() - 1).indexOf(token), + location.getColumnNumber() - 1); + }; + + NSObject x = PropertyListParser.parse(file); + NSDictionary d = (NSDictionary) x; + locationChecker.accept(d, "{"); + locationChecker.accept(d.get("keyA"), "valueA"); + locationChecker.accept(d.get("key&B"), "\"value&\\U0042\""); + locationChecker.accept(d.get("date"), "\"2011-11-28T09:21:30Z\""); + locationChecker.accept(d.get("data"), "<00000004 10410820 82>"); + NSArray array = assertInstanceOf(NSArray.class, d.get("array")); + locationChecker.accept(array, "("); + locationChecker.accept(array.objectAtIndex(0), "YES"); + locationChecker.accept(array.objectAtIndex(1), "NO"); + locationChecker.accept(array.objectAtIndex(2), "87"); + locationChecker.accept(array.objectAtIndex(3), "3.14159"); + } + + @Test + public void parse_providesCorrectObjectLocationsWhenAdditionalLineBreaksArePresent() throws Exception { + File file = new File("test-files/test1-ascii-multiline-handling.plist"); + String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.US_ASCII); + List lines = Files.readAllLines(file.toPath()); + + BiConsumer locationChecker = (NSObject object, String token) -> { + ASCIILocationInformation location = assertInstanceOf(ASCIILocationInformation.class, + object.getLocationInformation()); + assertEquals( + text.indexOf(token), + location.getOffset(), + "Incorrect location of " + object + ": " + location); + assertEquals( + lines.get(location.getLineNumber() - 1).indexOf(token), + location.getColumnNumber() - 1, + "Incorrect location of " + object + ": " + location); + }; + + NSObject x = PropertyListParser.parse(file); + NSDictionary d = (NSDictionary) x; + locationChecker.accept(d, "{"); + locationChecker.accept(d.get("keyA"), "valueA"); + locationChecker.accept(d.get("key&B"), "\"Multi"); + locationChecker.accept(d.get("date"), "\"2011-11-28T09:21:30Z\""); + locationChecker.accept(d.get("data"), "<00000004"); + NSArray array = assertInstanceOf(NSArray.class, d.get("array")); + locationChecker.accept(array, "("); + locationChecker.accept(array.objectAtIndex(0), "YES"); + locationChecker.accept(array.objectAtIndex(1), "NO"); + locationChecker.accept(array.objectAtIndex(2), "87"); + locationChecker.accept(array.objectAtIndex(3), "3.14159"); + } + + @Test + public void parse_canParseGnuStepFormat() throws Exception { + NSObject x = PropertyListParser.parse(new File("test-files/test1-ascii-gnustep.plist")); + NSDictionary d = assertInstanceOf(NSDictionary.class, x); + assertEquals(5, d.count()); + assertEquals("valueA", d.objectForKey("keyA").toString()); + assertEquals("value&B", d.objectForKey("key&B").toString()); + assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); + assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), + new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); + NSArray a = (NSArray) d.objectForKey("array"); + assertEquals(4, a.count()); + assertEquals(a.objectAtIndex(0), new NSNumber(true)); + assertEquals(a.objectAtIndex(1), new NSNumber(false)); + assertEquals(a.objectAtIndex(2), new NSNumber(87)); + assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); + } + + @Test + public void parse_providesCorrectObjectLocationsForGnuStepFormat() throws Exception { + File file = new File("test-files/test1-ascii-gnustep.plist"); + String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.US_ASCII); + List lines = Files.readAllLines(file.toPath()); + + BiConsumer locationChecker = (NSObject object, String token) -> { + ASCIILocationInformation location = assertInstanceOf(ASCIILocationInformation.class, + object.getLocationInformation()); + assertEquals(text.indexOf(token), location.getOffset()); + assertEquals(lines.get(location.getLineNumber() - 1).indexOf(token), + location.getColumnNumber() - 1); + }; + + NSObject x = PropertyListParser.parse(file); + NSDictionary d = (NSDictionary) x; + locationChecker.accept(d, "{"); + locationChecker.accept(d.get("keyA"), "valueA"); + locationChecker.accept(d.get("key&B"), "\"value&\\U0042\""); + locationChecker.accept(d.get("date"), "<*D2011-11-28 09:21:30 +0000>"); + locationChecker.accept(d.get("data"), "<00000004 10410820 82>"); + NSArray array = assertInstanceOf(NSArray.class, d.get("array")); + locationChecker.accept(array, "("); + locationChecker.accept(array.objectAtIndex(0), "<*BY>"); + locationChecker.accept(array.objectAtIndex(1), "<*BN>"); + locationChecker.accept(array.objectAtIndex(2), "<*I87>"); + locationChecker.accept(array.objectAtIndex(3), "<*R3.14159>"); + } + + @Test + public void parse_canHandleGnuStepBase64Data() throws Exception { + byte[] expectedData = new byte[]{(byte) 0xAA, (byte) 0xAA, (byte) 0xBB, (byte) 0xBB, + (byte) 0xCC, (byte) 0xCC}; + + NSObject x = PropertyListParser.parse(new File("test-files/test1-ascii-gnustep-base64.plist")); + NSDictionary d = assertInstanceOf(NSDictionary.class, x); + assertArrayEquals(expectedData, ((NSData) d.objectForKey("data")).bytes()); + } + + @Test + public void parse_rejectsAsciiNullCharactersInString() { + assertThrows(ParseException.class, () -> PropertyListParser.parse( + new File("test-files/test2-ascii-null-char-in-string.plist"))); + } + + @Test + public void parse_canHandleUtf8Encoding() throws Exception { + this.testAsciiUnicode("test-ascii-utf-8.plist"); + } + + @Test + public void parse_canHandleUtf16BeEncoding() throws Exception { + this.testAsciiUnicode("test-ascii-utf-16be.plist"); + } + + @Test + public void parse_canHandleUtf16LeEncoding() throws Exception { + this.testAsciiUnicode("test-ascii-utf-16le.plist"); + } + + @Test + public void parse_canHandleUtf32BeEncoding() throws Exception { + this.testAsciiUnicode("test-ascii-utf-32be.plist"); + } + + @Test + public void parse_canHandleUtf32LeEncoding() throws Exception { + this.testAsciiUnicode("test-ascii-utf-32le.plist"); + } + + @Test + public void parse_canHandleComments() throws Exception { + String stringFileContentStr = "/* Menu item to make the current document plain text */\n" + + "\"Make Plain Text\" = \"In reinen Text umwandeln\";\n" + + "/* Menu item to make the current document rich text */\n" + + "\"Make Rich Text\" = \"In formatierten Text umwandeln\";\n"; + byte[] stringFileContentRaw = stringFileContentStr.getBytes(); + + String stringFileContent = new String(stringFileContentRaw, StandardCharsets.UTF_8); + String asciiPropertyList = "{" + stringFileContent + "}"; + NSDictionary dict = (NSDictionary) ASCIIPropertyListParser.parse( + asciiPropertyList.getBytes(StandardCharsets.UTF_8)); + assertTrue(dict.containsKey("Make Plain Text")); + assertEquals("In reinen Text umwandeln", dict.get("Make Plain Text").toString()); + } + + @Test + public void parse_canHandleEscapedCharacters() throws Exception { + String asciiPropertyList = "{\n" + + "a = \"abc \\n def\";\n" + + "b = \"\\r\";\n" + + "c = \"xyz\\b\";\n" + + "d = \"\\tasdf\";\n" + + "e = \"\\\\ \\\"\";\n" + + "f = \"a \\' b\";\n" + + "g = \"\\u07F7\";" + + "h = \"\\775\";" + + "}"; + NSDictionary dict = (NSDictionary) ASCIIPropertyListParser.parse( + asciiPropertyList.getBytes(StandardCharsets.US_ASCII)); + assertEquals("abc \n def", dict.get("a").toString()); + assertEquals("\r", dict.get("b").toString()); + assertEquals("xyz\b", dict.get("c").toString()); + assertEquals("\tasdf", dict.get("d").toString()); + assertEquals("\\ \"", dict.get("e").toString()); + assertEquals("a ' b", dict.get("f").toString()); + assertEquals("߷", dict.get("g").toString()); + assertEquals("ǽ", dict.get("h").toString()); + } + + @Test + public void parse_canHandleIncompleteEscapeSequence() { + String asciiPropertyList = "{\n" + + "a = \"\\u123\";\n" + + "}"; + + ParseException ex = assertThrows(ParseException.class, () -> ASCIIPropertyListParser.parse( + asciiPropertyList.getBytes(StandardCharsets.US_ASCII))); + assertEquals(asciiPropertyList.indexOf('\\'), ex.getErrorOffset()); + } + + private void testAsciiUnicode(String filename) throws Exception { + // contains BOM, encoding shall be automatically detected + NSDictionary dict = (NSDictionary) ASCIIPropertyListParser.parse( + new File("test-files/" + filename)); + assertEquals(6, dict.count()); + assertEquals("JÔÖú@2x.jpg", dict.objectForKey("path").toString()); + assertEquals("QÔÖú@2x 啕.jpg", dict.objectForKey("Key QÔÖª@2x 䌡").toString()); + assertEquals("もじれつ", dict.get("quoted").toString()); + assertEquals("クオート無し", dict.get("not_quoted").toString()); + assertEquals("\"\\\":\n拡張文字キタアアア", dict.get("with_escapes").toString()); + assertEquals(" 幸", dict.get("with_u_escapes").toString()); + } +} diff --git a/src/test/java/com/dd/plist/test/ASCIIPropertyListWriterTest.java b/src/test/java/com/dd/plist/test/ASCIIPropertyListWriterTest.java new file mode 100644 index 0000000..6e73c88 --- /dev/null +++ b/src/test/java/com/dd/plist/test/ASCIIPropertyListWriterTest.java @@ -0,0 +1,41 @@ +package com.dd.plist.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.dd.plist.ASCIIPropertyListWriter; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSObject; +import com.dd.plist.PropertyListParser; +import java.io.File; +import org.junit.jupiter.api.Test; + +/** + * Tests for the {@link ASCIIPropertyListWriter} class. + * @author Daniel Dreibrodt + */ +public class ASCIIPropertyListWriterTest { + @Test + public void write_canWriteAppleFormat() throws Exception { + File in = new File("test-files/test1.plist"); + File out = new File("test-files/out-test1-ascii.plist"); + File in2 = new File("test-files/test1-ascii.plist"); + NSDictionary x = (NSDictionary) PropertyListParser.parse(in); + ASCIIPropertyListWriter.write(x, out); + + //Information gets lost when saving into the ASCII format (NSNumbers are converted to NSStrings) + + NSDictionary y = (NSDictionary) PropertyListParser.parse(out); + NSDictionary z = (NSDictionary) PropertyListParser.parse(in2); + assertEquals(y, z); + } + + @Test + public void writeGnuStep_canWriteGnuStepFormat() throws Exception { + File in = new File("test-files/test1.plist"); + File out = new File("test-files/out-test1-ascii-gnustep.plist"); + NSDictionary x = (NSDictionary) PropertyListParser.parse(in); + ASCIIPropertyListWriter.writeGnuStep(x, out); + NSObject y = PropertyListParser.parse(out); + assertEquals(x, y); + } +} diff --git a/src/test/java/com/dd/plist/test/BinaryPropertyListParserTest.java b/src/test/java/com/dd/plist/test/BinaryPropertyListParserTest.java new file mode 100644 index 0000000..71f1cb9 --- /dev/null +++ b/src/test/java/com/dd/plist/test/BinaryPropertyListParserTest.java @@ -0,0 +1,99 @@ +package com.dd.plist.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import com.dd.plist.BinaryLocationInformation; +import com.dd.plist.BinaryPropertyListParser; +import com.dd.plist.NSArray; +import com.dd.plist.NSData; +import com.dd.plist.NSDate; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSNumber; +import com.dd.plist.NSObject; +import com.dd.plist.PropertyListParser; +import java.io.File; +import java.util.Date; +import org.junit.jupiter.api.Test; + +/** + * Tests for the {@link BinaryPropertyListParser} class. + * @author Daniel Dreibrodt + */ +public class BinaryPropertyListParserTest { + @Test + public void parse_canParseBinaryPropertyList() throws Exception { + NSObject x = PropertyListParser.parse(new File("test-files/test1-binary.plist")); + + // check the data in it + NSDictionary d = assertInstanceOf(NSDictionary.class, x); + assertEquals(5, d.count()); + assertEquals("valueA", d.objectForKey("keyA").toString()); + assertEquals("value&B", d.objectForKey("key&B").toString()); + assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); + assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), + new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); + NSArray a = (NSArray) d.objectForKey("array"); + assertEquals(4, a.count()); + assertEquals(a.objectAtIndex(0), new NSNumber(true)); + assertEquals(a.objectAtIndex(1), new NSNumber(false)); + assertEquals(a.objectAtIndex(2), new NSNumber(87)); + assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); + } + + @Test + public void parse_providesCorrectObjectLocations() throws Exception { + NSObject x = PropertyListParser.parse(new File("test-files/test1-binary.plist")); + + NSDictionary d = (NSDictionary) x; + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, d.getLocationInformation()).getId(), + 0); + // each dictionary key is serialized as an NSObject, as we have 5 keys, the next value object has ID 6 + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + d.get("keyA").getLocationInformation()).getId(), + 6); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + d.get("key&B").getLocationInformation()).getId(), + 7); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + d.get("date").getLocationInformation()).getId(), + 8); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + d.get("data").getLocationInformation()).getId(), + 9); + NSArray array = assertInstanceOf(NSArray.class, d.get("array")); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, array.getLocationInformation()).getId(), + 10); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + array.objectAtIndex(0).getLocationInformation()).getId(), + 11); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + array.objectAtIndex(1).getLocationInformation()).getId(), + 12); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + array.objectAtIndex(2).getLocationInformation()).getId(), + 13); + assertEquals( + assertInstanceOf(BinaryLocationInformation.class, + array.objectAtIndex(3).getLocationInformation()).getId(), + 14); + } + + @Test + public void parse_canHandleNumbersWithInfinityValue() throws Exception { + NSDictionary dict = (NSDictionary) BinaryPropertyListParser.parse( + new File("test-files/infinity-binary.plist")); + assertEquals(Double.POSITIVE_INFINITY, ((NSNumber) dict.get("a")).doubleValue()); + assertEquals(Double.NEGATIVE_INFINITY, ((NSNumber) dict.get("b")).doubleValue()); + } +} diff --git a/src/test/java/com/dd/plist/test/BinaryPropertyListWriterTest.java b/src/test/java/com/dd/plist/test/BinaryPropertyListWriterTest.java new file mode 100644 index 0000000..994ca07 --- /dev/null +++ b/src/test/java/com/dd/plist/test/BinaryPropertyListWriterTest.java @@ -0,0 +1,27 @@ +package com.dd.plist.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.dd.plist.BinaryPropertyListWriter; +import com.dd.plist.NSObject; +import com.dd.plist.PropertyListParser; +import com.dd.plist.XMLPropertyListParser; +import java.io.File; +import org.junit.jupiter.api.Test; + +/** + * Tests for the {@link BinaryPropertyListWriter} class. + * @author Daniel Dreibrodt + */ +public class BinaryPropertyListWriterTest { + @Test + public void write_canWriteBinaryPropertyList() throws Exception { + NSObject x = XMLPropertyListParser.parse(new File("test-files/test1.plist")); + + // save and load as binary + BinaryPropertyListWriter.write(x, new File("test-files/out-testBinary.plist")); + NSObject y = PropertyListParser.parse(new File("test-files/out-testBinary.plist")); + assertEquals(x, y); + } + +} diff --git a/src/test/java/com/dd/plist/test/IssueTest.java b/src/test/java/com/dd/plist/test/IssueTest.java index 887e206..b7f265e 100644 --- a/src/test/java/com/dd/plist/test/IssueTest.java +++ b/src/test/java/com/dd/plist/test/IssueTest.java @@ -181,7 +181,7 @@ public void testIssue76_UnexpectedIllegalArgumentExceptionForInvalidNumberInAsci @Test public void testIssue76_UnexpectedIllegalArgumentExceptionForInvalidNumberInXmlPropertyList() { File plistFile = new File("test-files/github-issue76-xml.plist"); - assertThrows(ParseException.class, () -> PropertyListParser.parse(plistFile)); + assertThrows(PropertyListFormatException.class, () -> PropertyListParser.parse(plistFile)); } @Test @@ -209,7 +209,7 @@ public void testIssue88_providesCorrectErrorIndex() throws Exception { int errorOffset = parseException.getErrorOffset(); String unparseableString = fileContents.substring(errorOffset, errorOffset + 2); assertTrue(unparseableString.startsWith("\\")); - assertTrue(parseException.getMessage().endsWith(unparseableString)); + assertTrue(parseException.getMessage().contains(unparseableString)); } @ParameterizedTest diff --git a/src/test/java/com/dd/plist/test/NSNumberTest.java b/src/test/java/com/dd/plist/test/NSNumberTest.java index 66b7b62..1f52427 100644 --- a/src/test/java/com/dd/plist/test/NSNumberTest.java +++ b/src/test/java/com/dd/plist/test/NSNumberTest.java @@ -13,67 +13,66 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Tests for the NSString class. - * + * Tests for the {@link NSNumber} class. * @author Daniel Dreibrodt */ public class NSNumberTest { @Test - public void testInitializeFromNanString() { + public void init_canHandleNaNString() { NSNumber nan = new NSNumber("nan"); assertTrue(Double.isNaN(nan.doubleValue())); } @Test - public void testInitializeFromDoubleNaN() { + public void init_canHandleNaNDouble() { NSNumber nan = new NSNumber(Double.NaN); assertTrue(Double.isNaN(nan.doubleValue())); } @Test - public void testInitializeFromFloatNaN() { + public void init_canHandleNaNFloat() { NSNumber nan = new NSNumber(Float.NaN); assertTrue(Double.isNaN(nan.doubleValue())); } @Test - public void testNanIntValue() { + public void intValue_throwsIllegalStateExceptionForNaN() { NSNumber nan = new NSNumber(Double.NaN); assertThrows(IllegalStateException.class, nan::intValue); } @Test - public void testNanLongValue() { + public void longValue_throwsIllegalStateExceptionForNaN() { NSNumber nan = new NSNumber(Double.NaN); assertThrows(IllegalStateException.class, nan::longValue); } @Test - public void testNanFloatValue() { + public void floatValue_canHandleNaN() { NSNumber nan = new NSNumber(Double.NaN); assertTrue(Float.isNaN(nan.floatValue())); } @Test - public void testNanStringValue() { + public void stringValue_canHandleNaN() { NSNumber nan = new NSNumber(Double.NaN); assertEquals("nan", nan.stringValue()); } @Test - public void testNanToXml() { + public void toXML_canHandleNaN() { NSNumber nan = new NSNumber(Double.NaN); assertThat(nan.toXMLPropertyList(), containsString("nan")); } @Test - public void testNanToString() { + public void toString_canHandleNaN() { NSNumber nan = new NSNumber(Double.NaN); assertEquals("nan", nan.toString()); } @Test - public void testNanToBinaryAndBack() throws Exception { + public void toBinary_canHandleNaN() throws Exception { NSDictionary dict = new NSDictionary(); dict.put("NaN", new NSNumber(Double.NaN)); @@ -91,61 +90,61 @@ public void testNanToBinaryAndBack() throws Exception { } @Test - public void testInitializeFromPositiveInfinityString() { + public void init_canHandlePositiveInfinityString() { NSNumber inf = new NSNumber("+infinity"); assertEquals(Double.POSITIVE_INFINITY, inf.doubleValue()); } @Test - public void testInitializeFromDoublePositiveInfinity() { + public void init_canHandlePositiveInfinityDouble() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertEquals(Double.POSITIVE_INFINITY, inf.doubleValue()); } @Test - public void testInitializeFromFloatPositiveInfinity() { + public void init_canHandlePositiveInfinityFloat() { NSNumber inf = new NSNumber(Float.POSITIVE_INFINITY); assertEquals(Double.POSITIVE_INFINITY, inf.doubleValue()); } @Test - public void testPositiveInfinityIntValue() { + public void intValue_throwsIllegalStateExceptionForPositiveInfinity() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertThrows(IllegalStateException.class, inf::intValue); } @Test - public void testPositiveInfinityLongValue() { + public void longValue_throwsIllegalStateExceptionForPositiveInfinity() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertThrows(IllegalStateException.class, inf::longValue); } @Test - public void testPositiveInfinityFloatValue() { + public void floatValue_canHandlePositiveInfinity() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertEquals(Float.POSITIVE_INFINITY, inf.floatValue()); } @Test - public void testPositiveInfinityStringValue() { + public void stringValue_canHandlePositiveInfinity() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertEquals("+infinity", inf.stringValue()); } @Test - public void testPositiveInfinityToXml() { + public void toXml_canHandlePositiveInfinity() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertThat(inf.toXMLPropertyList(), containsString("+infinity")); } @Test - public void testPositiveInfinityToString() { + public void toString_canHandlePositiveInfinity() { NSNumber inf = new NSNumber(Double.POSITIVE_INFINITY); assertEquals("+infinity", inf.toString()); } @Test - public void testPositiveInfinityToBinaryAndBack() throws Exception { + public void toBinary_canHandlePositiveInfinity() throws Exception { NSDictionary dict = new NSDictionary(); dict.put("inf", new NSNumber(Double.POSITIVE_INFINITY)); @@ -163,61 +162,61 @@ public void testPositiveInfinityToBinaryAndBack() throws Exception { } @Test - public void testInitializeFromNegativeInfinityString() { + public void init_canHandleNegativeInfinityString() { NSNumber inf = new NSNumber("-infinity"); assertEquals(Double.NEGATIVE_INFINITY, inf.doubleValue()); } @Test - public void testInitializeFromDoubleNegativeInfinity() { + public void init_canHandleNegativeInfinityDouble() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertEquals(Double.NEGATIVE_INFINITY, inf.doubleValue()); } @Test - public void testInitializeFromFloatNegativeInfinity() { + public void init_canHandleNegativeInfinityFloat() { NSNumber inf = new NSNumber(Float.NEGATIVE_INFINITY); assertEquals(Double.NEGATIVE_INFINITY, inf.doubleValue()); } @Test - public void testNegativeInfinityIntValue() { + public void intValue_throwsIllegalStateExceptionForNegativeInfinity() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertThrows(IllegalStateException.class, inf::intValue); } @Test - public void testNegativeInfinityLongValue() { + public void longValue_throwsIllegalStateExceptionForNegativeInfinity() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertThrows(IllegalStateException.class, inf::longValue); } @Test - public void testNegativeInfinityFloatValue() { + public void floatValue_canHandleNegativeInfinity() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertEquals(Float.NEGATIVE_INFINITY, inf.floatValue()); } @Test - public void testNegativeInfinityStringValue() { + public void stringValue_canHandleNegativeInfinity() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertEquals("-infinity", inf.stringValue()); } @Test - public void testNegativeInfinityToXml() { + public void toXml_canHandleNegativeInfinity() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertThat(inf.toXMLPropertyList(), containsString("-infinity")); } @Test - public void testNegativeInfinityToString() { + public void toString_canHandleNegativeInfinity() { NSNumber inf = new NSNumber(Double.NEGATIVE_INFINITY); assertEquals("-infinity", inf.toString()); } @Test - public void testNegativeInfinityToBinaryAndBack() throws Exception { + public void toBinary_canHandleNegativeInfinity() throws Exception { NSDictionary dict = new NSDictionary(); dict.put("inf", new NSNumber(Double.NEGATIVE_INFINITY)); diff --git a/src/test/java/com/dd/plist/test/NSSetTests.java b/src/test/java/com/dd/plist/test/NSSetTest.java similarity index 87% rename from src/test/java/com/dd/plist/test/NSSetTests.java rename to src/test/java/com/dd/plist/test/NSSetTest.java index f940280..f7bb748 100644 --- a/src/test/java/com/dd/plist/test/NSSetTests.java +++ b/src/test/java/com/dd/plist/test/NSSetTest.java @@ -7,7 +7,11 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNull; -public class NSSetTests { +/** + * Tests for the {@link NSSet} class. + * @author Daniel Dreibrodt + */ +public class NSSetTest { @Test public void init_arrayContainingNull_doesNotThrow() { assertDoesNotThrow(() -> new NSSet(new NSObject[] { null })); @@ -19,7 +23,7 @@ public void init_ordered_arrayContainingNull_doesNotThrow() { } @Test - public void add_null_doesNotThrow() { + public void addObject_null_doesNotThrow() { NSSet set = new NSSet(); assertDoesNotThrow(() -> set.addObject(null)); } diff --git a/src/test/java/com/dd/plist/test/NSStringTest.java b/src/test/java/com/dd/plist/test/NSStringTest.java index 32a7e15..d64fb91 100644 --- a/src/test/java/com/dd/plist/test/NSStringTest.java +++ b/src/test/java/com/dd/plist/test/NSStringTest.java @@ -6,36 +6,36 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Tests for the NSString class. + * Tests for the {@link NSString} class. * @author Daniel Dreibrodt */ public class NSStringTest { @Test - public void testIntValuePositive() { + public void intValue_canHandlePositiveNumber() { NSString s = new NSString("42"); assertEquals(42, s.intValue()); } @Test - public void testIntValueNegative() { + public void intValue_canHandleNegativeNumber() { NSString s = new NSString("-42"); assertEquals(-42, s.intValue()); } @Test - public void testIntValueFloat() { + public void intValue_canHandleDecimalNumber() { NSString s = new NSString("42.87654"); assertEquals(42, s.intValue()); } @Test - public void testIntValueSkipsWhitespace() { + public void intValue_ignoresLeadingWhiteSpace() { NSString s = new NSString(" 42"); assertEquals(42, s.intValue()); } @Test - public void testIntValueIgnoresSubsequentCharacters() { + public void intValue_ignoresSubsequentCharacters() { NSString s = new NSString(" 42 is the meaning of life. 12345678."); assertEquals(42, s.intValue()); @@ -44,55 +44,55 @@ public void testIntValueIgnoresSubsequentCharacters() { } @Test - public void testIntValueMaxValue() { + public void intValue_canHandleMaxValue() { NSString s = new NSString("2147483647"); assertEquals(Integer.MAX_VALUE, s.intValue()); } @Test - public void testIntValueMoreThanMaxValue() { + public void intValue_canHandleMoreThanMaxValue() { NSString s = new NSString("2147483648"); assertEquals(Integer.MAX_VALUE, s.intValue()); } @Test - public void testIntValueMinValue() { + public void intValue_canHandleMinValue() { NSString s = new NSString("-2147483648"); assertEquals(Integer.MIN_VALUE, s.intValue()); } @Test - public void testIntValueLessThanMinValue() { + public void intValue_canHandleLessThanMaxValue() { NSString s = new NSString("-2147483649"); assertEquals(Integer.MIN_VALUE, s.intValue()); } @Test - public void testFloatValuePositive() { + public void floatValue_canHandlePositiveNumber() { NSString s = new NSString("42"); assertEquals(42, s.floatValue(), 0); } @Test - public void testFloatValueNegative() { + public void floatValue_canHandleNegativeNumber() { NSString s = new NSString("-42"); assertEquals(-42, s.floatValue(), 0); } @Test - public void testFloatValueFloat() { + public void floatValue_canHandleDecimalNumber() { NSString s = new NSString("42.87654"); assertEquals(42.87654, s.floatValue(), 1E-5); } @Test - public void testFloatValueSkipsWhitespace() { + public void floatValue_ignoresLeadingWhiteSpace() { NSString s = new NSString(" 42.1234"); assertEquals(42.1234, s.floatValue(), 1E-5); } @Test - public void testFloatValueIgnoresSubsequentCharacters() { + public void floatValue_ignoresSubsequentCharacters() { NSString s = new NSString(" 42.5 is the meaning of life. 12345678."); assertEquals(42.5, s.floatValue(), 0); @@ -101,55 +101,55 @@ public void testFloatValueIgnoresSubsequentCharacters() { } @Test - public void testFloatValueMaxValue() { + public void floatValue_canHandleMaxValue() { NSString s = new NSString("340282350000000000000000000000000000000"); assertEquals(Float.MAX_VALUE, s.floatValue(), 0); } @Test - public void testFloatValueMoreThanMaxValue() { + public void floatValue_canHandleMoreThanMaxValue() { NSString s = new NSString("340282350000000000000000000000000000000.1"); assertEquals(Float.MAX_VALUE, s.floatValue(), 0); } @Test - public void testFloatValueMinValue() { + public void floatValue_canHandleMinValue() { NSString s = new NSString("-340282350000000000000000000000000000000"); assertEquals(-Float.MAX_VALUE, s.floatValue(), 0); } @Test - public void testFloatValueLessThanMinValue() { + public void floatValue_canHandleLessThanMinValue() { NSString s = new NSString("-340282350000000000000000000000000000000.1"); assertEquals(-Float.MAX_VALUE, s.floatValue(), 0); } @Test - public void testDoubleValuePositive() { + public void doubleValue_canHandlePositiveNumber() { NSString s = new NSString("42"); assertEquals(42, s.doubleValue(), 0); } @Test - public void testDoubleValueNegative() { + public void doubleValue_canHandleNegativeNumber() { NSString s = new NSString("-42"); assertEquals(-42, s.doubleValue(), 0); } @Test - public void testDoubleValueFloat() { + public void doubleValue_canHandleDecimalNumber() { NSString s = new NSString("42.87654"); assertEquals(42.87654, s.doubleValue(), 1E-5); } @Test - public void testDoubleValueSkipsWhitespace() { + public void doubleValue_ignoresLeadingWhiteSpace() { NSString s = new NSString(" 42.1234"); assertEquals(42.1234, s.doubleValue(), 1E-5); } @Test - public void testDoubleValueIgnoresSubsequentCharacters() { + public void doubleValue_ignoresSubsequentCharacters() { NSString s = new NSString(" 42.5 is the meaning of life. 12345678."); assertEquals(42.5, s.doubleValue(), 0); @@ -158,7 +158,7 @@ public void testDoubleValueIgnoresSubsequentCharacters() { } @Test - public void testBoolValueRegularCases() { + public void boolValue_canHandleAllBooleanRepresentations() { assertTrue(new NSString("YES").boolValue()); assertTrue(new NSString("yes").boolValue()); assertTrue(new NSString("TRUE").boolValue()); @@ -173,7 +173,7 @@ public void testBoolValueRegularCases() { } @Test - public void testBoolValueLeadingWhitespace() { + public void boolValue_ignoresLeadingWhiteSpace() { assertTrue(new NSString(" YES").boolValue()); assertTrue(new NSString(" yes").boolValue()); assertTrue(new NSString(" TRUE").boolValue()); @@ -188,7 +188,7 @@ public void testBoolValueLeadingWhitespace() { } @Test - public void testBoolValueLeadingZeroes() { + public void boolValue_ignoresLeadingZeroes() { assertTrue(new NSString("0YES").boolValue()); assertTrue(new NSString("0yes").boolValue()); assertTrue(new NSString("0TRUE").boolValue()); @@ -203,7 +203,7 @@ public void testBoolValueLeadingZeroes() { } @Test - public void testBoolValueSign() { + public void boolValue_ignoresLeadingPlusOrMinus() { assertTrue(new NSString("+YES").boolValue()); assertTrue(new NSString("-yes").boolValue()); assertTrue(new NSString("-TRUE").boolValue()); @@ -218,7 +218,7 @@ public void testBoolValueSign() { } @Test - public void testBoolValueIntegers() { + public void boolValue_canHandleNumbers() { assertTrue(new NSString("002").boolValue()); assertTrue(new NSString("+03").boolValue()); assertTrue(new NSString(" 04").boolValue()); @@ -230,7 +230,7 @@ public void testBoolValueIntegers() { } @Test - public void testToXmlWithInvalidChars() { + public void toXml_removesIllegalCharacters() { String xml = new NSString("\2Hello\0World\3\r\n\tHow are you?\uFFFF\uFFFEI am a \ud83d\udc3b.").toXMLPropertyList(); assertEquals("HelloWorld\r\n\tHow are you?I am a \ud83d\udc3b.", getStringFromXml(xml)); } diff --git a/src/test/java/com/dd/plist/test/ParseTest.java b/src/test/java/com/dd/plist/test/ParseTest.java deleted file mode 100644 index 8461a68..0000000 --- a/src/test/java/com/dd/plist/test/ParseTest.java +++ /dev/null @@ -1,329 +0,0 @@ -package com.dd.plist.test; - -import com.dd.plist.*; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.text.ParseException; -import java.util.Date; - -import static org.junit.jupiter.api.Assertions.*; - -public class ParseTest { - /** - * Test the xml reader/writer - */ - @Test - public void testXml() throws Exception { - // parse an example plist file - NSObject x = PropertyListParser.parse(new File("test-files/test1.plist")); - - // check the data in it - NSDictionary d = (NSDictionary) x; - assertEquals(5, d.count()); - assertEquals("valueA", d.objectForKey("keyA").toString()); - assertEquals("value&B", d.objectForKey("key&B").toString()); - assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); - assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); - NSArray a = (NSArray) d.objectForKey("array"); - assertEquals(4, a.count()); - assertEquals(a.objectAtIndex(0), new NSNumber(true)); - assertEquals(a.objectAtIndex(1), new NSNumber(false)); - assertEquals(a.objectAtIndex(2), new NSNumber(87)); - assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); - - // read/write it, make sure we get the same thing - XMLPropertyListWriter.write(x, new File("test-files/out-testXml.plist")); - NSObject y = PropertyListParser.parse(new File("test-files/out-testXml.plist")); - assertEquals(x, y); - } - - /** - * Test parsing of an XML property list in UTF-16BE. - */ - @Test - public void testXmlUtf16BeWithBom() throws Exception { - this.testXmlEncoding("UTF-16BE-BOM"); - } - - /** - * Test parsing of an XML property list in UTF-16BE, but without the BOM. - */ - @Test - public void testXmlUtf16BeWithoutBom() throws Exception { - this.testXmlEncoding("UTF-16BE"); - } - - /** - * Test parsing of an XML property list in UTF-16LE. - */ - @Test - public void testXmlUtf16LeWithBom() throws Exception { - this.testXmlEncoding("UTF-16LE-BOM"); - } - - /** - * Test parsing of an XML property list in UTF-16LE, but without the BOM. - */ - @Test - public void testXmlUtf16LeWithoutBom() throws Exception { - this.testXmlEncoding("UTF-16LE"); - } - - /** - * Test parsing of an XML property list in UTF-32BE. - */ - @Test - public void testXmlUtf32BeWithBom() throws Exception { - this.testXmlEncoding("UTF-32BE-BOM"); - } - - /** - * Test parsing of an XML property list in UTF-32BE, but without the BOM. - */ - @Test - public void testXmlUtf32BeWithoutBom() throws Exception { - this.testXmlEncoding("UTF-32BE"); - } - - /** - * Test parsing of an XML property list in UTF-32LE. - */ - @Test - public void testXmlUtf32LeWithBom() throws Exception { - this.testXmlEncoding("UTF-32LE-BOM"); - } - - /** - * Test parsing of an XML property list in UTF-32LE, but without the BOM. - */ - @Test - public void testXmlUtf32LeWithoutBom() throws Exception { - this.testXmlEncoding("UTF-32LE"); - } - - private void testXmlEncoding(String encoding) throws Exception { - NSObject x = PropertyListParser.parse(new File("test-files/test-xml-" + encoding.toLowerCase() + ".plist")); - - // check the data in it - NSDictionary d = (NSDictionary) x; - assertEquals(5, d.count()); - assertEquals("valueA", d.objectForKey("keyA").toString()); - assertEquals("value&B \u2705", d.objectForKey("key&B").toString()); - assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); - assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); - NSArray a = (NSArray) d.objectForKey("array"); - assertEquals(4, a.count()); - assertEquals(a.objectAtIndex(0), new NSNumber(true)); - assertEquals(a.objectAtIndex(1), new NSNumber(false)); - assertEquals(a.objectAtIndex(2), new NSNumber(87)); - assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); - } - - @Test - public void testXmlWithInfinityValues() throws Exception { - // See https://github.com/3breadt/dd-plist/issues/83 - NSDictionary dictFromXml = (NSDictionary) XMLPropertyListParser.parse(new File("test-files/infinity-xml.plist")); - assertEquals(Double.POSITIVE_INFINITY, ((NSNumber)dictFromXml.get("a")).doubleValue()); - assertEquals(Double.NEGATIVE_INFINITY, ((NSNumber)dictFromXml.get("b")).doubleValue()); - } - - /** - * Test the binary reader/writer. - */ - @Test - public void testBinary() throws Exception { - NSObject x = PropertyListParser.parse(new File("test-files/test1.plist")); - - // save and load as binary - BinaryPropertyListWriter.write(x, new File("test-files/out-testBinary.plist")); - NSObject y = PropertyListParser.parse(new File("test-files/out-testBinary.plist")); - assertEquals(x, y); - } - - @Test - public void testBinaryWithInfinityValues() throws Exception { - NSDictionary dictFromXml = (NSDictionary) BinaryPropertyListParser.parse(new File("test-files/infinity-binary.plist")); - assertEquals(Double.POSITIVE_INFINITY, ((NSNumber)dictFromXml.get("a")).doubleValue()); - assertEquals(Double.NEGATIVE_INFINITY, ((NSNumber)dictFromXml.get("b")).doubleValue()); - } - - /** - * NSSet only occurs in binary property lists, so we have to test it separately. - * NSSets are not yet supported in reading/writing, as binary property list format v1+ is required. - */ - /*public void testSet() throws Exception { - NSSet s = new NSSet(); - s.addObject(new NSNumber(1)); - s.addObject(new NSNumber(3)); - s.addObject(new NSNumber(2)); - - NSSet orderedSet = new NSSet(true); - s.addObject(new NSNumber(1)); - s.addObject(new NSNumber(3)); - s.addObject(new NSNumber(2)); - - NSDictionary dict = new NSDictionary(); - dict.put("set1", s); - dict.put("set2", orderedSet); - - PropertyListParser.saveAsBinary(dict, new File("test-files/out-testSet.plist")); - NSObject parsedRoot = PropertyListParser.parse(new File("test-files/out-testSet.plist")); - assertTrue(parsedRoot.equals(dict)); - }*/ - - @Test - public void testASCII() throws Exception { - NSObject x = PropertyListParser.parse(new File("test-files/test1-ascii.plist")); - NSDictionary d = (NSDictionary) x; - assertEquals(5, d.count()); - assertEquals("valueA", d.objectForKey("keyA").toString()); - assertEquals("value&B", d.objectForKey("key&B").toString()); - assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); - assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); - NSArray a = (NSArray) d.objectForKey("array"); - assertEquals(4, a.count()); - assertEquals(a.objectAtIndex(0), new NSString("YES")); - assertEquals(a.objectAtIndex(1), new NSString("NO")); - assertEquals(a.objectAtIndex(2), new NSString("87")); - assertEquals(a.objectAtIndex(3), new NSString("3.14159")); - } - - @Test - public void testGnuStepASCII() throws Exception { - NSObject x = PropertyListParser.parse(new File("test-files/test1-ascii-gnustep.plist")); - NSDictionary d = (NSDictionary) x; - assertEquals(5, d.count()); - assertEquals("valueA", d.objectForKey("keyA").toString()); - assertEquals("value&B", d.objectForKey("key&B").toString()); - assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); - assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); - NSArray a = (NSArray) d.objectForKey("array"); - assertEquals(4, a.count()); - assertEquals(a.objectAtIndex(0), new NSNumber(true)); - assertEquals(a.objectAtIndex(1), new NSNumber(false)); - assertEquals(a.objectAtIndex(2), new NSNumber(87)); - assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); - } - - @Test - public void testGnuStepASCIIWithBase64() throws Exception { - byte[] expectedData = new byte[]{(byte) 0xAA, (byte) 0xAA, (byte) 0xBB, (byte) 0xBB, (byte) 0xCC, (byte) 0xCC}; - - NSObject x = PropertyListParser.parse(new File("test-files/test1-ascii-gnustep-base64.plist")); - NSDictionary d = (NSDictionary) x; - assertEquals(5, d.count()); - assertEquals("valueA", d.objectForKey("keyA").toString()); - assertEquals("value&B", d.objectForKey("key&B").toString()); - assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); - assertArrayEquals(expectedData, ((NSData) d.objectForKey("data")).bytes()); - NSArray a = (NSArray) d.objectForKey("array"); - assertEquals(4, a.count()); - assertEquals(a.objectAtIndex(0), new NSNumber(true)); - assertEquals(a.objectAtIndex(1), new NSNumber(false)); - assertEquals(a.objectAtIndex(2), new NSNumber(87)); - assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); - } - - @Test - public void testASCIIWriting() throws Exception { - File in = new File("test-files/test1.plist"); - File out = new File("test-files/out-test1-ascii.plist"); - File in2 = new File("test-files/test1-ascii.plist"); - NSDictionary x = (NSDictionary) PropertyListParser.parse(in); - ASCIIPropertyListWriter.write(x, out); - - //Information gets lost when saving into the ASCII format (NSNumbers are converted to NSStrings) - - NSDictionary y = (NSDictionary) PropertyListParser.parse(out); - NSDictionary z = (NSDictionary) PropertyListParser.parse(in2); - assertEquals(y, z); - } - - @Test - public void testGnuStepASCIIWriting() throws Exception { - File in = new File("test-files/test1.plist"); - File out = new File("test-files/out-test1-ascii-gnustep.plist"); - NSDictionary x = (NSDictionary) PropertyListParser.parse(in); - ASCIIPropertyListWriter.writeGnuStep(x, out); - NSObject y = PropertyListParser.parse(out); - assertEquals(x, y); - } - - @Test - public void testAsciiNullCharactersInString() { - assertThrows(ParseException.class, () -> PropertyListParser.parse(new File("test-files/test2-ascii-null-char-in-string.plist"))); - } - - @Test - public void testAsciiPropertyListEncodedWithUtf8() throws Exception { - this.testAsciiUnicode("test-ascii-utf-8.plist"); - } - - @Test - public void testAsciiPropertyListEncodedWithUtf16Be() throws Exception { - this.testAsciiUnicode("test-ascii-utf-16be.plist"); - } - - @Test - public void testAsciiPropertyListEncodedWithUtf16Le() throws Exception { - this.testAsciiUnicode("test-ascii-utf-16le.plist"); - } - - @Test - public void testAsciiPropertyListEncodedWithUtf32Be() throws Exception { - this.testAsciiUnicode("test-ascii-utf-32be.plist"); - } - - @Test - public void testAsciiPropertyListEncodedWithUtf32Le() throws Exception { - this.testAsciiUnicode("test-ascii-utf-32le.plist"); - } - - private void testAsciiUnicode(String filename) throws Exception { - // contains BOM, encoding shall be automatically detected - NSDictionary dict = (NSDictionary) ASCIIPropertyListParser.parse(new File("test-files/" + filename)); - assertEquals(6, dict.count()); - assertEquals("JÔÖú@2x.jpg", dict.objectForKey("path").toString()); - assertEquals("QÔÖú@2x 啕.jpg", dict.objectForKey("Key QÔÖª@2x 䌡").toString()); - assertEquals("もじれつ", dict.get("quoted").toString()); - assertEquals("クオート無し", dict.get("not_quoted").toString()); - assertEquals("\"\\\":\n拡張文字キタアアア", dict.get("with_escapes").toString()); - assertEquals("\u0020\u5e78", dict.get("with_u_escapes").toString()); - } - - @Test - public void testAsciiCommentsAreNotIncludedInStrings() throws Exception { - String stringFileContentStr = "/* Menu item to make the current document plain text */\n" + - "\"Make Plain Text\" = \"In reinen Text umwandeln\";\n" + - "/* Menu item to make the current document rich text */\n" + - "\"Make Rich Text\" = \"In formatierten Text umwandeln\";\n"; - byte[] stringFileContentRaw = stringFileContentStr.getBytes(); - - String stringFileContent = new String(stringFileContentRaw, StandardCharsets.UTF_8); - String asciiPropertyList = "{" + stringFileContent + "}"; - NSDictionary dict = (NSDictionary) ASCIIPropertyListParser.parse(asciiPropertyList.getBytes(StandardCharsets.UTF_8)); - assertTrue(dict.containsKey("Make Plain Text")); - assertEquals("In reinen Text umwandeln", dict.get("Make Plain Text").toString()); - } - - @Test - public void testAsciiEscapeCharacters() throws Exception { - String asciiPropertyList = "{\n" + - "a = \"abc \\n def\";\n" + - "b = \"\\r\";\n" + - "c = \"xyz\\b\";\n" + - "d = \"\\tasdf\";\n" + - "e = \"\\\\ \\\"\";\n" + - "f = \"a \\' b\";\n" + - "}"; - NSDictionary dict = (NSDictionary) ASCIIPropertyListParser.parse(asciiPropertyList.getBytes(StandardCharsets.UTF_8)); - assertEquals("abc \n def", dict.get("a").toString()); - assertEquals("\r", dict.get("b").toString()); - assertEquals("xyz\b", dict.get("c").toString()); - assertEquals("\tasdf", dict.get("d").toString()); - assertEquals("\\ \"", dict.get("e").toString()); - assertEquals("a ' b", dict.get("f").toString()); - } -} diff --git a/src/test/java/com/dd/plist/test/UidTests.java b/src/test/java/com/dd/plist/test/UidTest.java similarity index 94% rename from src/test/java/com/dd/plist/test/UidTests.java rename to src/test/java/com/dd/plist/test/UidTest.java index 4e09b84..f2c396b 100644 --- a/src/test/java/com/dd/plist/test/UidTests.java +++ b/src/test/java/com/dd/plist/test/UidTest.java @@ -11,10 +11,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -public class UidTests { +/** + * Tests for the {@link UID} class. + * @author Daniel Dreibrodt + */ +public class UidTest { @Test - public void constructoRejectsTooLargeValues() + public void init_throwsIllegalArgumentExceptionForMoreThan16Bytes() { byte[] data = new byte[17]; data[0] = 0x01; @@ -22,7 +26,7 @@ public void constructoRejectsTooLargeValues() } @Test - public void getBytes() { + public void getBytes_returnsMinimumPossibleLength() { assertEquals(1, new UID(null, new byte[]{0x01}).getBytes().length); assertEquals(2, new UID(null, new byte[]{0x01, 0x00}).getBytes().length); assertEquals(4, new UID(null, new byte[]{0x01, 0x00, 0x00}).getBytes().length); @@ -43,7 +47,7 @@ public void getBytes() { @ParameterizedTest @MethodSource("provideUIDs") - public void compareToAndEquals(UID a, UID b, int expectation) { + public void compareToAndEquals_worksCorrectly(UID a, UID b, int expectation) { assertEquals(expectation, a.compareTo(b), "compareTo returned unexpected value"); assertEquals(expectation * -1, b.compareTo(a), "compareTo returned unexpected value"); assertEquals(expectation == 0, a.equals(b), "equals returned unexpected value"); diff --git a/src/test/java/com/dd/plist/test/XMLPropertyListParserTest.java b/src/test/java/com/dd/plist/test/XMLPropertyListParserTest.java new file mode 100644 index 0000000..1a0afaa --- /dev/null +++ b/src/test/java/com/dd/plist/test/XMLPropertyListParserTest.java @@ -0,0 +1,153 @@ +package com.dd.plist.test; + +import com.dd.plist.*; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Date; +import java.util.function.BiConsumer; + +import static org.junit.jupiter.api.Assertions.*; + +public class XMLPropertyListParserTest { + + @Test + public void parse_canParseXmlPropertyList() throws Exception { + // parse an example plist file + NSObject x = PropertyListParser.parse(new File("test-files/test1.plist")); + + // check the data in it + NSDictionary d = assertInstanceOf(NSDictionary.class, x); + assertEquals(5, d.count()); + assertEquals("valueA", d.objectForKey("keyA").toString()); + assertEquals("value&B", d.objectForKey("key&B").toString()); + assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); + assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), + new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); + NSArray a = (NSArray) d.objectForKey("array"); + assertEquals(4, a.count()); + assertEquals(a.objectAtIndex(0), new NSNumber(true)); + assertEquals(a.objectAtIndex(1), new NSNumber(false)); + assertEquals(a.objectAtIndex(2), new NSNumber(87)); + assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); + } + + @Test + public void parse_providesCorrectObjectLocations() throws Exception { + BiConsumer locationChecker = (NSObject object, String expectedLocation) -> { + XMLLocationInformation location = assertInstanceOf(XMLLocationInformation.class, + object.getLocationInformation()); + assertEquals( + expectedLocation, + location.getXPath() + ";" + location.getLineNumber() + ":" + location.getColumnNumber()); + }; + + NSObject x = XMLPropertyListParser.parse(new File("test-files/test1.plist"), true); + NSDictionary d = (NSDictionary) x; + + locationChecker.accept(d, "/plist/dict;4:7"); + locationChecker.accept(d.get("keyA"), "/plist/dict/*[2];6:11"); + locationChecker.accept(d.get("key&B"), "/plist/dict/*[4];8:11"); + locationChecker.accept(d.get("date"), "/plist/dict/*[6];10:9"); + locationChecker.accept(d.get("data"), "/plist/dict/*[8];12:9"); + NSArray array = assertInstanceOf(NSArray.class, d.get("array")); + locationChecker.accept(array, "/plist/dict/*[10];14:10"); + locationChecker.accept(array.objectAtIndex(0), "/plist/dict/*[10]/*[1];15:12"); + locationChecker.accept(array.objectAtIndex(1), "/plist/dict/*[10]/*[2];16:13"); + locationChecker.accept(array.objectAtIndex(2), "/plist/dict/*[10]/*[3];17:14"); + locationChecker.accept(array.objectAtIndex(3), "/plist/dict/*[10]/*[4];18:11"); + } + + /** + * Test parsing of an XML property list in UTF-16BE. + */ + @Test + public void parse_canHandleUtf16BeEncoding() throws Exception { + this.testXmlEncoding("UTF-16BE-BOM"); + } + + /** + * Test parsing of an XML property list in UTF-16BE, but without the BOM. + */ + @Test + public void parse_canHandleUtf16BeEncodingWithoutBom() throws Exception { + this.testXmlEncoding("UTF-16BE"); + } + + /** + * Test parsing of an XML property list in UTF-16LE. + */ + @Test + public void parse_canHandleUtf16LeEncoding() throws Exception { + this.testXmlEncoding("UTF-16LE-BOM"); + } + + /** + * Test parsing of an XML property list in UTF-16LE, but without the BOM. + */ + @Test + public void parse_canHandleUtf16LeEncodingWithoutBom() throws Exception { + this.testXmlEncoding("UTF-16LE"); + } + + /** + * Test parsing of an XML property list in UTF-32BE. + */ + @Test + public void parse_canHandleUtf32BeEncoding() throws Exception { + this.testXmlEncoding("UTF-32BE-BOM"); + } + + /** + * Test parsing of an XML property list in UTF-32BE, but without the BOM. + */ + @Test + public void parse_canHandleUtf32BeEncodingWithoutBom() throws Exception { + this.testXmlEncoding("UTF-32BE"); + } + + /** + * Test parsing of an XML property list in UTF-32LE. + */ + @Test + public void parse_canHandleUtf32LeEncoding() throws Exception { + this.testXmlEncoding("UTF-32LE-BOM"); + } + + /** + * Test parsing of an XML property list in UTF-32LE, but without the BOM. + */ + @Test + public void parse_canHandleUtf32LeEncodingWithoutBom() throws Exception { + this.testXmlEncoding("UTF-32LE"); + } + + @Test + public void parse_canHandleNumbersWithInfinityValue() throws Exception { + // See https://github.com/3breadt/dd-plist/issues/83 + NSDictionary dictFromXml = (NSDictionary) XMLPropertyListParser.parse( + new File("test-files/infinity-xml.plist")); + assertEquals(Double.POSITIVE_INFINITY, ((NSNumber) dictFromXml.get("a")).doubleValue()); + assertEquals(Double.NEGATIVE_INFINITY, ((NSNumber) dictFromXml.get("b")).doubleValue()); + } + + private void testXmlEncoding(String encoding) throws Exception { + NSObject x = PropertyListParser.parse( + new File("test-files/test-xml-" + encoding.toLowerCase() + ".plist")); + + // check the data in it + NSDictionary d = assertInstanceOf(NSDictionary.class, x); + assertEquals(5, d.count()); + assertEquals("valueA", d.objectForKey("keyA").toString()); + assertEquals("value&B \u2705", d.objectForKey("key&B").toString()); + assertEquals(((NSDate) d.objectForKey("date")).getDate(), new Date(1322472090000L)); + assertArrayEquals(((NSData) d.objectForKey("data")).bytes(), + new byte[]{0x00, 0x00, 0x00, 0x04, 0x10, 0x41, 0x08, 0x20, (byte) 0x82}); + NSArray a = (NSArray) d.objectForKey("array"); + assertEquals(4, a.count()); + assertEquals(a.objectAtIndex(0), new NSNumber(true)); + assertEquals(a.objectAtIndex(1), new NSNumber(false)); + assertEquals(a.objectAtIndex(2), new NSNumber(87)); + assertEquals(a.objectAtIndex(3), new NSNumber(3.14159)); + } +} diff --git a/src/test/java/com/dd/plist/test/XMLPropertyListWriterTest.java b/src/test/java/com/dd/plist/test/XMLPropertyListWriterTest.java new file mode 100644 index 0000000..a13f85c --- /dev/null +++ b/src/test/java/com/dd/plist/test/XMLPropertyListWriterTest.java @@ -0,0 +1,22 @@ +package com.dd.plist.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.dd.plist.NSObject; +import com.dd.plist.PropertyListParser; +import com.dd.plist.XMLPropertyListWriter; +import java.io.File; +import org.junit.jupiter.api.Test; + +public class XMLPropertyListWriterTest { + @Test + public void write_canWriteXmlPropertyList() throws Exception { + // parse an example plist file + NSObject x = PropertyListParser.parse(new File("test-files/test1.plist")); + + // read/write it, make sure we get the same thing + XMLPropertyListWriter.write(x, new File("test-files/out-testXml.plist")); + NSObject y = PropertyListParser.parse(new File("test-files/out-testXml.plist")); + assertEquals(x, y); + } +} diff --git a/test-files/test1-ascii-multiline-handling.plist b/test-files/test1-ascii-multiline-handling.plist new file mode 100644 index 0000000..21f90c7 --- /dev/null +++ b/test-files/test1-ascii-multiline-handling.plist @@ -0,0 +1,25 @@ +{ +/* This + is + a + multi + line + comment */ + keyA + = valueA; + "key&\102" = + "Multi + line + string"; + date= + +"2011-11-28T09:21:30Z"; + + + data = <00000004 + 10410820 + 82>; + array = ( + YES, NO, + 87, 3.14159); +} \ No newline at end of file diff --git a/test-files/test1-binary.plist b/test-files/test1-binary.plist new file mode 100644 index 0000000000000000000000000000000000000000..5ee9c450c9f00974c5d2a0e7dc3bf599c7b25bdb GIT binary patch literal 145 zcmYc)$jK}&F)+Bw$i&RT%Er#Y$rX~FTIm=HB-ETjQW8s2K}2F`Vo_0IWms8aPHC!R zIG9p%GIre3+$F`pz~BkQECP-k3QbG6d3gCaIRwI$9XJ(#evod<;}DjRQ`XQkwRH;! V4GWJ1sssW?MhMNo52ayL5&%N;AI$&& literal 0 HcmV?d00001