ch.randelshofer.quaqua.util.BinaryPListParser Maven / Gradle / Ivy
Show all versions of Quaqua Show documentation
/*
* @(#)BinaryPListParser.java
*
* Copyright (c) 2005-2010 Werner Randelshofer, Immensee, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package ch.randelshofer.quaqua.util;
import ch.randelshofer.quaqua.ext.nanoxml.*;
import ch.randelshofer.quaqua.ext.base64.*;
import java.io.*;
import java.math.BigInteger;
import java.util.*;
import java.text.*;
/**
* Reads a binary PList file and returns it as a NanoXML XMLElement.
*
* The NanoXML XMLElement returned by this reader is equivalent to the
* XMLElement returned, if a PList file in XML format is parsed with
* NanoXML.
*
* Description about property list taken from
* Apple's online documentation:
*
* "A property list is a data representation used by Mac OS X Cocoa and Core
* Foundation as a convenient way to store, organize, and access standard object
* types. Frequently called a plist, a property list is an object of one of
* several certain Cocoa or Core Foundation types, including arrays,
* dictionaries, strings, binary data, numbers, dates, and Boolean values. If
* the object is a container (an array or dictionary), all objects contained
* within it must also be supported property list objects. (Arrays and
* dictionaries can contain objects not supported by the architecture, but are
* then not property lists, and cannot be saved and restored with the various
* property list methods.)"
*
* XXX - This implementation can not read date values. Date values will always
* have the current date.
*
* @see ch.randelshofer.quaqua.ext.nanoxml.XMLElement
*
* @author Werner Randelshofer
* @version $Id: BinaryPListParser.java 363 2010-11-21 17:41:04Z wrandelshofer $
*/
public class BinaryPListParser {
private final static boolean DEBUG = false;
/* Description of the binary plist format derived from
* http://cvs.opendarwin.org/cgi-bin/cvsweb.cgi/~checkout~/src/CoreFoundation/Parsing.subproj/CFBinaryPList.c?rev=1.1.1.3&content-type=text/plain
*
* EBNF description of the file format:
*
* bplist ::= header objectTable offsetTable trailer
*
* header ::= magicNumber fileFormatVersion
* magicNumber ::= "bplist"
* fileFormatVersion ::= "00"
*
* objectTable ::= { null | bool | fill | number | date | data |
* string | uid | array | dict }
*
* null ::= 0b0000 0b0000
*
* bool ::= false | true
* false ::= 0b0000 0b1000
* true ::= 0b0000 0b1001
*
* fill ::= 0b0000 0b1111 // fill byte
*
* number ::= int | real
* int ::= 0b0001 0bnnnn byte*(2^nnnn) // 2^nnnn big-endian bytes
* real ::= 0b0010 0bnnnn byte*(2^nnnn) // 2^nnnn big-endian bytes
*
* date ::= 0b0011 0b0011 byte*8 // 8 byte float big-endian bytes
*
* data ::= 0b0100 0bnnnn [int] byte* // nnnn is number of bytes
* // unless 0b1111 then a int
* // variable-sized object follows
* // to indicate the number of bytes
*
* string ::= asciiString | unicodeString
* asciiString ::= 0b0101 0bnnnn [int] byte*
* unicodeString ::= 0b0110 0bnnnn [int] short*
* // nnnn is number of bytes
* // unless 0b1111 then a int
* // variable-sized object follows
* // to indicate the number of bytes
*
* uid ::= 0b1000 0bnnnn byte* // nnnn+1 is # of bytes
*
* array ::= 0b1010 0bnnnn [int] objref* //
* // nnnn is number of objref
* // unless 0b1111 then a int
* // variable-sized object follows
* // to indicate the number of objref
*
* dict ::= 0b1010 0bnnnn [int] keyref* objref*
* // nnnn is number of keyref and
* // objref pairs
* // unless 0b1111 then a int
* // variable-sized object follows
* // to indicate the number of pairs
*
* objref = byte | short // if refCount
* // is less than 256 then objref is
* // an unsigned byte, otherwise it
* // is an unsigned big-endian short
*
* keyref = byte | short // if refCount
* // is less than 256 then objref is
* // an unsigned byte, otherwise it
* // is an unsigned big-endian short
*
* unused ::= 0b0111 0bxxxx | 0b1001 0bxxxx |
* 0b1011 0bxxxx | 0b1100 0bxxxx |
* 0b1110 0bxxxx | 0b1111 0bxxxx
*
*
* offsetTable ::= { int } // list of ints, byte size of which
* // is given in trailer
* // these are the byte offsets into
* // the file
* // number of these is in the trailer
*
* trailer ::= refCount offsetCount objectCount topLevelOffset
*
* refCount ::= byte*8 // unsigned big-endian long
* offsetCount ::= byte*8 // unsigned big-endian long
* objectCount ::= byte*8 // unsigned big-endian long
* topLevelOffset ::= byte*8 // unsigned big-endian long
*
*/
/**
* Total count of objrefs and keyrefs.
*/
private int refCount;
/**
* Total count of ofsets.
*/
private int offsetCount;
/**
* Total count of objects.
*/
private int objectCount;
/**
* Offset in file of top level offset in offset table.
*/
private int topLevelOffset;
/**
* Object table.
* We gradually fill in objects from the binary PList object table into
* this list.
*/
private ArrayList objectTable;
/** Holder for a binary PList Uid element. */
private static class BPLUid {
private final int number;
public BPLUid(int number) {
super();
this.number = number;
}
public int getNumber() {
return number;
}
}
/**
* Holder for a binary PList array element.
*/
private static class BPLArray {
ArrayList objectTable;
int[] objref;
public Object getValue(int i) {
return objectTable.get(objref[i]);
}
@Override
public String toString() {
StringBuffer buf = new StringBuffer("Array{");
for (int i = 0; i < objref.length; i++) {
if (i > 0) {
buf.append(',');
}
if (objectTable.size() > objref[i]
&& objectTable.get(objref[i]) != this) {
buf.append(objectTable.get(objref[i]));
} else {
buf.append("*" + objref[i]);
}
}
buf.append('}');
return buf.toString();
}
}
/**
* Holder for a binary PList dict element.
*/
private static class BPLDict {
ArrayList objectTable;
int[] keyref;
int[] objref;
public String getKey(int i) {
return objectTable.get(keyref[i]).toString();
}
public Object getValue(int i) {
return objectTable.get(objref[i]);
}
@Override
public String toString() {
StringBuffer buf = new StringBuffer("BPLDict{");
for (int i = 0; i < keyref.length; i++) {
if (i > 0) {
buf.append(',');
}
if (keyref[i] < 0 || keyref[i] >= objectTable.size()) {
buf.append("#" + keyref[i]);
} else if (objectTable.get(keyref[i]) == this) {
buf.append("*" + keyref[i]);
} else {
buf.append(objectTable.get(keyref[i]));
//buf.append(keyref[i]);
}
buf.append(":");
if (objref[i] < 0 || objref[i] >= objectTable.size()) {
buf.append("#" + objref[i]);
} else if (objectTable.get(objref[i]) == this) {
buf.append("*" + objref[i]);
} else {
buf.append(objectTable.get(objref[i]));
//buf.append(objref[i]);
}
}
buf.append('}');
return buf.toString();
}
}
/**
* Creates a new instance.
*/
public BinaryPListParser() {
}
/**
* Parses a binary PList file and turns it into a XMLElement.
* The XMLElement is equivalent with a XML PList file parsed using
* NanoXML.
*
* @param file A file containing a binary PList.
* @return Returns the parsed XMLElement.
*/
public XMLElement parse(File file) throws IOException {
RandomAccessFile raf = null;
byte[] buf = null;
try {
raf = new RandomAccessFile(file, "r");
// Parse the HEADER
// ----------------
// magic number ("bplist")
// file format version ("00")
int bpli = raf.readInt();
int st00 = raf.readInt();
if (bpli != 0x62706c69 || st00 != 0x73743030) {
throw new IOException("parseHeader: File does not start with 'bplist00' magic.");
}
// Parse the TRAILER
// ----------------
// byte size of offset ints in offset table
// byte size of object refs in arrays and dicts
// number of offsets in offset table (also is number of objects)
// element # in offset table which is top level object
raf.seek(raf.length() - 32);
// count of offset ints in offset table
offsetCount = (int) raf.readLong();
// count of object refs in arrays and dicts
refCount = (int) raf.readLong();
// count of offsets in offset table (also is number of objects)
objectCount = (int) raf.readLong();
// element # in offset table which is top level object
topLevelOffset = (int) raf.readLong();
buf = new byte[topLevelOffset - 8];
raf.seek(8);
raf.readFully(buf);
} finally {
if (raf != null) {
raf.close();
}
}
// Parse the OBJECT TABLE
// ----------------------
objectTable = new ArrayList();
DataInputStream in = null;
try {
in = new DataInputStream(
new ByteArrayInputStream(buf));
parseObjectTable(in);
} finally {
if (in != null) {
in.close();
}
}
// Convert the object table to XML and return it
XMLElement root = new XMLElement(new HashMap(), false, false);
root.setName("plist");
root.setAttribute("version", "1.0");
convertObjectTableToXML(root, objectTable.get(0));
return root;
}
/**
* Converts the object table in the binary PList into an XMLElement.
*/
private void convertObjectTableToXML(XMLElement parent, Object object) {
XMLElement elem = parent.createAnotherElement();
if (object instanceof BPLDict) {
BPLDict dict = (BPLDict) object;
elem.setName("dict");
for (int i = 0; i < dict.keyref.length; i++) {
XMLElement key = parent.createAnotherElement();
key.setName("key");
key.setContent(dict.getKey(i));
elem.addChild(key);
convertObjectTableToXML(elem, dict.getValue(i));
}
} else if (object instanceof BPLArray) {
BPLArray arr = (BPLArray) object;
elem.setName("array");
for (int i = 0; i < arr.objref.length; i++) {
convertObjectTableToXML(elem, arr.getValue(i));
}
} else if (object instanceof String) {
elem.setName("string");
elem.setContent((String) object);
} else if (object instanceof Integer) {
elem.setName("integer");
elem.setContent(object.toString());
} else if (object instanceof Long) {
elem.setName("integer");
elem.setContent(object.toString());
} else if (object instanceof Float) {
elem.setName("real");
elem.setContent(object.toString());
} else if (object instanceof Double) {
elem.setName("real");
elem.setContent(object.toString());
} else if (object instanceof Boolean) {
elem.setName("boolean");
elem.setContent(object.toString());
} else if (object instanceof byte[]) {
elem.setName("data");
elem.setContent(Base64.encodeBytes((byte[]) object));
} else if (object instanceof Date) {
elem.setName("date");
DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
elem.setContent(format.format((Date) object));
} else if (object instanceof BPLUid) {
elem.setName("UID");
elem.setContent(Integer.toString(((BPLUid) object).getNumber()));
} else {
elem.setName("unsupported");
elem.setContent(object.toString());
}
parent.addChild(elem);
}
/**
* Object Formats (marker byte followed by additional info in some cases)
* null 0000 0000
* bool 0000 1000 // false
* bool 0000 1001 // true
* fill 0000 1111 // fill byte
* int 0001 nnnn ... // # of bytes is 2^nnnn, big-endian bytes
* real 0010 nnnn ... // # of bytes is 2^nnnn, big-endian bytes
* date 0011 0011 ... // 8 byte float follows, big-endian bytes
* data 0100 nnnn [int] ... // nnnn is number of bytes unless 1111 then int count follows, followed by bytes
* string 0101 nnnn [int] ... // ASCII string, nnnn is # of chars, else 1111 then int count, then bytes
* string 0110 nnnn [int] ... // Unicode string, nnnn is # of chars, else 1111 then int count, then big-endian 2-byte shorts
* 0111 xxxx // unused
* uid 1000 nnnn ... // nnnn+1 is # of bytes
* 1001 xxxx // unused
* array 1010 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows
* 1011 xxxx // unused
* 1100 xxxx // unused
* dict 1101 nnnn [int] keyref* objref* // nnnn is count, unless '1111', then int count follows
* 1110 xxxx // unused
* 1111 xxxx // unused
*/
private void parseObjectTable(DataInputStream in) throws IOException {
int marker;
while ((marker = in.read()) != -1) {
switch ((marker & 0xf0) >> 4) {
case 0: {
parsePrimitive(in, marker & 0xf);
break;
}
case 1: {
int count = 1 << (marker & 0xf);
parseInt(in, count);
break;
}
case 2: {
int count = 1 << (marker & 0xf);
parseReal(in, count);
break;
}
case 3: {
if ((marker & 0xf) != 3) {
throw new IOException("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
}
parseDate(in);
break;
}
case 4: {
int count = marker & 0xf;
if (count == 15) {
count = readCount(in);
}
parseData(in, count);
break;
}
case 5: {
int count = marker & 0xf;
if (count == 15) {
count = readCount(in);
}
parseAsciiString(in, count);
break;
}
case 6: {
int count = marker & 0xf;
if (count == 15) {
count = readCount(in);
}
parseUnicodeString(in, count);
break;
}
case 7: {
if (DEBUG) {
System.out.println("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
}
return;
// throw new IOException("parseObjectTable: illegal marker "+Integer.toBinaryString(marker));
//break;
}
case 8: {
int count = (marker & 0xf) + 1;
if (DEBUG) {
System.out.println("uid " + count);
}
parseUID(in, count);
break;
}
case 9: {
throw new IOException("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
//break;
}
case 10: {
int count = marker & 0xf;
if (count == 15) {
count = readCount(in);
}
if (refCount > 255) {
parseShortArray(in, count);
} else {
parseByteArray(in, count);
}
break;
}
case 11: {
throw new IOException("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
//break;
}
case 12: {
throw new IOException("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
//break;
}
case 13: {
int count = marker & 0xf;
if (count == 15) {
count = readCount(in);
}
if (refCount > 256) {
parseShortDict(in, count);
} else {
parseByteDict(in, count);
}
break;
}
case 14: {
throw new IOException("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
//break;
}
case 15: {
throw new IOException("parseObjectTable: illegal marker " + Integer.toBinaryString(marker));
//break;
}
}
}
}
/**
* Reads a count value from the object table. Count values are encoded
* using the following scheme:
*
* int 0001 nnnn ... // # of bytes is 2^nnnn, big-endian bytes
*/
private int readCount(DataInputStream in) throws IOException {
int marker = in.read();
if (marker == -1) {
throw new IOException("variableLengthInt: Illegal EOF in marker");
}
if (((marker & 0xf0) >> 4) != 1) {
throw new IOException("variableLengthInt: Illegal marker " + Integer.toBinaryString(marker));
}
int count = 1 << (marker & 0xf);
int value = 0;
for (int i = 0; i < count; i++) {
int b = in.read();
if (b == -1) {
throw new IOException("variableLengthInt: Illegal EOF in value");
}
value = (value << 8) | b;
}
return value;
}
/**
* null 0000 0000
* bool 0000 1000 // false
* bool 0000 1001 // true
* fill 0000 1111 // fill byte
*/
private void parsePrimitive(DataInputStream in, int primitive) throws IOException {
switch (primitive) {
case 0:
objectTable.add(null);
break;
case 8:
objectTable.add(Boolean.FALSE);
break;
case 9:
objectTable.add(Boolean.TRUE);
break;
case 15:
// fill byte: don't add to object table
break;
default:
throw new IOException("parsePrimitive: illegal primitive " + Integer.toBinaryString(primitive));
}
}
/**
* array 1010 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows
*/
private void parseByteArray(DataInputStream in, int count) throws IOException {
BPLArray arr = new BPLArray();
arr.objectTable = objectTable;
arr.objref = new int[count];
for (int i = 0; i < count; i++) {
arr.objref[i] = in.readByte() & 0xff;
if (arr.objref[i] == -1) {
throw new IOException("parseByteArray: illegal EOF in objref*");
}
}
objectTable.add(arr);
}
/**
* array 1010 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows
*/
private void parseShortArray(DataInputStream in, int count) throws IOException {
BPLArray arr = new BPLArray();
arr.objectTable = objectTable;
arr.objref = new int[count];
for (int i = 0; i < count; i++) {
arr.objref[i] = in.readShort() & 0xffff;
if (arr.objref[i] == -1) {
throw new IOException("parseShortArray: illegal EOF in objref*");
}
}
objectTable.add(arr);
}
/*
* data 0100 nnnn [int] ... // nnnn is number of bytes unless 1111 then int count follows, followed by bytes
*/
private void parseData(DataInputStream in, int count) throws IOException {
byte[] data = new byte[count];
in.readFully(data);
objectTable.add(data);
}
/**
* byte dict 1101 nnnn keyref* objref* // nnnn is less than '1111'
*/
private void parseByteDict(DataInputStream in, int count) throws IOException {
BPLDict dict = new BPLDict();
dict.objectTable = objectTable;
dict.keyref = new int[count];
dict.objref = new int[count];
for (int i = 0; i < count; i++) {
dict.keyref[i] = in.readByte() & 0xff;
}
for (int i = 0; i < count; i++) {
dict.objref[i] = in.readByte() & 0xff;
}
objectTable.add(dict);
}
/**
* short dict 1101 ffff int keyref* objref* // int is count
*/
private void parseShortDict(DataInputStream in, int count) throws IOException {
BPLDict dict = new BPLDict();
dict.objectTable = objectTable;
dict.keyref = new int[count];
dict.objref = new int[count];
for (int i = 0; i < count; i++) {
dict.keyref[i] = in.readShort() & 0xffff;
}
for (int i = 0; i < count; i++) {
dict.objref[i] = in.readShort() & 0xffff;
}
objectTable.add(dict);
}
/**
* string 0101 nnnn [int] ... // ASCII string, nnnn is # of chars, else 1111 then int count, then bytes
*/
private void parseAsciiString(DataInputStream in, int count) throws IOException {
byte[] buf = new byte[count];
in.readFully(buf);
String str = new String(buf, "ASCII");
objectTable.add(str);
}
private void parseUID(DataInputStream in, int count) throws IOException {
if (count > 4) {
throw new IOException("parseUID: unsupported byte count: "+count);
}
byte[] uid = new byte[count];
in.readFully(uid);
objectTable.add(new BPLUid(new BigInteger(uid).intValue()));
}
/**
* int 0001 nnnn ... // # of bytes is 2^nnnn, big-endian bytes
*/
private void parseInt(DataInputStream in, int count) throws IOException {
if (count > 8) {
throw new IOException("parseInt: unsupported byte count: " + count);
}
long value = 0;
for (int i = 0; i < count; i++) {
int b = in.read();
if (b == -1) {
throw new IOException("parseInt: Illegal EOF in value");
}
value = (value << 8) | b;
}
objectTable.add(value);
}
/**
* real 0010 nnnn ... // # of bytes is 2^nnnn, big-endian bytes
*/
private void parseReal(DataInputStream in, int count) throws IOException {
switch (count) {
case 4:
objectTable.add(new Float(in.readFloat()));
break;
case 8:
objectTable.add(new Double(in.readDouble()));
break;
default:
throw new IOException("parseReal: unsupported byte count:" + count);
}
}
/**
* date 0011 0011 ... // 8 byte float follows, big-endian bytes
*/
private void parseDate(DataInputStream in) throws IOException {
// XXX - This does not yield a date :(
double date = in.readDouble();
//objectTable.add(new Date((long) date));
objectTable.add(new Date());
}
/**
* string 0110 nnnn [int] ... // Unicode string, nnnn is # of chars, else 1111 then int count, then big-endian 2-byte shorts
*/
private void parseUnicodeString(DataInputStream in, int count) throws IOException {
char[] buf = new char[count];
for (int i = 0; i < count; i++) {
buf[i] = in.readChar();
}
String str = new String(buf);
objectTable.add(str);
}
}