All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.main.java.com.dd.plist.BinaryPropertyListParser Maven / Gradle / Ivy

/*
 * plist - An open source library to parse and generate property lists
 * Copyright (C) 2011 Daniel Dreibrodt
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.dd.plist;

import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.Objects;

/**
 * Parses property lists that are in Apple's binary format.
 * Use this class when you are sure about the format of the property list.
 * Otherwise, use the PropertyListParser class.
 * 

* Parsing is done by calling the static parse methods. * * @author Daniel Dreibrodt */ public final class BinaryPropertyListParser { /** * Major version of the property list format */ @SuppressWarnings("FieldCanBeLocal") //Useful when the features of different format versions are implemented private int majorVersion; /** * Minor version of the property list format */ @SuppressWarnings("FieldCanBeLocal") //Useful when the features of different format versions are implemented private int minorVersion; /** * The property list data. */ private byte[] bytes; /** * Length of an object reference in bytes */ private int objectRefSize; private int offsetSize; private int numObjects; private int offsetTableOffset; /** * Protected constructor so that instantiation is fully controlled by the * static parse methods. * * @see BinaryPropertyListParser#parse(byte[]) */ private BinaryPropertyListParser() { /* empty */ } /** * Parses a binary property list file. * * @param f The binary 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 PropertyListFormatException When the property list's format could not be parsed. * @throws java.io.IOException If a {@link NSString} object could not be decoded or an I/O error occurs on the input stream. */ public static NSObject parse(File f) throws IOException, PropertyListFormatException { return parse(f.toPath()); } /** * Parses a binary property list file. * * @param path The path to the binary 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 PropertyListFormatException When the property list's format could not be parsed. * @throws java.io.IOException If a {@link NSString} object could not be decoded or an I/O error occurs on the input stream. */ public static NSObject parse(Path path) throws IOException, PropertyListFormatException { try (InputStream fileInputStream = Files.newInputStream(path)) { return parse(fileInputStream); } } /** * Parses a binary property list from an input stream. * This method does not close the specified input stream. * * @param is The input stream that points to 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 PropertyListFormatException When the property list's format could not be parsed. * @throws java.io.IOException If a {@link NSString} object could not be decoded or an I/O error occurs on the input stream. */ public static NSObject parse(InputStream is) throws IOException, PropertyListFormatException { return parse(PropertyListParser.readAll(is)); } /** * Parses a binary property list from a byte array. * * @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. */ public static NSObject parse(byte[] data) throws PropertyListFormatException, UnsupportedEncodingException { BinaryPropertyListParser parser = new BinaryPropertyListParser(); return parser.doParse(data); } /** * Parses a binary property list from a byte array. * * @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 { Objects.requireNonNull(data); this.bytes = data; String magic = new String(copyOfRange(this.bytes, 0, 8)); if (!magic.startsWith("bplist")) { throw new IllegalArgumentException("The given data is no binary property list. Wrong magic bytes: " + magic); } this.majorVersion = magic.charAt(6) - 0x30; //ASCII number this.minorVersion = magic.charAt(7) - 0x30; //ASCII number // 0.0 - OS X Tiger and earlier // 0.1 - Leopard // 0.? - Snow Leopard // 1.5 - Lion // 2.0 - Snow Lion if (this.majorVersion > 0) { throw new PropertyListFormatException("Unsupported binary property list format: v" + this.majorVersion + "." + this.minorVersion + ". " + "Version 1.0 and later are not yet supported."); //Version 1.0+ is not even supported by OS X's own parser } if (this.bytes.length < 40 /* header + trailer length */) { throw new PropertyListFormatException("The binary property list does not contain a complete object offset table."); } // Parse trailer, last 32 bytes of the file byte[] trailer = copyOfRange(this.bytes, this.bytes.length - 32, this.bytes.length); // Trailer starts with 6 null bytes (index 0 to 5) this.offsetSize = (int) parseUnsignedInt(trailer, 6, 7); this.objectRefSize = (int) parseUnsignedInt(trailer, 7, 8); this.numObjects = (int) parseUnsignedInt(trailer, 8, 16); int topObject = (int) parseUnsignedInt(trailer, 16, 24); this.offsetTableOffset = (int) parseUnsignedInt(trailer, 24, 32); // Validate consistency of the trailer if (this.offsetTableOffset + (this.numObjects + 1) * this.offsetSize > this.bytes.length || topObject >= this.bytes.length - 32) { throw new PropertyListFormatException("The binary property list contains a corrupted object offset table."); } return this.parseObject(topObject); } /** * Parses an object inside the currently parsed binary property list. * For the format specification check * * Apple's binary property list parser implementation. * * @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. */ private NSObject parseObject(int obj) throws PropertyListFormatException, UnsupportedEncodingException { int offset = this.getObjectOffset(obj); byte type = this.bytes[offset]; int objType = (type & 0xF0) >> 4; //First 4 bits int objInfo = type & 0x0F; //Second 4 bits switch (objType) { case 0x0: { //Simple switch (objInfo) { case 0x0: { //null object (v1.0 and later) return null; } case 0x8: { //false return new NSNumber(false); } case 0x9: { //true return new NSNumber(true); } case 0xC: { //URL with no base URL (v1.0 and later) //TODO Implement binary URL parsing (not yet even implemented in Core Foundation as of revision 855.17) throw new UnsupportedOperationException("The given binary property list contains a URL object. Parsing of this object type is not yet implemented."); } case 0xD: { //URL with base URL (v1.0 and later) //TODO Implement binary URL parsing (not yet even implemented in Core Foundation as of revision 855.17) throw new UnsupportedOperationException("The given binary property list contains a URL object. Parsing of this object type is not yet implemented."); } case 0xE: { //16-byte UUID (v1.0 and later) //TODO Implement binary UUID parsing (not yet even implemented in Core Foundation as of revision 855.17) throw new UnsupportedOperationException("The given binary property list contains a UUID object. Parsing of this object type is not yet implemented."); } default: { throw new PropertyListFormatException("The given binary property list contains an object of unknown type (" + objType + ")"); } } } case 0x1: { //integer int length = (int) Math.pow(2, objInfo); return new NSNumber(this.bytes, offset + 1, offset + 1 + length, NSNumber.INTEGER); } case 0x2: { //real int length = (int) Math.pow(2, objInfo); return new NSNumber(this.bytes, offset + 1, offset + 1 + length, NSNumber.REAL); } case 0x3: { //Date if (objInfo != 0x3) { throw new PropertyListFormatException("The given binary property list contains a date object of an unknown type (" + objInfo + ")"); } return new NSDate(this.bytes, offset + 1, offset + 9); } case 0x4: { //Data int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int length = lengthAndOffset[0]; int dataOffset = lengthAndOffset[1]; return new NSData(copyOfRange(this.bytes, offset + dataOffset, offset + dataOffset + length)); } case 0x5: { //ASCII string int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int length = lengthAndOffset[0]; //Each character is 1 byte int strOffset = lengthAndOffset[1]; return new NSString(this.bytes, offset + strOffset, offset + strOffset + length, "ASCII"); } case 0x6: { //UTF-16-BE string int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int characters = lengthAndOffset[0]; int strOffset = lengthAndOffset[1]; //UTF-16 characters can have variable length, but the Core Foundation reference implementation //assumes 2 byte characters, thus only covering the Basic Multilingual Plane int length = characters * 2; return new NSString(this.bytes, offset + strOffset, offset + strOffset + length, "UTF-16BE"); } case 0x7: { //UTF-8 string (v1.0 and later) int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int strOffset = lengthAndOffset[1]; int characters = lengthAndOffset[0]; //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 int length = this.calculateUtf8StringLength(this.bytes, offset + strOffset, characters); return new NSString(this.bytes, offset + strOffset, offset + strOffset + length, "UTF-8"); } case 0x8: { //UID (v1.0 and later) int length = objInfo + 1; return new UID(String.valueOf(obj), copyOfRange(this.bytes, offset + 1, offset + 1 + length)); } case 0xA: { //Array int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int length = lengthAndOffset[0]; int arrayOffset = lengthAndOffset[1]; NSArray array = new NSArray(length); for (int i = 0; i < length; i++) { int objRef = (int) parseUnsignedInt(this.bytes, offset + arrayOffset + i * this.objectRefSize, offset + arrayOffset + (i + 1) * this.objectRefSize); array.setValue(i, this.parseObject(objRef)); } return array; } case 0xB: { //Ordered set (v1.0 and later) int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int length = lengthAndOffset[0]; int contentOffset = lengthAndOffset[1]; NSSet set = new NSSet(true); for (int i = 0; i < length; i++) { int objRef = (int) parseUnsignedInt(this.bytes, offset + contentOffset + i * this.objectRefSize, offset + contentOffset + (i + 1) * this.objectRefSize); set.addObject(this.parseObject(objRef)); } return set; } case 0xC: { //Set (v1.0 and later) int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int length = lengthAndOffset[0]; int contentOffset = lengthAndOffset[1]; NSSet set = new NSSet(); for (int i = 0; i < length; i++) { int objRef = (int) parseUnsignedInt(this.bytes, offset + contentOffset + i * this.objectRefSize, offset + contentOffset + (i + 1) * this.objectRefSize); set.addObject(this.parseObject(objRef)); } return set; } case 0xD: { //Dictionary int[] lengthAndOffset = this.readLengthAndOffset(objInfo, offset); int length = lengthAndOffset[0]; int contentOffset = lengthAndOffset[1]; NSDictionary dict = new NSDictionary(); for (int i = 0; i < length; i++) { int keyRef = (int) parseUnsignedInt(this.bytes, offset + contentOffset + i * this.objectRefSize, offset + contentOffset + (i + 1) * this.objectRefSize); int valRef = (int) parseUnsignedInt(this.bytes, offset + contentOffset + (length * this.objectRefSize) + i * this.objectRefSize, offset + contentOffset + (length * this.objectRefSize) + (i + 1) * this.objectRefSize); NSObject key = this.parseObject(keyRef); NSObject val = this.parseObject(valRef); assert key != null; //Encountering a null object at this point would either be a fundamental error in the parser or an error in the property list dict.put(key.toString(), val); } return dict; } default: { throw new PropertyListFormatException("The given binary property list contains an object of unknown type (" + objType + ")"); } } } /** * Reads the length for arrays, sets and dictionaries. * * @param objInfo Object information byte. * @param offset Offset in the byte array at which the object is located. * @return An array with the length two. First entry is the length, second entry the offset at which the content starts. */ private int[] readLengthAndOffset(int objInfo, int offset) { int lengthValue = objInfo; int offsetValue = 1; if (objInfo == 0xF) { int int_type = this.bytes[offset + 1]; int intType = (int_type & 0xF0) >> 4; if (intType != 0x1) { System.err.println("BinaryPropertyListParser: Length integer has an unexpected type" + intType + ". Attempting to parse anyway..."); } int intInfo = int_type & 0x0F; int intLength = (int) Math.pow(2, intInfo); offsetValue = 2 + intLength; if (intLength < 3) { lengthValue = (int) parseUnsignedInt(this.bytes, offset + 2, offset + 2 + intLength); } else { lengthValue = new BigInteger(copyOfRange(this.bytes, offset + 2, offset + 2 + intLength)).intValue(); } } return new int[]{lengthValue, offsetValue}; } private int calculateUtf8StringLength(byte[] bytes, int offset, int numCharacters) { int length = 0; for (int i = 0; i < numCharacters; i++) { int tempOffset = offset + length; if (bytes.length <= tempOffset) { //WARNING: Invalid UTF-8 string, fall back to length = number of characters return numCharacters; } if (Byte.toUnsignedInt(bytes[tempOffset]) < 0x80) { length++; } if (Byte.toUnsignedInt(bytes[tempOffset]) < 0xC2) { //Invalid value (marks continuation byte), fall back to length = number of characters return numCharacters; } else if (Byte.toUnsignedInt(bytes[tempOffset]) < 0xE0) { if ((bytes[tempOffset + 1] & 0xC0) != 0x80) { //Invalid continuation byte, fall back to length = number of characters return numCharacters; } length += 2; } else if (Byte.toUnsignedInt(bytes[tempOffset]) < 0xF0) { if ((bytes[tempOffset + 1] & 0xC0) != 0x80 || (bytes[tempOffset + 2] & 0xC0) != 0x80) { //Invalid continuation byte, fall back to length = number of characters return numCharacters; } length += 3; } else if (Byte.toUnsignedInt(bytes[tempOffset]) < 0xF5) { if ((bytes[tempOffset + 1] & 0xC0) != 0x80 || (bytes[tempOffset + 2] & 0xC0) != 0x80 || (bytes[tempOffset + 3] & 0xC0) != 0x80) { //Invalid continuation byte, fall back to length = number of characters return numCharacters; } length += 4; } } return length; } /** * Parses an unsigned integers from a byte array. * * @param bytes The byte array containing the unsigned integer. * @return The unsigned integer represented by the given bytes. */ @SuppressWarnings("unused") public static long parseUnsignedInt(byte[] bytes) { return parseUnsignedInt(bytes, 0, bytes.length); } /** * Parses an unsigned integer from a byte array. * * @param bytes The byte array containing the unsigned integer. * @param startIndex Beginning of the unsigned int in the byte array. * @param endIndex End of the unsigned int in the byte array. * @return The unsigned integer represented by the given bytes. */ public static long parseUnsignedInt(byte[] bytes, int startIndex, int endIndex) { long l = 0; for (int i = startIndex; i < endIndex; i++) { l <<= 8; l |= bytes[i] & 0xFF; } l &= 0xFFFFFFFFL; return l; } /** * Parses a long from a (big-endian) byte array. * * @param bytes The bytes representing the long integer. * @return The long integer represented by the given bytes. */ @SuppressWarnings("unused") public static long parseLong(byte[] bytes) { return parseLong(bytes, 0, bytes.length); } /** * Parses a long from a (big-endian) byte array. * * @param bytes The bytes representing the long integer. * @param startIndex Beginning of the long in the byte array. * @param endIndex End of the long in the byte array. * @return The long integer represented by the given bytes. */ public static long parseLong(byte[] bytes, int startIndex, int endIndex) { long l = 0; for (int i = startIndex; i < endIndex; i++) { l <<= 8; l |= bytes[i] & 0xFF; } return l; } /** * Parses a double from a (big-endian) byte array. * * @param bytes The bytes representing the double. * @return The double represented by the given bytes. */ @SuppressWarnings("unused") public static double parseDouble(byte[] bytes) { return parseDouble(bytes, 0, bytes.length); } /** * Parses a double from a (big-endian) byte array. * * @param bytes The bytes representing the double. * @param startIndex Beginning of the double in the byte array. * @param endIndex End of the double in the byte array. * @return The double represented by the given bytes. */ public static double parseDouble(byte[] bytes, int startIndex, int endIndex) { if (endIndex - startIndex == 8) { return Double.longBitsToDouble(parseLong(bytes, startIndex, endIndex)); } else if (endIndex - startIndex == 4) { return Float.intBitsToFloat((int) parseLong(bytes, startIndex, endIndex)); } else { throw new IllegalArgumentException("endIndex (" + endIndex + ") - startIndex (" + startIndex + ") != 4 or 8"); } } /** * Copies a part of a byte array into a new array. * * @param src The source array. * @param startIndex The index from which to start copying. * @param endIndex The index until which to copy. * @return The copied array. */ public static byte[] copyOfRange(byte[] src, int startIndex, int endIndex) { int length = endIndex - startIndex; if (length < 0) { throw new IllegalArgumentException("startIndex (" + startIndex + ")" + " > endIndex (" + endIndex + ")"); } byte[] dest = new byte[length]; System.arraycopy(src, startIndex, dest, 0, length); return dest; } private int getObjectOffset(int obj) throws PropertyListFormatException { if (obj > this.numObjects) { throw new PropertyListFormatException("The given binary property list contains an invalid object identifier."); } return (int) parseUnsignedInt(this.bytes, this.offsetTableOffset + obj * this.offsetSize, this.offsetTableOffset + (obj + 1) * this.offsetSize); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy